diff --git a/font.sf2 b/default.sf2 similarity index 100% rename from font.sf2 rename to default.sf2 diff --git a/src/output_manager/mod.rs b/src/output_manager/mod.rs index d932b0a7..4bc3c7c5 100644 --- a/src/output_manager/mod.rs +++ b/src/output_manager/mod.rs @@ -6,12 +6,12 @@ use synth_backend::SynthBackend; use std::{ fmt::{self, Display, Formatter}, - path::Path, + path::{Path, PathBuf}, }; #[derive(Debug, Clone, PartialEq)] pub enum OutputDescriptor { - Synth, + Synth(Option), MidiOut(MidiPortInfo), DummyOutput, } @@ -19,7 +19,7 @@ pub enum OutputDescriptor { impl Display for OutputDescriptor { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { - OutputDescriptor::Synth => write!(f, "Buildin Synth"), + OutputDescriptor::Synth(_) => write!(f, "Buildin Synth"), OutputDescriptor::MidiOut(info) => write!(f, "{}", info), OutputDescriptor::DummyOutput => write!(f, "No Output"), } @@ -45,11 +45,12 @@ pub struct OutputManager { impl OutputManager { pub fn new() -> Self { - let synth_backend = if Path::new("./font.sf2").exists() { - Some(SynthBackend::new()) - } else { - log::info!("./font.sf2 not found"); - None + let synth_backend = match SynthBackend::new() { + Ok(synth_backend) => Some(synth_backend), + Err(err) => { + log::error!("{:?}", err); + None + } }; let midi_backend = match MidiBackend::new() { @@ -72,6 +73,8 @@ impl OutputManager { pub fn get_outputs(&self) -> Vec { let mut outs = Vec::new(); + outs.push(OutputDescriptor::DummyOutput); + if let Some(synth) = &self.synth_backend { outs.append(&mut synth.get_outputs()); } @@ -79,17 +82,25 @@ impl OutputManager { outs.append(&mut midi.get_outputs()); } - outs.push(OutputDescriptor::DummyOutput); - outs } pub fn connect(&mut self, desc: OutputDescriptor) { if desc != self.output_connection.0 { match desc { - OutputDescriptor::Synth => { + OutputDescriptor::Synth(ref font) => { if let Some(ref mut synth) = self.synth_backend { - self.output_connection = (desc, Box::new(synth.new_output_connection())); + if let Some(font) = font.clone() { + self.output_connection = + (desc, Box::new(synth.new_output_connection(font))); + } else { + if Path::new("./default.sf2").exists() { + self.output_connection = ( + desc, + Box::new(synth.new_output_connection("./default.sf2".into())), + ); + } + } } } OutputDescriptor::MidiOut(ref info) => { diff --git a/src/output_manager/synth_backend.rs b/src/output_manager/synth_backend.rs index 745fa018..6d69bfc0 100644 --- a/src/output_manager/synth_backend.rs +++ b/src/output_manager/synth_backend.rs @@ -1,5 +1,7 @@ extern crate fluidlite_lib; +use std::{error::Error, path::PathBuf, sync::mpsc::Receiver}; + use crate::output_manager::{OutputConnection, OutputDescriptor}; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; @@ -21,29 +23,29 @@ pub struct SynthBackend { } impl SynthBackend { - pub fn new() -> Self { + pub fn new() -> Result> { let host = cpal::default_host(); let device = host .default_output_device() - .expect("failed to find a default output device"); + .ok_or("failed to find a default output device")?; - let config = device.default_output_config().unwrap(); + let config = device.default_output_config()?; let sample_format = config.sample_format(); let mut stream_config: cpal::StreamConfig = config.into(); stream_config.sample_rate.0 = 44100; - Self { + Ok(Self { _host: host, device, stream_config, sample_format, - } + }) } - fn run(&self, rx: std::sync::mpsc::Receiver) -> cpal::Stream { + fn run(&self, rx: Receiver, path: PathBuf) -> cpal::Stream { let mut buff: [f32; SAMPLES_SIZE] = [0.0f32; SAMPLES_SIZE]; let synth = { @@ -55,7 +57,7 @@ impl SynthBackend { rate.set((sample_rate / 2) as f64); let synth = fluidlite::Synth::new(settings).unwrap(); - synth.sfload("font.sf2", true).unwrap(); + synth.sfload(path, true).unwrap(); synth.set_sample_rate(sample_rate as f32); synth.set_gain(1.0); @@ -112,19 +114,19 @@ impl SynthBackend { stream } - pub fn new_output_connection(&mut self) -> SynthOutputConnection { + pub fn new_output_connection(&mut self, path: PathBuf) -> SynthOutputConnection { let (tx, rx) = std::sync::mpsc::channel::(); let _stream = match self.sample_format { - cpal::SampleFormat::F32 => self.run::(rx), - cpal::SampleFormat::I16 => self.run::(rx), - cpal::SampleFormat::U16 => self.run::(rx), + cpal::SampleFormat::F32 => self.run::(rx, path), + cpal::SampleFormat::I16 => self.run::(rx, path), + cpal::SampleFormat::U16 => self.run::(rx, path), }; SynthOutputConnection { _stream, tx } } pub fn get_outputs(&self) -> Vec { - vec![OutputDescriptor::Synth] + vec![OutputDescriptor::Synth(None)] } } diff --git a/src/scene/menu_scene/iced_menu.rs b/src/scene/menu_scene/iced_menu.rs index b4f427d5..3138412b 100644 --- a/src/scene/menu_scene/iced_menu.rs +++ b/src/scene/menu_scene/iced_menu.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + use iced_native::{ image, Align, Color, Column, Command, Container, Element, HorizontalAlignment, Image, Length, Program, Row, Text, VerticalAlignment, @@ -8,10 +10,12 @@ use crate::output_manager::OutputDescriptor; pub struct IcedMenu { midi_file: Option, + pub font_path: Option, - carousel: Carousel, + pub carousel: Carousel, file_select_button: neo_btn::State, + synth_button: neo_btn::State, prev_button: neo_btn::State, next_button: neo_btn::State, play_button: neo_btn::State, @@ -20,6 +24,7 @@ pub struct IcedMenu { #[derive(Debug, Clone)] pub enum Message { FileSelectPressed, + FontSelectPressed, PrevPressed, NextPressed, @@ -27,7 +32,7 @@ pub enum Message { OutputsUpdated(Vec), - MainMenuDone(lib_midi::Midi, usize, OutputDescriptor), + MainMenuDone(lib_midi::Midi, OutputDescriptor), } impl IcedMenu { @@ -45,10 +50,12 @@ impl IcedMenu { Self { midi_file, + font_path: None, carousel, file_select_button: neo_btn::State::new(), + synth_button: neo_btn::State::new(), prev_button: neo_btn::State::new(), next_button: neo_btn::State::new(), play_button: neo_btn::State::new(), @@ -90,6 +97,24 @@ impl Program for IcedMenu { } } + Message::FontSelectPressed => { + use nfd2::Response; + + match nfd2::DialogBuilder::single() + .filter("sf2") + .open() + .expect("Font Dialog Error") + { + Response::Okay(path) => { + log::info!("Font path = {:?}", path); + self.font_path = Some(path); + } + _ => { + log::error!("User canceled dialog"); + } + } + } + Message::NextPressed => { self.carousel.next(); } @@ -99,18 +124,23 @@ impl Program for IcedMenu { Message::PlayPressed => { if self.midi_file.is_some() { - async fn play( - midi: lib_midi::Midi, - id: usize, - out: OutputDescriptor, - ) -> Message { - Message::MainMenuDone(midi, id, out) + async fn play(m: Message) -> Message { + m } if self.midi_file.is_some() { if let Some(midi) = std::mem::replace(&mut self.midi_file, None) { if let Some(port) = self.carousel.get_item() { - return Command::from(play(midi, self.carousel.id, port.clone())); + let port = if let OutputDescriptor::Synth(_) = port { + OutputDescriptor::Synth(std::mem::replace( + &mut self.font_path, + None, + )) + } else { + port.clone() + }; + let event = Message::MainMenuDone(midi, port); + return Command::from(play(event)); } } } @@ -121,7 +151,7 @@ impl Program for IcedMenu { self.carousel.update(outs); } - Message::MainMenuDone(_, _, _) => {} + Message::MainMenuDone(_, _) => {} } Command::none() @@ -142,52 +172,65 @@ impl Program for IcedMenu { .on_press(Message::FileSelectPressed), ); - let item = self - .carousel - .get_item() + let item = self.carousel.get_item(); + + let label = item .map(|o| o.to_string()) .unwrap_or("Disconected".to_string()); - let text = Text::new(item) + let output = Text::new(label) .color(Color::WHITE) - // .height(Length::Units(100)) .size(30) .horizontal_alignment(HorizontalAlignment::Center) .vertical_alignment(VerticalAlignment::Center); - let select_row = Row::new() - .height(Length::Units(50)) - .push( - NeoBtn::new( - &mut self.prev_button, - Text::new("<") - .size(40) - .horizontal_alignment(HorizontalAlignment::Center) - .vertical_alignment(VerticalAlignment::Center), - ) - .width(Length::Fill) - .disabled(!self.carousel.check_prev()) - .on_press(Message::PrevPressed), + let mut select_row = Row::new().height(Length::Units(50)).push( + NeoBtn::new( + &mut self.prev_button, + Text::new("<") + .size(40) + .horizontal_alignment(HorizontalAlignment::Center) + .vertical_alignment(VerticalAlignment::Center), ) - .push( + .width(Length::Fill) + .disabled(!self.carousel.check_prev()) + .on_press(Message::PrevPressed), + ); + + if let Some(OutputDescriptor::Synth(_)) = item { + select_row = select_row.push( NeoBtn::new( - &mut self.next_button, - Text::new(">") - .size(40) + &mut self.synth_button, + Text::new("Soundfont") + .size(20) .horizontal_alignment(HorizontalAlignment::Center) .vertical_alignment(VerticalAlignment::Center), ) - .width(Length::Fill) - .disabled(!self.carousel.check_next()) - .on_press(Message::NextPressed), + .width(Length::Units(100)) + .height(Length::Fill) + .on_press(Message::FontSelectPressed), ); + } + + select_row = select_row.push( + NeoBtn::new( + &mut self.next_button, + Text::new(">") + .size(40) + .horizontal_alignment(HorizontalAlignment::Center) + .vertical_alignment(VerticalAlignment::Center), + ) + .width(Length::Fill) + .disabled(!self.carousel.check_next()) + .on_press(Message::NextPressed), + ); let controls = Column::new() .align_items(Align::Center) .width(Length::Units(500)) .spacing(30) .push(file_select_button) - .push(text) + .push(output) .push(select_row); let controls = Container::new(controls).width(Length::Fill).center_x(); @@ -238,7 +281,6 @@ impl Program for IcedMenu { }; let footer = Container::new(content) - .padding(10) .width(Length::Fill) .height(Length::Units(70)) .align_x(Align::End) @@ -250,7 +292,7 @@ impl Program for IcedMenu { } } -struct Carousel { +pub struct Carousel { outputs: Vec, id: usize, } @@ -263,6 +305,10 @@ impl Carousel { } } + pub fn id(&self) -> usize { + self.id + } + fn update(&mut self, outs: Vec) { self.outputs = outs; } diff --git a/src/scene/menu_scene/mod.rs b/src/scene/menu_scene/mod.rs index 5dda76c1..020c6c8a 100644 --- a/src/scene/menu_scene/mod.rs +++ b/src/scene/menu_scene/mod.rs @@ -164,10 +164,13 @@ impl Scene for MenuScene { let event = crate::block_on(async { f.await }); match event { - iced_menu::Message::MainMenuDone(midi, id, out) => { + iced_menu::Message::MainMenuDone(midi, out) => { + let program = self.iced_state.program(); + self.main_state.midi_file = Some(midi); - self.main_state.output_manager.selected_output_id = Some(id); + self.main_state.output_manager.selected_output_id = + Some(program.carousel.id()); self.main_state.output_manager.connect(out); return SceneEvent::MainMenu(Event::Play);