diff --git a/Cargo.lock b/Cargo.lock index 8632a2e..1895bb5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -447,6 +447,7 @@ dependencies = [ "midi-player", "quick-xml 0.31.0", "rfd", + "rodio", "rustpython-pylib", "rustpython-stdlib", "rustpython-vm", @@ -519,7 +520,7 @@ dependencies = [ "bitflags 2.6.0", "cexpr", "clang-sys", - "itertools 0.11.0", + "itertools 0.13.0", "proc-macro2", "quote", "regex", @@ -670,6 +671,12 @@ dependencies = [ "syn 2.0.80", ] +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.7.2" @@ -780,6 +787,12 @@ dependencies = [ "libloading 0.8.5", ] +[[package]] +name = "claxon" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bfbf56724aa9eca8afa4fcfadeb479e722935bb2a0900c2d37e0cc477af0688" + [[package]] name = "clipboard-win" version = "5.4.0" @@ -1308,6 +1321,15 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "endi" version = "1.1.0" @@ -1459,6 +1481,12 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de853764b47027c2e862a995c34978ffa63c1501f2e15f987ba11bd4f9bba193" +[[package]] +name = "extended" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365" + [[package]] name = "fast-srgb8" version = "1.0.0" @@ -1946,6 +1974,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "hound" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" + [[package]] name = "humantime" version = "2.1.0" @@ -2355,6 +2389,17 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lewton" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "777b48df9aaab155475a83a7df3070395ea1ac6902f5cd062b8f2b028075c030" +dependencies = [ + "byteorder", + "ogg", + "tinyvec", +] + [[package]] name = "lexical-parse-float" version = "0.8.5" @@ -3258,6 +3303,15 @@ dependencies = [ "cc", ] +[[package]] +name = "ogg" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6951b4e8bf21c8193da321bcce9c9dd2e13c858fe078bf9054a288b419ae5d6e" +dependencies = [ + "byteorder", +] + [[package]] name = "once_cell" version = "1.19.0" @@ -3856,6 +3910,20 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "rodio" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6006a627c1a38d37f3d3a85c6575418cfe34a5392d60a686d0071e1c8d427acb" +dependencies = [ + "claxon", + "cpal", + "hound", + "lewton", + "symphonia", + "thiserror", +] + [[package]] name = "roxmltree" version = "0.20.0" @@ -4678,6 +4746,151 @@ dependencies = [ "zeno", ] +[[package]] +name = "symphonia" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "815c942ae7ee74737bb00f965fa5b5a2ac2ce7b6c01c0cc169bbeaf7abd5f5a9" +dependencies = [ + "lazy_static", + "symphonia-bundle-flac", + "symphonia-bundle-mp3", + "symphonia-codec-aac", + "symphonia-codec-adpcm", + "symphonia-codec-pcm", + "symphonia-codec-vorbis", + "symphonia-core", + "symphonia-format-isomp4", + "symphonia-format-riff", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-bundle-flac" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72e34f34298a7308d4397a6c7fbf5b84c5d491231ce3dd379707ba673ab3bd97" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-bundle-mp3" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c01c2aae70f0f1fb096b6f0ff112a930b1fb3626178fba3ae68b09dce71706d4" +dependencies = [ + "lazy_static", + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-codec-aac" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdbf25b545ad0d3ee3e891ea643ad115aff4ca92f6aec472086b957a58522f70" +dependencies = [ + "lazy_static", + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-adpcm" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c94e1feac3327cd616e973d5be69ad36b3945f16b06f19c6773fc3ac0b426a0f" +dependencies = [ + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-pcm" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f395a67057c2ebc5e84d7bb1be71cce1a7ba99f64e0f0f0e303a03f79116f89b" +dependencies = [ + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-vorbis" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a98765fb46a0a6732b007f7e2870c2129b6f78d87db7987e6533c8f164a9f30" +dependencies = [ + "log", + "symphonia-core", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-core" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "798306779e3dc7d5231bd5691f5a813496dc79d3f56bf82e25789f2094e022c3" +dependencies = [ + "arrayvec", + "bitflags 1.3.2", + "bytemuck", + "lazy_static", + "log", +] + +[[package]] +name = "symphonia-format-isomp4" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abfdf178d697e50ce1e5d9b982ba1b94c47218e03ec35022d9f0e071a16dc844" +dependencies = [ + "encoding_rs", + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-format-riff" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f7be232f962f937f4b7115cbe62c330929345434c834359425e043bfd15f50" +dependencies = [ + "extended", + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-metadata" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc622b9841a10089c5b18e99eb904f4341615d5aa55bbf4eedde1be721a4023c" +dependencies = [ + "encoding_rs", + "lazy_static", + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-utils-xiph" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "484472580fa49991afda5f6550ece662237b00c6f562c7d9638d1b086ed010fe" +dependencies = [ + "symphonia-core", + "symphonia-metadata", +] + [[package]] name = "syn" version = "1.0.109" diff --git a/Cargo.toml b/Cargo.toml index 4402663..bd3c419 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ log = "0.4" midi-player = "0.2.1" quick-xml = "0.31.0" rfd = "0.14.0" +rodio = { version = "0.19", features = ["symphonia-all"] } 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/src/app/app.rs b/src/app/app.rs index 31d3c66..1701b87 100644 --- a/src/app/app.rs +++ b/src/app/app.rs @@ -7,10 +7,9 @@ use iced::widget::{ row, scrollable, text, text::Style as TextStyle, text_input, }; use iced::{time, Element, Font, Subscription, Task}; -use iced_aw::widget::number_input; use rfd::FileDialog; -use super::midi_player::{self, GlobalState as GlobalMidiPlayerState, State as MidiPlayerState}; +use super::player::{self, GlobalState as GlobalPlayerState, State as PlayerState}; use crate::interpreter; const TERM_WIDTH: u16 = 80; @@ -26,7 +25,7 @@ pub struct State { answer: String, output: Vec, question: Option, - midi_player_state: GlobalMidiPlayerState, + player_state: GlobalPlayerState, scratch_dir: String, input_id: String, path_lib: Vec, @@ -37,7 +36,7 @@ pub struct State { impl Default for State { fn default() -> Self { - let midi_player_state = GlobalMidiPlayerState::new(SOUND_FONT); + let midi_player_state = GlobalPlayerState::new(SOUND_FONT); let output = vec![Output::Normal( r#" _ _ ___ __ @@ -58,7 +57,7 @@ impl Default for State { .expect("the channel is unbound"); Self { - midi_player_state, + player_state: midi_player_state, answer: String::new(), output, question: None, @@ -73,11 +72,11 @@ impl Default for State { } #[derive(Debug)] -enum Output { +pub(crate) enum Output { Normal(String), Command(String), Error(String), - MidiPlayer(MidiPlayerState), + Player(PlayerState), } /// The iced update function. @@ -86,6 +85,16 @@ pub fn update(state: &mut State, message: Message) -> Task { Message::InputChanged(val) => { state.answer = val; } + Message::Answer(question, value) => { + state.question = None; + state + .output + .push(Output::Normal(format!("{question}{value}"))); + interpreter::INTERPRETER_WORKER + .response_sender + .send_blocking(value) + .expect("cannot send message to response receiver"); + } Message::SetScratchDir => { if let Some(value) = pick_directory("Choose scratch folder") { interpreter::INTERPRETER_WORKER @@ -94,77 +103,6 @@ pub fn update(state: &mut State, message: Message) -> Task { .expect("the channel is unbound"); } } - Message::PlayMidi(id) => { - if let Some(playing_id) = state.midi_player_state.playing_id { - if let Some(Output::MidiPlayer(player_state)) = state.output.get_mut(playing_id) { - player_state.is_playing = false; - state.midi_player_state.playing_id = None; - } - } - - if let Some(Output::MidiPlayer(player)) = state.output.get_mut(id) { - if player.path.exists() { - player.is_playing = true; - if let Err(e) = state - .midi_player_state - .controller - .set_file(Some(&player.path)) - { - state.output.push(Output::Error(e.to_string())); - return Task::none(); - } - state - .midi_player_state - .controller - .set_position(player.position); - state.midi_player_state.update_tempo(); - state.midi_player_state.controller.play(); - state.midi_player_state.playing_id = Some(id); - } else { - player.is_playing = false; - } - } - } - Message::StopMidi(id) => { - if let Some(Output::MidiPlayer(player_state)) = state.output.get_mut(id) { - player_state.is_playing = false; - state.midi_player_state.controller.stop(); - if let Some(playing_id) = state.midi_player_state.playing_id { - if playing_id == id { - state.midi_player_state.playing_id = None; - } - } - } - } - Message::ChangePlayingPosition(id, position) => { - if let Some(Output::MidiPlayer(player_state)) = state.output.get_mut(id) { - player_state.position = position; - - if let Some(playing_id) = state.midi_player_state.playing_id { - if playing_id == id { - state.midi_player_state.controller.set_position(position); - } - } - } - } - Message::Tick(_) => { - if let Some(playing_id) = state.midi_player_state.playing_id { - if let Some(Output::MidiPlayer(player_state)) = state.output.get_mut(playing_id) { - let position = state.midi_player_state.controller.position(); - - player_state.position = position; - - if position >= 1.0 { - player_state.is_playing = false; - player_state.position = 0.0; - state.midi_player_state.playing_id = None; - } - } - } - } - Message::SetTempo(tempo) => { - state.midi_player_state.set_tempo(tempo); - } Message::PiSelected(value) => { interpreter::INTERPRETER_WORKER .interp_sender @@ -177,7 +115,7 @@ pub fn update(state: &mut State, message: Message) -> Task { .send_blocking(interpreter::Message::SendCmd(format! {"tio {value}"})) .expect("cannot send message to the interpreter"); } - Message::InterpreterMessage(msg) => match msg { + Message::Interpreter(msg) => match msg { interpreter::Message::SendCmd(ref cmd) => { state.answer = "".to_owned(); state.output.push(Output::Command(cmd.to_owned())); @@ -205,10 +143,10 @@ pub fn update(state: &mut State, message: Message) -> Task { return text_input::focus(state.input_id.clone()); } interpreter::Message::LoadMidi(path) => { - state.output.push(Output::MidiPlayer(MidiPlayerState { + state.output.push(Output::Player(PlayerState { is_playing: false, path: path.into(), - id: state.output.len(), + id: player::PlayerId::Midi(state.output.len()), position: 0.0, })); } @@ -229,15 +167,9 @@ pub fn update(state: &mut State, message: Message) -> Task { } _ => (), }, - Message::Answer(question, value) => { - state.question = None; - state - .output - .push(Output::Normal(format!("{question}{value}"))); - interpreter::INTERPRETER_WORKER - .response_sender - .send_blocking(value) - .expect("cannot send message to response receiver"); + Message::Player(message) => { + return player::update(&mut state.output, &mut state.player_state, message) + .map(Message::Player) } } @@ -309,7 +241,7 @@ fn view_output(output: &Output) -> Element { Output::Error(msg) => container(text(msg).style(|theme: &iced::Theme| TextStyle { color: Some(theme.palette().danger), })), - Output::MidiPlayer(state) => container(midi_player::view(state)), + Output::Player(state) => container(player::view(state).map(Message::Player)), } .padding([10, 0]) .into() @@ -374,11 +306,7 @@ fn view_bottom_panel(state: &State) -> Element { row![ view_pici_chooser(state), horizontal_space(), - text("󰟚").font(iced_fonts::NERD_FONT).size(16), - text("=").size(16), - number_input(state.midi_player_state.tempo(), 20..=600, Message::SetTempo,) - .step(1) - .width(60.0), + player::view_tempo(&state.player_state).map(Message::Player) ] .spacing(10.0) .padding([18, 0]) @@ -429,22 +357,23 @@ fn pick_directory(title: &str) -> Option { #[derive(Debug, Clone)] pub enum Message { InputChanged(String), - InterpreterMessage(interpreter::Message), Answer(String, String), - PlayMidi(usize), - StopMidi(usize), - // id, position - ChangePlayingPosition(usize, f64), - Tick(time::Instant), - SetTempo(u16), SetScratchDir, PiSelected(String), TiSelected(String), + Interpreter(interpreter::Message), + Player(player::Message), } impl From for Message { fn from(value: interpreter::Message) -> Self { - Self::InterpreterMessage(value) + Self::Interpreter(value) + } +} + +impl From for Message { + fn from(value: player::Message) -> Self { + Self::Player(value) } } @@ -470,10 +399,12 @@ pub fn subscription(state: &State) -> Subscription { } }) }) - .map(Message::InterpreterMessage); + .map(Message::Interpreter); - let position_listener = if state.midi_player_state.controller.is_playing() { - time::every(time::Duration::from_millis(20)).map(Message::Tick) + let position_listener = if state.player_state.playing() { + time::every(time::Duration::from_millis(20)) + .map(player::Message::Tick) + .map(Message::Player) } else { Subscription::none() }; diff --git a/src/app/midi_player.rs b/src/app/midi_player.rs deleted file mode 100644 index a588291..0000000 --- a/src/app/midi_player.rs +++ /dev/null @@ -1,137 +0,0 @@ -use std::path::PathBuf; - -use super::app::Message; -use cpal::{ - traits::{DeviceTrait, HostTrait, StreamTrait}, - Stream as AudioStream, StreamConfig, -}; -use iced::{ - widget::{button, row, slider, text}, - Element, -}; -use midi_player::{Player, PlayerController, Settings as PlayerSettings}; - -pub(crate) struct GlobalState { - pub(crate) controller: PlayerController, - _audio_stream: AudioStream, - pub(crate) playing_id: Option, - pub(crate) tempo: u16, -} - -impl GlobalState { - pub(crate) fn new(sf: &str) -> Self { - 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); - - Self { - controller, - _audio_stream: audio_stream, - playing_id: None, - tempo: 120, - } - } - - fn start_audio_loop(mut player: Player) -> AudioStream { - let host = cpal::default_host(); - let device = host - .default_output_device() - .expect("No output device available"); - let channels = 2_usize; - let config = StreamConfig { - channels: channels as u16, - sample_rate: cpal::SampleRate(player.settings().sample_rate), - buffer_size: cpal::BufferSize::Fixed(player.settings().audio_buffer_size), - }; - - let err_fn = |err| eprintln!("An error occurred on the output audio stream: {}", err); - - let mut left = vec![0f32; player.settings().audio_buffer_size as usize]; - let mut right = vec![0f32; player.settings().audio_buffer_size as usize]; - - let stream = device - .build_output_stream( - &config, - move |data: &mut [f32], _: &cpal::OutputCallbackInfo| { - let sample_count = data.len() / channels; - - player.render(&mut left, &mut right); - - if !left.is_empty() { - for i in 0..sample_count { - data[channels * i] = left[i]; - data[channels * i + 1] = right[i]; - } - } - }, - err_fn, - None, - ) - .unwrap(); - - stream.play().expect("cannot run audio stream"); - - stream - } - - 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); - } - - pub(crate) fn update_tempo(&mut self) { - self.set_tempo(self.tempo); - } -} - -#[derive(Debug)] -pub(crate) struct State { - pub(crate) is_playing: bool, - pub(crate) path: PathBuf, - pub(crate) id: usize, - pub(crate) position: f64, -} - -pub(crate) fn view(state: &State) -> Element { - let disabled = !state.path.exists() && !state.is_playing; - let label = text(if state.is_playing { "" } else { "" }) - .font(iced_fonts::NERD_FONT) - .align_x(iced::Alignment::Center) - .size(24); - let message = if state.is_playing { - Message::StopMidi(state.id) - } else { - Message::PlayMidi(state.id) - }; - let button = button(label); - let player = row![ - if disabled { - button - } else { - button.on_press(message) - } - .width(50), - slider(0.0..=1.0, state.position, |v| { - Message::ChangePlayingPosition(state.id, v) - }) - .step(0.001) - ] - .align_y(iced::Alignment::Center) - .spacing(10.0); - - if disabled { - text(format!( - "File {} does not exist.", - state.path.to_string_lossy() - )) - .style(text::danger) - .into() - } else { - player.into() - } -} diff --git a/src/app/mod.rs b/src/app/mod.rs index a5d9bb2..f6cd13f 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -3,4 +3,4 @@ pub use app::*; mod app; -mod midi_player; +mod player; diff --git a/src/app/player.rs b/src/app/player.rs new file mode 100644 index 0000000..ade4618 --- /dev/null +++ b/src/app/player.rs @@ -0,0 +1,267 @@ +use std::path::PathBuf; + +use cpal::{ + traits::{DeviceTrait, HostTrait, StreamTrait}, + Stream as AudioStream, StreamConfig, +}; +use iced::{ + time, + widget::{button, row, slider, text}, + Element, Task, +}; +use iced_aw::number_input; +use midi_player::{Player, PlayerController, Settings as PlayerSettings}; + +use super::app; + +pub(crate) struct GlobalState { + controller: PlayerController, + _audio_stream: AudioStream, + playing_id: Option, + tempo: u16, +} + +impl GlobalState { + pub(crate) fn new(sf: &str) -> Self { + 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); + + Self { + controller, + _audio_stream: audio_stream, + playing_id: None, + tempo: 120, + } + } + + pub(crate) fn playing(&self) -> bool { + self.controller.is_playing() + } + + fn start_audio_loop(mut player: Player) -> AudioStream { + let host = cpal::default_host(); + let device = host + .default_output_device() + .expect("No output device available"); + let channels = 2_usize; + let config = StreamConfig { + channels: channels as u16, + sample_rate: cpal::SampleRate(player.settings().sample_rate), + buffer_size: cpal::BufferSize::Fixed(player.settings().audio_buffer_size), + }; + + let err_fn = |err| eprintln!("An error occurred on the output audio stream: {}", err); + + let mut left = vec![0f32; player.settings().audio_buffer_size as usize]; + let mut right = vec![0f32; player.settings().audio_buffer_size as usize]; + + let stream = device + .build_output_stream( + &config, + move |data: &mut [f32], _: &cpal::OutputCallbackInfo| { + let sample_count = data.len() / channels; + + player.render(&mut left, &mut right); + + if !left.is_empty() { + for i in 0..sample_count { + data[channels * i] = left[i]; + data[channels * i + 1] = right[i]; + } + } + }, + err_fn, + None, + ) + .unwrap(); + + stream.play().expect("cannot run audio stream"); + + stream + } + + 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); + } + + pub(crate) fn update_tempo(&mut self) { + self.set_tempo(self.tempo); + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PlayerId { + Midi(usize), + Audio(usize), +} + +impl PlayerId { + pub(crate) fn inner(&self) -> usize { + match self { + PlayerId::Midi(v) => *v, + PlayerId::Audio(v) => *v, + } + } +} + +impl From for usize { + fn from(value: PlayerId) -> Self { + match value { + PlayerId::Midi(v) => v, + PlayerId::Audio(v) => v, + } + } +} + +#[derive(Debug)] +pub(crate) struct State { + pub(crate) is_playing: bool, + pub(crate) path: PathBuf, + pub(crate) id: PlayerId, + pub(crate) position: f64, +} + +#[derive(Debug, Clone)] +pub enum Message { + Play(PlayerId), + Pause(PlayerId), + ChangePosition(PlayerId, f64), + SetTempo(u16), + 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 { + let disabled = !state.path.exists() && !state.is_playing; + let label = text(if state.is_playing { "" } else { "" }) + .font(iced_fonts::NERD_FONT) + .align_x(iced::Alignment::Center) + .size(24); + let message = if state.is_playing { + Message::Pause(state.id) + } else { + Message::Play(state.id) + }; + let button = button(label); + let player = row![ + if disabled { + button + } else { + button.on_press(message) + } + .width(50), + slider(0.0..=1.0, state.position, |v| { + Message::ChangePosition(state.id, v) + }) + .step(0.001) + ] + .align_y(iced::Alignment::Center) + .spacing(10.0); + + if disabled { + text(format!( + "File {} does not exist.", + state.path.to_string_lossy() + )) + .style(text::danger) + .into() + } else { + player.into() + } +} + +pub(crate) fn view_tempo(global_state: &GlobalState) -> Element { + row![ + text("󰟚").font(iced_fonts::NERD_FONT).size(16), + text("=").size(16), + number_input(global_state.tempo(), 20..=600, Message::SetTempo) + .step(1) + .width(60.0), + ] + .spacing(10.0) + .align_y(iced::Alignment::Center) + .into() +}