diff --git a/Cargo.lock b/Cargo.lock index 1895bb5..03efc80 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3913,15 +3913,13 @@ dependencies = [ [[package]] name = "rodio" version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6006a627c1a38d37f3d3a85c6575418cfe34a5392d60a686d0071e1c8d427acb" +source = "git+https://github.com/RustAudio/rodio.git#d346cfa65bb09a053d2ecd9a9994391f3eeb70db" dependencies = [ "claxon", "cpal", "hound", "lewton", "symphonia", - "thiserror", ] [[package]] @@ -4757,6 +4755,7 @@ dependencies = [ "symphonia-bundle-mp3", "symphonia-codec-aac", "symphonia-codec-adpcm", + "symphonia-codec-alac", "symphonia-codec-pcm", "symphonia-codec-vorbis", "symphonia-core", @@ -4810,6 +4809,16 @@ dependencies = [ "symphonia-core", ] +[[package]] +name = "symphonia-codec-alac" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d8a6666649a08412906476a8b0efd9b9733e241180189e9f92b09c08d0e38f3" +dependencies = [ + "log", + "symphonia-core", +] + [[package]] name = "symphonia-codec-pcm" version = "0.5.4" diff --git a/Cargo.toml b/Cargo.toml index bd3c419..57b135c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,12 @@ log = "0.4" midi-player = "0.2.1" quick-xml = "0.31.0" rfd = "0.14.0" -rodio = { version = "0.19", features = ["symphonia-all"] } +# it's git for now, because the version supporting aiff have not released yet +rodio = { git = "https://github.com/RustAudio/rodio.git", features = [ + "symphonia-all", + "symphonia-aiff", + "symphonia-alac", +] } rustpython-pylib = { version = "0.4.0", features = ["freeze-stdlib"] } rustpython-stdlib = "0.4.0" rustpython-vm = { version = "0.4.0", features = ["freeze-stdlib"] } diff --git a/doc/src/SUMMARY.md b/doc/src/SUMMARY.md index 3a0a428..63edf55 100644 --- a/doc/src/SUMMARY.md +++ b/doc/src/SUMMARY.md @@ -31,10 +31,6 @@ - [Copying and Removing PathInstances](chapter03/04-copying-and-removing-pathinstances.md) - [Editing PathInstances](chapter03/05-editing-pathinstances.md) - [Tutorial 4: Creating and Editing Textures](chapter04/01-tutorial-4-creating-and-editing-textures.md) - - [Editing ParameterObjects](chapter04/010-editing-parameterobjects.md) - - [Editing Rhythm ParameterObjects](chapter04/011-editing-rhythm-parameterobjects.md) - - [Editing Instruments and Altering EventMode](chapter04/012-editing-instruments-and-altering-eventmode.md) - - [Displaying Texture Parameter Values](chapter04/013-displaying-texture-parameter-values.md) - [Introduction to Textures and ParameterObjects](chapter04/02-introduction-to-textures-and-parameterobjects.md) - [Introduction Instrument Models](chapter04/03-introduction-instrument-models.md) - [Selecting and Viewing TextureModules](chapter04/04-selecting-and-viewing-texturemodules.md) @@ -43,6 +39,10 @@ - [Editing TextureInstance Attributes](chapter04/07-editing-textureinstance-attributes.md) - [Muting Textures](chapter04/08-muting-textures.md) - [Viewing and Searching ParameterObjects](chapter04/09-viewing-and-searching-parameterobjects.md) + - [Editing ParameterObjects](chapter04/010-editing-parameterobjects.md) + - [Editing Rhythm ParameterObjects](chapter04/011-editing-rhythm-parameterobjects.md) + - [Editing Instruments and Altering EventMode](chapter04/012-editing-instruments-and-altering-eventmode.md) + - [Displaying Texture Parameter Values](chapter04/013-displaying-texture-parameter-values.md) - [Tutorial 5: Textures and Paths](chapter05/01-tutorial-5-textures-and-paths.md) - [Path Linking and Pitch Formation Redundancy](chapter05/02-path-linking-and-pitch-formation-redundancy.md) - [Creating a Path With a Duration Fraction](chapter05/03-creating-a-path-with-a-duration-fraction.md) diff --git a/pysrc/athenaCL/libATH/command.py b/pysrc/athenaCL/libATH/command.py index e32b942..63abb31 100644 --- a/pysrc/athenaCL/libATH/command.py +++ b/pysrc/athenaCL/libATH/command.py @@ -6758,54 +6758,48 @@ def __init__(self, ao, args="", **keywords): self.processSwitch = 1 # display only self.gatherSwitch = 1 # display only self.cmdStr = "ELh" + self.audioPath = None + self.midiPath = None def gather(self): outComplete = self.ao.aoInfo["outComplete"] if outComplete == []: return lang.msgELcreateFirst - self.fmtFound = [] msg = [] # all outputs that produce an audio file - self.audioPath = self.ao.aoInfo["fpAudio"] if "csoundData" in outComplete or "csoundScore" in outComplete: - if os.path.isfile(self.audioPath): # if file found - self.fmtFound.append(self.audioPath) + audioPath = self.ao.aoInfo["fpAudio"] + if os.path.isfile(audioPath): # if file found + self.audioPath = audioPath else: # files was created but does not exists - msg.append(lang.msgELaudioMoved % self.audioPath) + msg.append(lang.msgELaudioMoved % audioPath) # only midi file produces an output - self.midPath = self.ao.aoInfo["fpMidi"] if "midiFile" in outComplete: - if os.path.isfile(self.midPath): # if file found - self.fmtFound.append(self.midPath) + midiPath = self.ao.aoInfo["fpMidi"] + if os.path.isfile(midiPath): # if file found + self.midiPath = midiPath else: - msg.append(lang.msgELaudioMoved % self.midPath) - if self.fmtFound == []: + msg.append(lang.msgELaudioMoved % midiPath) + + if not self.midiPath and not self.audioPath: return "".join(msg) def process(self): - # this file render prep should be done after EMr - # if os.name == 'mac': # must check that fp is in found - # if self.audioPath in self.fmtFound: - # prefDict = self.ao.external.getPrefGroup('external') - # audioTools.setMacAudioRsrc(prefDict['audioFormat'], - # self.audioPath, prefDict) - if os.name == "posix": - pass - else: # win or other - pass + pass def display(self): msg = [] - prefDict = self.ao.external.getPrefGroup("external") - for hPath in self.fmtFound: # audio path - failFlag = osTools.openMedia(hPath, prefDict) - if failFlag == "failed": - msg.append(lang.msgELhearError % hPath) - else: - msg.append(lang.msgELhearInit % hPath) + if self.audioPath: + dialogExt.playAudio(self.audioPath) + msg.append("EL hear (Audio) %s\n" % self.audioPath) + + if self.midiPath: + dialogExt.playMidi(self.midiPath) + msg.append("EL hear (MIDI) %s\n" % self.midiPath) + return "".join(msg) diff --git a/src/app/app.rs b/src/app/app.rs index 1701b87..534ccc4 100644 --- a/src/app/app.rs +++ b/src/app/app.rs @@ -9,7 +9,7 @@ use iced::widget::{ use iced::{time, Element, Font, Subscription, Task}; use rfd::FileDialog; -use super::player::{self, GlobalState as GlobalPlayerState, State as PlayerState}; +use super::player::{self, GlobalState as GlobalPlayerState, Track as PlayerState}; use crate::interpreter; const TERM_WIDTH: u16 = 80; @@ -150,6 +150,14 @@ pub fn update(state: &mut State, message: Message) -> Task { position: 0.0, })); } + interpreter::Message::LoadAudio(path) => { + state.output.push(Output::Player(PlayerState { + is_playing: false, + path: path.into(), + id: player::PlayerId::Audio(state.output.len()), + position: 0.0, + })); + } interpreter::Message::ScratchDir(value) => { state.scratch_dir = value; } diff --git a/src/app/player.rs b/src/app/player.rs index ade4618..fb1ed47 100644 --- a/src/app/player.rs +++ b/src/app/player.rs @@ -1,4 +1,9 @@ +use std::collections::HashMap; +use std::error::Error; +use std::fs::File; +use std::io::BufReader; use std::path::PathBuf; +use std::time::Duration; use cpal::{ traits::{DeviceTrait, HostTrait, StreamTrait}, @@ -11,14 +16,18 @@ use iced::{ }; use iced_aw::number_input; use midi_player::{Player, PlayerController, Settings as PlayerSettings}; +use rodio::{source::Source, Decoder, OutputStream, OutputStreamHandle, Sink}; use super::app; pub(crate) struct GlobalState { - controller: PlayerController, + midi_player_controller: PlayerController, _audio_stream: AudioStream, - playing_id: Option, + _output_stream: OutputStream, + stream_handle: OutputStreamHandle, + playing_track: Option, tempo: u16, + audio_player_cache: HashMap, } impl GlobalState { @@ -26,21 +35,22 @@ impl GlobalState { let settings = PlayerSettings::builder().build(); let (player, controller) = Player::new(sf, settings) .expect("midi player should be initialized with default settings and soundfont"); - let audio_stream = Self::start_audio_loop(player); + let audio_stream = Self::start_midi_renderer(player); + let (_output_stream, stream_handle) = + OutputStream::try_default().expect("Default audio stream should work"); Self { - controller, + midi_player_controller: controller, _audio_stream: audio_stream, - playing_id: None, + playing_track: None, tempo: 120, + _output_stream, + stream_handle, + audio_player_cache: HashMap::new(), } } - pub(crate) fn playing(&self) -> bool { - self.controller.is_playing() - } - - fn start_audio_loop(mut player: Player) -> AudioStream { + fn start_midi_renderer(mut player: Player) -> AudioStream { let host = cpal::default_host(); let device = host .default_output_device() @@ -82,18 +92,220 @@ impl GlobalState { stream } + fn play(&mut self, track: &mut Track) -> Result<(), Box> { + if !track.path.exists() { + track.is_playing = false; + return Ok(()); // this is handled by gui + } + + match track.id { + PlayerId::Midi(_) => self.play_midi(track)?, + PlayerId::Audio(_) => self.play_audio(track), + } + + track.is_playing = true; + self.playing_track = Some(track.id); + + Ok(()) + } + + fn play_midi(&mut self, track: &Track) -> Result<(), Box> { + self.midi_player_controller.set_file(Some(&track.path))?; + self.midi_player_controller.set_position(track.position); + self.set_tempo(self.tempo); + self.midi_player_controller.play(); + + Ok(()) + } + + fn play_audio(&mut self, track: &Track) { + // the path has checked for existence at this point + let controller = self + .audio_player_cache + .entry(track.path.clone()) + .or_insert_with(|| AudioPlayerController::new(track, &self.stream_handle)); + controller.set_position(track.position); + controller.play(track, &self.stream_handle); + } + + fn pause(&mut self, track: &mut Track) { + match track.id { + PlayerId::Midi(_) => self.midi_player_controller.stop(), + PlayerId::Audio(_) => { + if let Some(controller) = self.audio_player_cache.get(&track.path) { + controller.pause(); + } + } + } + + track.is_playing = false; + if let Some(playing_id) = self.playing_track { + if playing_id == track.id { + self.playing_track = None; + } + } + } + + fn set_position(&mut self, track: &mut Track, position: f64) { + track.position = position; + + if let Some(playing_id) = self.playing_track { + if playing_id == track.id { + match playing_id { + PlayerId::Midi(_) => self.midi_player_controller.set_position(position), + PlayerId::Audio(_) => { + if let Some(controller) = self.audio_player_cache.get(&track.path) { + controller.set_position(position); + } + } + } + } + } + } + pub(crate) fn tempo(&self) -> u16 { self.tempo } pub(crate) fn set_tempo(&mut self, tempo: u16) { self.tempo = tempo; - self.controller.set_tempo(tempo as f32); + self.midi_player_controller.set_tempo(tempo as f32); } - pub(crate) fn update_tempo(&mut self) { - self.set_tempo(self.tempo); + pub(crate) fn playing(&self) -> bool { + self.playing_track.is_some() + } + + fn on_tick(&mut self, track: &mut Track) { + let position = match track.id { + PlayerId::Midi(_) => self.midi_player_controller.position(), + PlayerId::Audio(_) => self + .audio_player_cache + .get(&track.path) + .map(|c| c.position()) + .unwrap_or_default(), + }; + + track.position = position; + + if position >= 1.0 { + track.is_playing = false; + track.position = 0.0; + self.playing_track = None; + } + } +} + +// we need this type to keep the duration of the file +struct AudioPlayerController { + sink: Sink, + duration: Duration, +} + +impl AudioPlayerController { + fn new(track: &Track, stream_handle: &OutputStreamHandle) -> Self { + let file = File::open(&track.path).unwrap(); + let file = BufReader::new(file); + let source = Decoder::new(file).expect("there should not be unsupported file formats"); + let duration = source + .total_duration() + .expect("duration is finite and known for an audio file"); + let sink = + Sink::try_new(stream_handle).expect("sink should be initialized from default stream"); + sink.append(source); + Self { sink, duration } + } + + fn play(&mut self, track: &Track, stream_handle: &OutputStreamHandle) { + self.maybe_reinit(track, stream_handle); + self.sink.play() + } + + fn pause(&self) { + self.sink.pause() + } + + fn position(&self) -> f64 { + self.sink.get_pos().as_secs_f64() / self.duration.as_secs_f64() + } + + fn set_position(&self, position: f64) { + let position = self.duration.mul_f64(position); + self.sink + .try_seek(position) + .expect("seek should work for an audio file"); + } + + /// When the file plays to the end, sink becomes empty and can't play this file anymore. This + /// function reinitializes the sink in this case. + /// + /// It should be called before play. + fn maybe_reinit(&mut self, track: &Track, stream_handle: &OutputStreamHandle) { + if self.sink.len() > 0 { + return; + } + let file = File::open(&track.path).unwrap(); + let file = BufReader::new(file); + let source = Decoder::new(file).expect("there should not be unsupported file formats"); + let duration = source + .total_duration() + .expect("duration is finite and known for an audio file"); + let sink = + Sink::try_new(stream_handle).expect("sink should be initialized from default stream"); + sink.append(source); + + self.sink = sink; + self.duration = duration; + } +} + +pub(crate) fn update( + output: &mut Vec, + state: &mut GlobalState, + message: Message, +) -> Task { + match message { + Message::Play(track_id) => { + // if another file is playing - stop it + if let Some(app::Output::Player(track)) = state + .playing_track + .and_then(|playing_track| output.get_mut(playing_track.inner())) + { + state.pause(track); + } + + // play + if let Some(app::Output::Player(track)) = output.get_mut(track_id.inner()) { + if let Err(e) = state.play(track) { + output.push(app::Output::Error(e.to_string())); + return Task::none(); + } + } + } + Message::Pause(id) => { + if let Some(app::Output::Player(track)) = output.get_mut(id.inner()) { + state.pause(track); + } + } + Message::ChangePosition(id, position) => { + if let Some(app::Output::Player(track)) = output.get_mut(id.inner()) { + state.set_position(track, position); + } + } + Message::SetTempo(tempo) => { + state.set_tempo(tempo); + } + Message::Tick(_) => { + if let Some(app::Output::Player(track)) = state + .playing_track + .and_then(|id| output.get_mut(id.inner())) + { + state.on_tick(track); + } + } } + + Task::none() } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -121,7 +333,7 @@ impl From for usize { } #[derive(Debug)] -pub(crate) struct State { +pub(crate) struct Track { pub(crate) is_playing: bool, pub(crate) path: PathBuf, pub(crate) id: PlayerId, @@ -137,84 +349,7 @@ pub enum Message { Tick(time::Instant), } -pub(crate) fn update( - output: &mut Vec, - state: &mut GlobalState, - message: Message, -) -> Task { - match message { - Message::Play(id) => { - if let Some(playing_id) = state.playing_id { - if let Some(app::Output::Player(player_state)) = output.get_mut(playing_id.inner()) - { - player_state.is_playing = false; - state.playing_id = None; - } - } - - if let Some(app::Output::Player(player)) = output.get_mut(id.inner()) { - if player.path.exists() { - player.is_playing = true; - if let Err(e) = state.controller.set_file(Some(&player.path)) { - output.push(app::Output::Error(e.to_string())); - return Task::none(); - } - state.controller.set_position(player.position); - state.update_tempo(); - state.controller.play(); - state.playing_id = Some(id); - } else { - player.is_playing = false; - } - } - } - Message::Pause(id) => { - if let Some(app::Output::Player(player_state)) = output.get_mut(id.inner()) { - player_state.is_playing = false; - state.controller.stop(); - if let Some(playing_id) = state.playing_id { - if playing_id == id { - state.playing_id = None; - } - } - } - } - Message::ChangePosition(id, position) => { - if let Some(app::Output::Player(player_state)) = output.get_mut(id.inner()) { - player_state.position = position; - - if let Some(playing_id) = state.playing_id { - if playing_id == id { - state.controller.set_position(position); - } - } - } - } - Message::SetTempo(tempo) => { - state.set_tempo(tempo); - } - Message::Tick(_) => { - if let Some(playing_id) = state.playing_id { - if let Some(app::Output::Player(player_state)) = output.get_mut(playing_id.inner()) - { - let position = state.controller.position(); - - player_state.position = position; - - if position >= 1.0 { - player_state.is_playing = false; - player_state.position = 0.0; - state.playing_id = None; - } - } - } - } - } - - Task::none() -} - -pub(crate) fn view(state: &State) -> Element { +pub(crate) fn view(state: &Track) -> Element { let disabled = !state.path.exists() && !state.is_playing; let label = text(if state.is_playing { "" } else { "" }) .font(iced_fonts::NERD_FONT) diff --git a/src/interpreter/dialog_ext.rs b/src/interpreter/dialog_ext.rs index 3cb0999..e5db155 100644 --- a/src/interpreter/dialog_ext.rs +++ b/src/interpreter/dialog_ext.rs @@ -145,4 +145,14 @@ pub(super) mod _inner { Ok(()) } + + #[pyfunction(name = "playAudio")] + pub(crate) fn play_audio(path: String) -> PyResult<()> { + interpreter::INTERPRETER_WORKER + .gui_sender + .send_blocking(interpreter::Message::LoadAudio(path)) + .expect("cannot send message via channel"); + + Ok(()) + } } diff --git a/src/interpreter/interpreter.rs b/src/interpreter/interpreter.rs index 0a31bd9..3188978 100644 --- a/src/interpreter/interpreter.rs +++ b/src/interpreter/interpreter.rs @@ -89,6 +89,10 @@ pub enum Message { /// /// The value is the path to the file. LoadMidi(String), + /// Play an Audio file (in the output area). + /// + /// The value is the path to the file. + LoadAudio(String), /// Get scratch dir. GetScratchDir, /// The result of `Self::GetScratchDir`. diff --git a/src/interpreter/mod.rs b/src/interpreter/mod.rs index 2ed104b..df4c953 100644 --- a/src/interpreter/mod.rs +++ b/src/interpreter/mod.rs @@ -1,7 +1,7 @@ //! athenaCL interpreter. pub use interpreter::*; -mod dialog_ext; mod athena_obj_ext; -mod xml_tools_ext; +mod dialog_ext; mod interpreter; +mod xml_tools_ext;