diff --git a/Cargo.lock b/Cargo.lock index 3239e90e..31e85160 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -105,12 +105,11 @@ dependencies = [ [[package]] name = "audio" -version = "0.1.4" +version = "0.2.0" dependencies = [ "chrono", "common", "cpal", - "crossbeam-channel", "flacenc", "hashbrown 0.13.2", "hound", @@ -239,7 +238,7 @@ checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" [[package]] name = "cacophony" -version = "0.1.4" +version = "0.2.0" dependencies = [ "audio", "clap", @@ -397,7 +396,7 @@ dependencies = [ [[package]] name = "common" -version = "0.1.4" +version = "0.2.0" dependencies = [ "clap", "directories", @@ -1009,7 +1008,7 @@ dependencies = [ [[package]] name = "input" -version = "0.1.4" +version = "0.2.0" dependencies = [ "clap", "common", @@ -1035,7 +1034,7 @@ dependencies = [ [[package]] name = "io" -version = "0.1.4" +version = "0.2.0" dependencies = [ "audio", "common", @@ -1658,7 +1657,7 @@ checksum = "8d91edf4fbb970279443471345a4e8c491bf05bb283b3e6c88e4e606fd8c181b" [[package]] name = "oxisynth" version = "0.0.3" -source = "git+https://github.com/PolyMeilex/OxiSynth.git?branch=master#7b56c47bac2d96952b803100d4ec04f8f7dd839b" +source = "git+https://github.com/subalterngames/OxiSynth.git?branch=midi_event_copy_clone#547d62343119c0bdea24ce8b65e63ae7162f1e6d" dependencies = [ "bitflags 1.3.2", "byte-slice-cast", @@ -1883,7 +1882,7 @@ checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" [[package]] name = "render" -version = "0.1.4" +version = "0.2.0" dependencies = [ "audio", "colorgrad", @@ -2100,7 +2099,7 @@ checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" [[package]] name = "soundfont" version = "0.0.2" -source = "git+https://github.com/PolyMeilex/OxiSynth.git?branch=master#7b56c47bac2d96952b803100d4ec04f8f7dd839b" +source = "git+https://github.com/subalterngames/OxiSynth.git?branch=midi_event_copy_clone#547d62343119c0bdea24ce8b65e63ae7162f1e6d" dependencies = [ "riff", ] @@ -2214,7 +2213,7 @@ dependencies = [ [[package]] name = "text" -version = "0.1.4" +version = "0.2.0" dependencies = [ "common", "csv", diff --git a/Cargo.toml b/Cargo.toml index f9645f8d..d73d9630 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ members = ["audio", "common", "input", "io", "render", "text"] [workspace.package] -version = "0.1.4" +version = "0.2.0" authors = ["Esther Alter "] description = "A minimalist and ergonomic MIDI sequencer" documentation = "https://github.com/subalterngames/cacophony" @@ -13,7 +13,6 @@ serde_json = "1.0" rust-ini = "0.18" directories = "5.0.1" midir = "0.9.1" -crossbeam-channel = "0.5.8" csv = "1.2.1" cpal = "0.13.1" hound = "3.5.0" @@ -70,8 +69,8 @@ features = [] [workspace.dependencies.oxisynth] version = "0.0.3" features = [] -git = "https://github.com/PolyMeilex/OxiSynth.git" -branch = "master" +git = "https://github.com/subalterngames/OxiSynth.git" +branch = "midi_event_copy_clone" [workspace.dependencies.tts] version = "0.25.6" @@ -85,7 +84,7 @@ speech_dispatcher_0_9 = ["text/speech_dispatcher_0_9"] [package] name = "cacophony" -version = "0.1.4" +version = "0.2.0" authors = ["Esther Alter "] description = "A minimalist and ergonomic MIDI sequencer" documentation = "https://github.com/subalterngames/cacophony" @@ -121,7 +120,7 @@ path = "text" name = "Cacophony" identifier = "com.subalterngames.cacophony" icon = ["icon/32.png", "icon/64.png", "icon/128.png", "icon/256.png"] -version = "0.1.4" +version = "0.2.0" resources = ["data/*"] copyright = "Copyright (c) Subaltern Games LLC 2023. All rights reserved." short_description = "A minimalist and ergonomic MIDI sequencer." diff --git a/audio/Cargo.toml b/audio/Cargo.toml index e76cf67f..f238e3f3 100644 --- a/audio/Cargo.toml +++ b/audio/Cargo.toml @@ -8,7 +8,6 @@ edition.workspace = true [dependencies] cpal = { workspace = true } -crossbeam-channel = { workspace = true } hound = { workspace = true } id3 = { workspace = true } mp3lame-encoder = { workspace = true } diff --git a/audio/src/command.rs b/audio/src/command.rs index 2854a52a..dc4a2c74 100644 --- a/audio/src/command.rs +++ b/audio/src/command.rs @@ -1,31 +1,8 @@ -use crate::export_state::ExportState; use std::path::PathBuf; /// A command for the synthesizer. #[derive(Debug, Eq, PartialEq, Clone)] pub enum Command { - /// Set the synthesizer's framerate. - SetFramerate { framerate: u32 }, - /// Send this to announce that we're playing music, as opposed to arbitrary user input audio. - PlayMusic { time: u64 }, - /// Send this to stop playing music. - StopMusic, - /// Schedule a stop-all event. - StopMusicAt { time: u64 }, - /// Stop all sound. - SoundOff, - /// Note-on ASAP. - NoteOn { channel: u8, key: u8, velocity: u8 }, - /// Schedule a note-on event. - NoteOnAt { - channel: u8, - key: u8, - velocity: u8, - start: u64, - end: u64, - }, - /// Note-off ASAP. - NoteOff { channel: u8, key: u8 }, /// Load a SoundFont file. LoadSoundFont { channel: u8, path: PathBuf }, /// Set a program. @@ -39,8 +16,4 @@ pub enum Command { UnsetProgram { channel: u8 }, /// Set the overall gain. SetGain { gain: u8 }, - /// Export audio. - Export { path: PathBuf, state: ExportState }, - /// Ask for the export state. - SendExportState, } diff --git a/audio/src/conn.rs b/audio/src/conn.rs index bb3e1724..e4921c27 100644 --- a/audio/src/conn.rs +++ b/audio/src/conn.rs @@ -1,86 +1,548 @@ -use crate::{AudioMessage, Command, CommandsMessage, ExportState, Player, SynthState, TimeState}; -use crossbeam_channel::{Receiver, Sender}; +use crate::decayer::Decayer; +use crate::export::{ExportState, ExportType, Exportable, MultiFileSuffix}; +use crate::exporter::Exporter; +use crate::play_state::PlayState; +use crate::types::SharedPlayState; +use crate::SharedExportState; +use crate::{ + midi_event_queue::MidiEventQueue, types::SharedSample, Command, Player, Program, + SharedMidiEventQueue, SharedSynth, SynthState, +}; +use common::open_file::Extension; +use common::{MidiTrack, Music, PathsState, State, Time, MAX_VOLUME}; +use hashbrown::HashMap; +use oxisynth::{MidiEvent, SoundFont, SoundFontId, Synth}; +use parking_lot::Mutex; +use std::fs::File; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use std::thread::spawn; + +/// A convenient wrapper for a SoundFont. +struct SoundFontBanks { + id: SoundFontId, + /// The banks and their presets. + banks: HashMap>, +} + +impl SoundFontBanks { + pub fn new(font: SoundFont, synth: &mut SharedSynth) -> Self { + let mut banks: HashMap> = HashMap::new(); + (0u32..=128u32).for_each(|b| { + let presets: Vec = (0u8..128) + .filter(|p| font.preset(b, *p).is_some()) + .collect(); + if !presets.is_empty() { + banks.insert(b, presets); + } + }); + let mut synth = synth.lock(); + let id = synth.add_font(font, true); + Self { id, banks } + } +} /// The connects used by an external function. pub struct Conn { - /// The state (as far as we know). This is received from the synthesizer. - pub state: SynthState, /// The current export state, if any. - pub export_state: Option, + pub export_state: SharedExportState, /// The playback framerate. pub framerate: f32, /// The audio player. This is here so we don't drop it. _player: Option, - /// Send commands to the synthesizer. - send_commands: Sender, - /// Receive the program state. - recv: Receiver, - /// Receive the export state. - recv_export: Receiver>, - /// Receive the updated time. - recv_time: Receiver, - /// Receive an audio sample. - recv_sample: Receiver, /// The most recent sample. - pub sample: Option, + /// `render::MainMenu` uses this to for its power bars. + pub sample: SharedSample, + /// A shared Oxisynth synthesizer. + /// The `Conn` uses this to send MIDI events and export. + /// The `Player` uses this to write samples to the output buffer. + synth: SharedSynth, + /// A queue of scheduled MIDI events. + /// The `Conn` can add to this. + /// The `Player` can read this and remove events. + midi_event_queue: SharedMidiEventQueue, + /// A HashMap of loaded SoundFonts. Key = The path to a .sf2 file. + soundfonts: HashMap, + /// Metadata for all SoundFont programs. + pub state: SynthState, + /// Export settings. + pub exporter: Exporter, + /// A flag that `Player` uses to decide how to write samples to the output buffer. + pub play_state: SharedPlayState, } -impl Conn { - pub(crate) fn new( - player: Option, - send_commands: Sender, - recv: Receiver, - recv_export: Receiver>, - recv_time: Receiver, - recv_sample: Receiver, - ) -> Self { +impl Default for Conn { + fn default() -> Self { + // Set the synthesizer. + let mut synth = Synth::default(); + synth.set_gain(1.0); + let synth = Arc::new(Mutex::new(synth)); + + // Create other shared data. + let midi_event_queue = Arc::new(Mutex::new(MidiEventQueue::default())); + let sample = Arc::new(Mutex::new((0.0, 0.0))); + let play_state = Arc::new(Mutex::new(PlayState::NotPlaying)); + + // Create the player. + let player_synth = Arc::clone(&synth); + let player_midi_event_queue = Arc::clone(&midi_event_queue); + let player_sample = Arc::clone(&sample); + let player_play_state = Arc::clone(&play_state); + let player = Player::new( + player_midi_event_queue, + player_synth, + player_sample, + player_play_state, + ); + + // Get the framerate. let framerate = match &player { Some(player) => player.framerate as f32, None => 0.0, }; Self { - state: SynthState::default(), - export_state: None, + export_state: Arc::new(Mutex::new(ExportState::NotExporting)), _player: player, - send_commands, - recv, - recv_export, - recv_time, - recv_sample, framerate, - sample: None, + sample, + synth, + midi_event_queue, + soundfonts: HashMap::default(), + state: SynthState::default(), + exporter: Exporter::default(), + play_state, + } + } +} + +impl Conn { + /// Do all note-on events created by user input on this app frame. + pub fn note_ons(&mut self, state: &State, note_ons: &[[u8; 3]]) { + if let Some(track) = state.music.get_selected_track() { + if !note_ons.is_empty() { + let mut synth = self.synth.lock(); + let gain = track.gain as f32 / MAX_VOLUME as f32; + for note_on in note_ons.iter() { + let _ = synth.send_event(MidiEvent::NoteOn { + channel: track.channel, + key: note_on[1], + vel: (note_on[2] as f32 * gain) as u8, + }); + } + // Play audio. + let mut play_state = self.play_state.lock(); + *play_state = PlayState::Decaying; + } } } - /// Try to send commands and receive a `SynthState`, which updates `self.state. - /// - /// - `commands` The commands that we'll send. - pub fn send(&mut self, commands: CommandsMessage) { - match self.send_commands.send(commands) { - Ok(_) => (), - Err(error) => panic!("Error sending commands: {}", error), + /// Do all note-off events created by user input on this app frame. + pub fn note_offs(&mut self, state: &State, note_offs: &[u8]) { + if let Some(track) = state.music.get_selected_track() { + if !note_offs.is_empty() { + let mut synth = self.synth.lock(); + for note_off in note_offs.iter() { + let _ = synth.send_event(MidiEvent::NoteOff { + channel: track.channel, + key: *note_off, + }); + } + } } - // Update the state. - if let Ok(state) = self.recv.recv() { - self.state = state; + } + + /// Execute a slice of commands sent from `io`. + pub fn do_commands(&mut self, commands: &[Command]) { + for command in commands.iter() { + match command { + Command::LoadSoundFont { channel, path } => { + match &self.soundfonts.get(path) { + // We already loaded this font. + Some(_) => { + self.set_program_default(*channel, path); + } + // Load the font. + None => match SoundFont::load(&mut File::open(path).unwrap()) { + Ok(font) => { + let banks = SoundFontBanks::new(font, &mut self.synth); + self.soundfonts.insert(path.clone(), banks); + // Set the default program. + self.set_program_default(*channel, path); + // Restore the other programs. + let programs = self.state.programs.clone(); + for program in programs.iter().filter(|p| p.0 != channel) { + if self.soundfonts.contains_key(&program.1.path) { + let mut synth = self.synth.lock(); + synth + .program_select( + *program.0, + self.soundfonts[&program.1.path].id, + program.1.bank, + program.1.preset, + ) + .unwrap(); + } + } + } + Err(error) => { + panic!("Failed to load SoundFont: {:?}", error) + } + }, + } + } + Command::SetProgram { + channel, + path, + bank_index, + preset_index, + } => { + let soundfont = &self.soundfonts[path]; + let banks = soundfont.banks.keys().copied().collect::>(); + let bank = banks[*bank_index]; + let preset = soundfont.banks[&bank][*preset_index]; + let channel = *channel; + self.set_program(channel, path, bank, preset, soundfont.id); + } + Command::UnsetProgram { channel } => { + self.state.programs.remove(channel); + } + Command::SetGain { gain } => { + let mut synth = self.synth.lock(); + synth.set_gain(*gain as f32 / MAX_VOLUME as f32); + self.state.gain = *gain; + } + } } } - /// Call this once per frame. - pub fn update(&mut self) { - if let Ok(time) = self.recv_time.try_recv() { - self.state.time = time; + /// Start to play music if music isn't playing. Stop music if music is playing. + pub fn set_music(&mut self, state: &State) { + let play_state = *self.play_state.lock(); + match play_state { + PlayState::NotPlaying => self.start_music(state), + _ => self.stop_music(&state.music), } - self.sample = match self.recv_sample.try_recv() { - Ok(sample) => Some(sample), - Err(_) => None, - }; - // Get the export state. - if self.export_state.is_some() { - self.send(vec![Command::SendExportState]); - if let Ok(export_state) = self.recv_export.recv() { - self.export_state = export_state; + } + + pub fn exporting(&self) -> bool { + *self.export_state.lock() != ExportState::NotExporting + } + + /// Schedule MIDI events and start to play music. + fn start_music(&mut self, state: &State) { + // Get the start time. + let start = state + .time + .ppq_to_samples(state.time.playback, self.framerate); + + // Set the playback framerate. + let mut synth = self.synth.lock(); + synth.set_sample_rate(self.framerate); + drop(synth); + + // Enqueue note events. + let mut midi_event_queue = self.midi_event_queue.lock(); + for track in state.music.get_playable_tracks().iter() { + for note in track.get_playback_notes(state.time.playback) { + // Note-on event. + midi_event_queue.enqueue( + state.time.ppq_to_samples(note.start, self.framerate), + MidiEvent::NoteOn { + channel: track.channel, + key: note.note, + vel: note.velocity, + }, + ); + // Note-off event. + midi_event_queue.enqueue( + state.time.ppq_to_samples(note.end, self.framerate), + MidiEvent::NoteOff { + channel: track.channel, + key: note.note, + }, + ); + } + } + // Sort the events by start time. + midi_event_queue.sort(); + drop(midi_event_queue); + + // Play music. + let mut play_state = self.play_state.lock(); + *play_state = PlayState::Playing(start); + } + + /// Stop ongoing music. + fn stop_music(&mut self, music: &Music) { + let mut synth = self.synth.lock(); + for track in music.midi_tracks.iter() { + if synth + .send_event(MidiEvent::AllNotesOff { + channel: track.channel, + }) + .is_ok() + && synth + .send_event(MidiEvent::AllSoundOff { + channel: track.channel, + }) + .is_ok() + {} + } + drop(synth); + // Let the audio decay. + let mut play_state = self.play_state.lock(); + *play_state = PlayState::Decaying; + } + + /// Set the synthesizer program to a default program. + fn set_program_default(&mut self, channel: u8, path: &Path) { + let soundfont = &self.soundfonts[path]; + // Get the bank info. + let mut banks: Vec = soundfont.banks.keys().copied().collect(); + banks.sort(); + let bank = banks[0]; + let preset = soundfont.banks[&bank][0]; + // Select the default program. + let id = self.soundfonts[path].id; + self.set_program(channel, path, bank, preset, id); + } + + /// Set the synthesizer program to a program. + fn set_program(&mut self, channel: u8, path: &Path, bank: u32, preset: u8, id: SoundFontId) { + let mut synth = self.synth.lock(); + if synth.program_select(channel, id, bank, preset).is_ok() { + let soundfont = &self.soundfonts[path]; + // Get the bank info. + let bank_index = soundfont.banks.keys().position(|&b| b == bank).unwrap(); + // Get the preset info. + let preset_index = soundfont.banks[&bank] + .iter() + .position(|&p| p == preset) + .unwrap(); + let preset_name = synth.channel_preset(channel).unwrap().name().to_string(); + let num_banks = soundfont.banks.len(); + let num_presets = soundfont.banks[&bank].len(); + let program = Program { + path: path.to_path_buf(), + num_banks, + bank_index, + bank, + num_presets, + preset_index, + preset_name, + preset, + }; + // Remember the program. + self.state.programs.insert(channel, program); + } + } + + pub fn start_export(&mut self, state: &State, paths_state: &PathsState) { + let mut exportables = vec![]; + let tracks = state.music.get_playable_tracks(); + self.set_export_framerate(); + + // Export each track as a separate file. + if self.exporter.multi_file { + for track in tracks { + let mut events = MidiEventQueue::default(); + let mut t1 = 0; + let gain = track.get_gain_f(); + self.enqueue_track_events(track, &state.time, &mut events, &mut t1, gain); + events.sort(); + let suffix = Some(self.get_export_file_suffix(track)); + // Add an exportable. + exportables.push(Exportable { + events, + total_samples: t1, + suffix, + }); + } + } + // Export all tracks combined. + else { + let mut t1 = 0; + let mut events = MidiEventQueue::default(); + for track in tracks { + let gain = track.get_gain_f(); + self.enqueue_track_events(track, &state.time, &mut events, &mut t1, gain); + } + events.sort(); + // Add an exportable. + exportables.push(Exportable { + events, + total_samples: t1, + suffix: None, + }); + } + + let export_state = Arc::clone(&self.export_state); + let synth = Arc::clone(&self.synth); + let exporter = self.exporter.clone(); + let path = paths_state.exports.get_path(); + let player_framerate = self.framerate; + spawn(move || { + Self::export( + exportables, + export_state, + synth, + exporter, + path, + player_framerate, + ) + }); + } + + fn enqueue_track_events( + &self, + track: &MidiTrack, + time: &Time, + events: &mut MidiEventQueue, + t1: &mut u64, + gain: f32, + ) { + let framerate = self.exporter.framerate.get_f(); + for note in track.notes.iter() { + // Note-on. + events.enqueue( + time.ppq_to_samples(note.start, framerate), + MidiEvent::NoteOn { + channel: track.channel, + key: note.note, + vel: (note.velocity as f32 * gain) as u8, + }, + ); + let end = time.ppq_to_samples(note.end, framerate); + // This is the last known event. + if *t1 < end { + *t1 = end; + } + events.enqueue( + end, + MidiEvent::NoteOff { + channel: track.channel, + key: note.note, + }, + ); + } + } + + fn export( + mut exportables: Vec, + export_state: SharedExportState, + synth: SharedSynth, + exporter: Exporter, + path: PathBuf, + player_framerate: f32, + ) { + let mut decayer = Decayer::default(); + let extension: Extension = exporter.export_type.get().into(); + for exportable in exportables.iter_mut() { + let total_samples = exportable.total_samples; + // Get the audio buffers. + let mut left = vec![0.0f32; total_samples as usize]; + let mut right = vec![0.0f32; total_samples as usize]; + // Set the initial wav export state. + Self::set_export_state_wav(exportable, &export_state, 0); + let mut synth = synth.lock(); + for t in 0..total_samples { + // Get and send each event at this time. + for event in exportable.events.dequeue(t).iter() { + let _ = synth.send_event(*event); + } + // Set the export state. + Self::set_export_state_wav(exportable, &export_state, t); + let t = t as usize; + (left[t], right[t]) = synth.read_next(); } + // Append decaying silence. + Self::set_export_state(&export_state, ExportState::AppendingDecay); + decayer.decaying = true; + while decayer.decaying { + decayer.decay_two_channels(&mut left, &mut right, &mut synth); + } + // Convert. + Self::set_export_state(&export_state, ExportState::WritingToDisk); + let filename = path.file_stem().unwrap().to_str().unwrap(); + let extension = extension.to_str(true); + let path = match &exportable.suffix { + Some(suffix) => path + .parent() + .unwrap() + .join(format!("{}_{}{}", filename, suffix, extension)) + .to_path_buf(), + None => path + .parent() + .unwrap() + .join(format!("{}{}", filename, extension)) + .to_path_buf(), + }; + let audio = [left, right]; + match &exporter.export_type.get() { + ExportType::Mid => { + panic!("Tried exporting a .mid from the synthesizer") + } + // Export to a .wav file. + ExportType::Wav => { + exporter.wav(&path, &audio); + } + ExportType::MP3 => { + exporter.mp3(&path, &audio); + } + ExportType::Ogg => { + exporter.ogg(&path, &audio); + } + ExportType::Flac => exporter.flac(&path, &audio), + } + // Done. + Self::set_export_state(&export_state, ExportState::Done); + } + Self::set_export_state(&export_state, ExportState::NotExporting); + synth.lock().set_sample_rate(player_framerate); + } + + /// Set the exporter's framerate. + fn set_export_framerate(&mut self) { + let framerate = self.exporter.framerate.get_f(); + let mut synth = self.synth.lock(); + synth.set_sample_rate(framerate); + } + + /// Set the number of exported wav samples. + fn set_export_state_wav( + exportable: &Exportable, + export_state: &SharedExportState, + exported_samples: u64, + ) { + let mut export_state = export_state.lock(); + *export_state = ExportState::WritingWav { + total_samples: exportable.total_samples, + exported_samples, + } + } + + /// Set the export state. + fn set_export_state(export_state: &SharedExportState, state: ExportState) { + let mut export_state = export_state.lock(); + *export_state = state; + } + + fn get_export_file_suffix(&self, track: &MidiTrack) -> String { + // Get the path for this track. + match self.exporter.multi_file_suffix.get() { + MultiFileSuffix::Channel => track.channel.to_string(), + MultiFileSuffix::Preset => self + .state + .programs + .get(&track.channel) + .unwrap() + .preset_name + .clone(), + MultiFileSuffix::ChannelAndPreset => format!( + "{}_{}", + track.channel, + self.state.programs.get(&track.channel).unwrap().preset_name + ), } } } diff --git a/audio/src/decayer.rs b/audio/src/decayer.rs new file mode 100644 index 00000000..515f3420 --- /dev/null +++ b/audio/src/decayer.rs @@ -0,0 +1,53 @@ +use crate::SharedSynth; +use oxisynth::Synth; + +/// Export this many bytes per decay chunk. +const DECAY_CHUNK_SIZE: usize = 4096; +/// Oxisynth usually doesn't zero out its audio. This is essentially an epsilon. +/// This is used to detect if the export is done. +const SILENCE: f32 = 1e-7; + +/// Write audio samples during a decay. +pub(crate) struct Decayer { + pub buffer: [f32; DECAY_CHUNK_SIZE], + buffer_1: [f32; DECAY_CHUNK_SIZE], + pub decaying: bool, +} + +impl Default for Decayer { + fn default() -> Self { + Self { + buffer: [0.0; DECAY_CHUNK_SIZE], + buffer_1: [0.0; DECAY_CHUNK_SIZE], + decaying: false, + } + } +} + +impl Decayer { + pub fn decay_shared(&mut self, synth: &SharedSynth, len: usize) { + for sample in self.buffer[0..len].chunks_mut(2) { + let mut synth = synth.lock(); + synth.write(sample); + } + self.set_decaying(len); + } + + pub fn decay_two_channels( + &mut self, + left: &mut Vec, + right: &mut Vec, + synth: &mut Synth, + ) { + // Write samples. + synth.write((self.buffer.as_mut(), self.buffer_1.as_mut())); + left.extend(self.buffer); + right.extend(self.buffer_1); + self.decaying = self.buffer.iter().any(|s| s.abs() > SILENCE) + || self.buffer_1.iter().any(|s| s.abs() > SILENCE); + } + + fn set_decaying(&mut self, len: usize) { + self.decaying = self.buffer[0..len].iter().any(|s| s.abs() > SILENCE); + } +} diff --git a/audio/src/export.rs b/audio/src/export.rs new file mode 100644 index 00000000..262c8afe --- /dev/null +++ b/audio/src/export.rs @@ -0,0 +1,13 @@ +mod export_setting; +mod export_state; +mod export_type; +mod exportable; +mod metadata; +mod multi_file_suffix; + +pub use export_setting::ExportSetting; +pub use export_state::ExportState; +pub use export_type::ExportType; +pub(crate) use exportable::Exportable; +pub use metadata::Metadata; +pub use multi_file_suffix::MultiFileSuffix; diff --git a/audio/src/exporter/export_setting.rs b/audio/src/export/export_setting.rs similarity index 100% rename from audio/src/exporter/export_setting.rs rename to audio/src/export/export_setting.rs diff --git a/audio/src/export/export_state.rs b/audio/src/export/export_state.rs new file mode 100644 index 00000000..f3324b53 --- /dev/null +++ b/audio/src/export/export_state.rs @@ -0,0 +1,15 @@ +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub enum ExportState { + NotExporting, + /// Writing samples to a wav buffer. + WritingWav { + total_samples: u64, + exported_samples: u64, + }, + /// Writing decay to a wav buffer while the audio decays. + AppendingDecay, + /// Converting the wav buffer to another file type and write to disk. + WritingToDisk, + /// Done exporting. + Done, +} diff --git a/audio/src/exporter/export_type.rs b/audio/src/export/export_type.rs similarity index 100% rename from audio/src/exporter/export_type.rs rename to audio/src/export/export_type.rs diff --git a/audio/src/export/exportable.rs b/audio/src/export/exportable.rs new file mode 100644 index 00000000..7deb202d --- /dev/null +++ b/audio/src/export/exportable.rs @@ -0,0 +1,7 @@ +use crate::midi_event_queue::MidiEventQueue; + +pub(crate) struct Exportable { + pub events: MidiEventQueue, + pub total_samples: u64, + pub suffix: Option, +} diff --git a/audio/src/exporter/metadata.rs b/audio/src/export/metadata.rs similarity index 100% rename from audio/src/exporter/metadata.rs rename to audio/src/export/metadata.rs diff --git a/audio/src/exporter/multi_file_suffix.rs b/audio/src/export/multi_file_suffix.rs similarity index 100% rename from audio/src/exporter/multi_file_suffix.rs rename to audio/src/export/multi_file_suffix.rs diff --git a/audio/src/export_state.rs b/audio/src/export_state.rs deleted file mode 100644 index afe58bee..00000000 --- a/audio/src/export_state.rs +++ /dev/null @@ -1,17 +0,0 @@ -/// The state of audio that is being exported. -#[derive(Debug, Eq, PartialEq, Copy, Clone)] -pub struct ExportState { - /// The number of samples that have been exported. - pub exported: u64, - /// The total number of samples. - pub samples: u64, -} - -impl ExportState { - pub fn new(samples: u64) -> Self { - Self { - exported: 0, - samples, - } - } -} diff --git a/audio/src/exporter.rs b/audio/src/exporter.rs index c4465f10..497c64c5 100644 --- a/audio/src/exporter.rs +++ b/audio/src/exporter.rs @@ -1,38 +1,29 @@ -mod export_type; -use common::IndexedValues; -use serde::{Deserialize, Serialize}; -mod metadata; -mod multi_file_suffix; -use crate::{AudioBuffer, SharedExporter, SynthState}; +use crate::export::{ExportSetting, ExportType, Metadata, MultiFileSuffix}; +use crate::{AudioBuffer, SynthState}; use chrono::Datelike; use chrono::Local; +use common::IndexedValues; use common::{Index, Music, Time, U64orF32, DEFAULT_FRAMERATE, PPQ_F, PPQ_U}; -pub use export_type::*; +use flacenc::bitsink::ByteSink; +use flacenc::component::BitRepr; +use flacenc::config::Encoder as FlacEncoder; +use flacenc::encode_with_fixed_block_size; +use flacenc::source::MemSource; use hound::{SampleFormat, WavSpec, WavWriter}; use id3::{Tag, TagLike, Version}; -pub use metadata::*; +use metaflac::Tag as FlacTag; use midly::num::{u15, u24, u28, u4}; use midly::{ write_std, Format, Header, MetaMessage, MidiMessage, Timing, Track, TrackEvent, TrackEventKind, }; use mp3lame_encoder::*; -pub use multi_file_suffix::*; use oggvorbismeta::*; +use serde::{Deserialize, Serialize}; use std::fs::OpenOptions; use std::io::Read; use std::io::{Cursor, Write}; use std::path::Path; -use std::sync::Arc; use vorbis_encoder::Encoder; -mod export_setting; -pub use export_setting::ExportSetting; -use flacenc::bitsink::ByteSink; -use flacenc::component::BitRepr; -use flacenc::config::Encoder as FlacEncoder; -use flacenc::encode_with_fixed_block_size; -use flacenc::source::MemSource; -use metaflac::Tag as FlacTag; -use parking_lot::Mutex; /// The number of channels. const NUM_CHANNELS: usize = 2; @@ -201,11 +192,6 @@ impl Default for Exporter { } impl Exporter { - /// Returns a new shareable Exporter. - pub fn new_shared() -> SharedExporter { - Arc::new(Mutex::new(Exporter::default())) - } - /// Export to a .mid file. /// - `path` Output to this path. /// - `music` This is what we're saving. diff --git a/audio/src/lib.rs b/audio/src/lib.rs index 0ccf5e6d..3907c8d4 100644 --- a/audio/src/lib.rs +++ b/audio/src/lib.rs @@ -1,113 +1,29 @@ //! This crate handles all audio output in Cacophony: //! -//! - `Player` handles the cpal audio output stream. It receives audio samples. -//! - `Synthesizer` handles the audio generator synthesizer. It runs in its own thread. It can receive commands and will try to send audio samples. -//! - `Conn` manages the connection between external crates (command input), the synthesizer (audio sample output), and the audio player. -//! - `Exporter` handles all exporting. This is different from writing samples; see its documentation. +//! - `Player` handles the cpal audio output stream. +//! - `Conn` manages the connection between external crates (command input), the synthesizer, and the audio player. +//! - `Exporter` handles all exporting to disk. //! -//! It is possible to rout synthesizer output to either a `Player` (to play the audio) or to a file buffer (to write to disk). +//! Various data structs are shared in a Arc> format. These aren't a unified struct because they need to be locked at different times. //! -//! There is one way to input: -//! -//! - `Command` is the enum value describing a synthesizer command. -//! -//! There are four data struct outputs that other crates in Cacophony can read, and may be sent by the `Conn`: -//! -//! - `SynthState` describes the state of the synthesizer. -//! - `Program` is a struct found within `SynthState` that describes a single program (preset, bank, etc.). -//! - `TimeState` describes the current playback time. -//! - `ExportState` can be used to monitor how many bytes have been exported to a .wav file. -//! -//! As far as external crates are concerned, it's only necessary to do the following: -//! -//! 1. Create a shared exporter on the main thread: `Exporter::new_shared()`. -//! 2. Call `connect()` on the main thread, which sets up everything else and returns a `Conn`. +//! As far as external crates are concerned, it's only necessary to create a new Conn: `Conn::default()`. mod command; mod conn; -mod export_state; +mod decayer; +pub mod export; pub mod exporter; +pub(crate) mod midi_event_queue; +pub mod play_state; mod player; mod program; mod synth_state; -mod synthesizer; -mod time_state; +pub(crate) mod timed_midi_event; mod types; pub use crate::command::Command; pub use crate::conn::Conn; use crate::program::Program; pub use crate::synth_state::SynthState; -use crate::time_state::TimeState; -pub(crate) use crate::types::AudioBuffer; -pub use crate::types::{AudioMessage, CommandsMessage, SharedExporter}; -use crossbeam_channel::{bounded, unbounded}; -pub use export_state::ExportState; +pub(crate) use crate::types::{AudioBuffer, SharedMidiEventQueue, SharedSynth}; +pub use crate::types::{AudioMessage, CommandsMessage, SharedExportState, SharedPlayState}; use player::Player; -use std::sync::Arc; -use std::thread::spawn; - -/// Start the synthesizer and the audio player. Returns a `conn`. -pub fn connect(exporter: &SharedExporter) -> Conn { - let (send_commands, recv_commands) = unbounded(); - let (send_state, recv_state) = bounded(1); - let (send_audio, recv_audio) = bounded(1); - let (send_time, recv_time) = bounded(1); - let (send_export, recv_export) = bounded(1); - let (send_sample, recv_sample) = bounded(1); - - let ex = Arc::clone(exporter); - // Spawn the synthesizer thread. - spawn(move || { - synthesizer::Synthesizer::start( - recv_commands, - send_audio, - send_state, - send_export, - send_time, - send_sample, - ex, - ) - }); - // Spawn the audio thread. - let player = Player::new(recv_audio); - // Get the conn. - Conn::new( - player, - send_commands, - recv_state, - recv_export, - recv_time, - recv_sample, - ) -} - -#[cfg(test)] -mod tests { - use crate::exporter::Exporter; - use crate::{connect, Command}; - use std::path::PathBuf; - - const SF_PATH: &str = "tests/CT1MBGMRSV1.06.sf2"; - const CHANNEL: u8 = 0; - - #[test] - fn sf() { - // Make sure we can load the file. - assert!(std::fs::File::open(SF_PATH).is_ok()); - let exporter = Exporter::new_shared(); - let mut conn = connect(&exporter); - let commands = vec![Command::LoadSoundFont { - path: PathBuf::from(SF_PATH), - channel: CHANNEL, - }]; - // Make sure we can send commands. - conn.send(commands); - assert!(conn.state.programs.contains_key(&CHANNEL)); - let program = &conn.state.programs[&CHANNEL]; - assert_eq!(program.num_banks, 2); - assert_eq!(program.bank_index, 0); - assert_eq!(program.num_presets, 128); - assert_eq!(program.preset_index, 0); - assert_eq!(program.preset_name, "Piano 1"); - } -} diff --git a/audio/src/midi_event_queue.rs b/audio/src/midi_event_queue.rs new file mode 100644 index 00000000..17fd99cd --- /dev/null +++ b/audio/src/midi_event_queue.rs @@ -0,0 +1,42 @@ +use super::timed_midi_event::TimedMidiEvent; +use oxisynth::MidiEvent; + +/// A queue of timed MIDI events. +#[derive(Default)] +pub(crate) struct MidiEventQueue { + /// The events. Assume that this is sorted. + events: Vec, +} + +impl MidiEventQueue { + /// Enqueue a new MIDI event. + /// + /// - `time` The start time of the event in number of samples. + /// - `event` The MIDI event. + pub(crate) fn enqueue(&mut self, time: u64, event: MidiEvent) { + // Add the event. + self.events.push(TimedMidiEvent { time, event }); + } + + pub(crate) fn get_next_time(&self) -> Option { + if self.events.is_empty() { + None + } else { + Some(self.events[0].time) + } + } + + /// Sort the list of events by start time. + pub(crate) fn sort(&mut self) { + self.events.sort() + } + + /// Dequeue any events that start at `time`. + pub(crate) fn dequeue(&mut self, time: u64) -> Vec { + let mut midi_events = vec![]; + while !self.events.is_empty() && self.events[0].time == time { + midi_events.push(self.events.remove(0).event); + } + midi_events + } +} diff --git a/audio/src/play_state.rs b/audio/src/play_state.rs new file mode 100644 index 00000000..55f2c144 --- /dev/null +++ b/audio/src/play_state.rs @@ -0,0 +1,9 @@ +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub enum PlayState { + /// Not playing any audio. + NotPlaying, + /// Playing music. There are queued events. Value: The elapsed time in samples. + Playing(u64), + /// There are no more events. Audio is decaying. + Decaying, +} diff --git a/audio/src/player.rs b/audio/src/player.rs index 6cf8ac0a..ecc3d2fc 100644 --- a/audio/src/player.rs +++ b/audio/src/player.rs @@ -1,7 +1,10 @@ -use crate::AudioMessage; +use crate::decayer::Decayer; +use crate::play_state::PlayState; +use crate::types::SharedSample; +use crate::{SharedMidiEventQueue, SharedPlayState, SharedSynth}; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; use cpal::*; -use crossbeam_channel::Receiver; +use oxisynth::Synth; const ERROR_MESSAGE: &str = "Failed to create an audio output stream: "; @@ -17,7 +20,12 @@ pub(crate) struct Player { } impl Player { - pub(crate) fn new(recv: Receiver) -> Option { + pub(crate) fn new( + midi_event_queue: SharedMidiEventQueue, + synth: SharedSynth, + sample: SharedSample, + play_state: SharedPlayState, + ) -> Option { // Get the host. let host = default_host(); // Try to get an output device. @@ -34,23 +42,20 @@ impl Player { } // We have a device and a config! Ok(config) => { - let sample_format = config.sample_format(); let framerate = config.sample_rate().0; let stream_config: StreamConfig = config.into(); let channels = stream_config.channels as usize; // Try to get a stream. - let stream = match sample_format { - SampleFormat::F32 => { - Player::run::(recv, channels, device, stream_config) - } - SampleFormat::I16 => { - Player::run::(recv, channels, device, stream_config) - } - SampleFormat::U16 => { - Player::run::(recv, channels, device, stream_config) - } - }; + let stream = Player::run( + channels, + device, + stream_config, + midi_event_queue, + synth, + sample, + play_state, + ); Some(Self { _host: host, _stream: stream, @@ -62,40 +67,128 @@ impl Player { } /// Start running the stream. - fn run( - recv: Receiver, + fn run( channels: usize, device: Device, stream_config: StreamConfig, - ) -> Option - where - T: Sample, - { + midi_event_queue: SharedMidiEventQueue, + synth: SharedSynth, + sample: SharedSample, + play_state: SharedPlayState, + ) -> Option { // Define the error callback. let err_callback = |err| println!("Stream error: {}", err); - // Move `recv` into a closure. - let next_sample = move || recv.recv(); - let two_channels = channels == 2; + let mut buffer = vec![0.0; 2]; + let mut sample_buffer = [0.0; 2]; + let mut decayer = Decayer::default(); // Define the data callback used by cpal. Move `stream_send` into the closure. - let data_callback = move |output: &mut [T], _: &OutputCallbackInfo| { - for frame in output.chunks_mut(channels) { - // Try to receive a new sample. - if let Ok((l, r)) = next_sample() { - // This is almost certainly more performant than the code in the `else` block. - if two_channels { - frame[0] = Sample::from::(&l); - frame[1] = Sample::from::(&r); - } else { - let channels = [Sample::from::(&l), Sample::from::(&r)]; - for (id, sample) in frame.iter_mut().enumerate() { - *sample = channels[id % 2]; + let data_callback = move |output: &mut [f32], _: &OutputCallbackInfo| { + let ps = *play_state.lock(); + match ps { + // Assume that there is no audio and do nothing. + PlayState::NotPlaying => (), + // Add decay. + PlayState::Decaying => { + let len = output.len(); + // Write the decay block. + decayer.decay_shared(&synth, len); + // Set the decay block. + if decayer.decaying { + // Copy into output. + if two_channels { + output.copy_from_slice(decayer.buffer[0..len].as_mut()); + } else { + for (out_frame, in_frame) in output + .chunks_mut(channels) + .zip(decayer.buffer[0..len].chunks_mut(2)) + { + for (id, sample) in out_frame.iter_mut().enumerate() { + *sample = in_frame[id % 2]; + } + } + } + } + // Done decaying. + else { + // Fill the output with silence. + output.iter_mut().for_each(|o| *o = 0.0); + let mut play_state = play_state.lock(); + *play_state = PlayState::NotPlaying; + } + } + // Playing music. + PlayState::Playing(time) => { + let len = output.len(); + // Resize the buffers. + if len > buffer.len() { + buffer.resize(len, 0.0); + } + // Get the next sample. + let mut synth = synth.lock(); + let mut midi_event_queue = midi_event_queue.lock(); + // Iterate through the output buffer's frames. + let mut begin_decay = false; + let buffer_len = len / channels; + let mut t = time; + for frame in output.chunks_mut(channels) { + match midi_event_queue.get_next_time() { + Some(next_time) => { + // There are events on this frame. + if t == next_time { + // Dequeue events. + let events = midi_event_queue.dequeue(t); + // Send the MIDI events to the synth. + if !events.is_empty() { + for event in events { + if synth.send_event(event).is_ok() {} + } + } + } + // Add the sample. + // This is almost certainly more performant than the code in the `else` block. + if two_channels { + // Get the sample. + synth.write(frame); + } + // Add for more than one channel. This is slower. + else { + synth.write(sample_buffer.as_mut_slice()); + for (id, sample) in frame.iter_mut().enumerate() { + *sample = sample_buffer[id % 2]; + } + } + // Advance time. + t += 1; + } + // There are no more events. + None => { + begin_decay = true; + break; + } } } + if begin_decay { + *play_state.lock() = PlayState::Decaying; + Self::begin_decay( + buffer[0..buffer_len].as_mut(), + output, + channels, + two_channels, + &play_state, + &mut synth, + ); + } else { + *play_state.lock() = PlayState::Playing(t); + } } } + // Share the first sample. + let mut sample = sample.lock(); + sample.0 = output[0]; + sample.1 = output[1] }; // Build the cpal output stream from the stream config info and the callbacks. @@ -108,4 +201,26 @@ impl Player { Err(_) => None, } } + + fn begin_decay( + buffer: &mut [f32], + output: &mut [f32], + channels: usize, + two_channels: bool, + play_state: &SharedPlayState, + synth: &mut Synth, + ) { + if two_channels { + synth.write(output); + } else { + // Write decay samples. + synth.write(buffer.as_mut()); + for (out_frame, in_frame) in output.chunks_mut(channels).zip(buffer.chunks(2)) { + for (id, sample) in out_frame.iter_mut().enumerate() { + *sample = in_frame[id % 2]; + } + } + } + *play_state.lock() = PlayState::Decaying; + } } diff --git a/audio/src/synth_state.rs b/audio/src/synth_state.rs index f6a6a9c0..d3068942 100644 --- a/audio/src/synth_state.rs +++ b/audio/src/synth_state.rs @@ -1,5 +1,4 @@ use crate::Program; -use crate::TimeState; use common::MAX_VOLUME; use hashbrown::HashMap; use serde::{Deserialize, Serialize}; @@ -9,8 +8,6 @@ use serde::{Deserialize, Serialize}; pub struct SynthState { /// The program state per channel. pub programs: HashMap, - /// The current playback time. - pub time: TimeState, /// The current gain. pub gain: u8, } @@ -19,7 +16,6 @@ impl Default for SynthState { fn default() -> Self { Self { programs: HashMap::new(), - time: TimeState::default(), gain: MAX_VOLUME, } } @@ -29,7 +25,6 @@ impl Clone for SynthState { fn clone(&self) -> Self { Self { programs: self.programs.clone(), - time: self.time, gain: self.gain, } } diff --git a/audio/src/synthesizer.rs b/audio/src/synthesizer.rs deleted file mode 100644 index c7c45b7a..00000000 --- a/audio/src/synthesizer.rs +++ /dev/null @@ -1,537 +0,0 @@ -use crate::exporter::*; -use crate::{ - AudioBuffer, AudioMessage, Command, CommandsMessage, ExportState, Program, SharedExporter, - SynthState, TimeState, -}; -use crossbeam_channel::{Receiver, Sender}; -use hashbrown::HashMap; -use oxisynth::{MidiEvent, SoundFont, SoundFontId, Synth}; -use std::fs::File; -use std::path::PathBuf; - -/// Export this many bytes per decay chunk. -const DECAY_CHUNK_SIZE: usize = 2048; -/// Oxisynth usually doesn't zero out its audio. This is essentially an epsilon. -/// This is used to detect if the export is done. -const SILENCE: [f32; 2] = [-1e-7, 1e-7]; - -/// A convenient wrapper for a SoundFont. -struct SoundFontBanks { - id: SoundFontId, - /// The banks and their presets. - banks: HashMap>, -} - -impl SoundFontBanks { - pub fn new(font: SoundFont, synth: &mut Synth) -> Self { - let mut banks: HashMap> = HashMap::new(); - (0u32..129u32).for_each(|b| { - let presets: Vec = (0u8..128) - .filter(|p| font.preset(b, *p).is_some()) - .collect(); - if !presets.is_empty() { - banks.insert(b, presets); - } - }); - - let id = synth.add_font(font, true); - Self { id, banks } - } -} - -/// A queued MIDI event. -struct QueuedEvent { - /// The event time. - time: u64, - /// The event. - event: MidiEvent, -} - -/// Synthesize audio. -/// -/// - A list of `Command` can be received from the `Conn`. If received, the `Synthesizer` executes the commands and sends a `SynthState` to the `Conn`. -/// - Per frame, the `Synthesizer` reads audio from its synthesizer and tries to send a sample to the `Player` and a `TimeState` to the `Conn`. -pub(crate) struct Synthesizer { - /// The synthesizer. - synth: Synth, - /// A map of the SoundFonts and their banks. Key = Path. - soundfonts: HashMap, - /// A list of queued MIDI events. - events_queue: Vec, - /// If true, we're ready to receive more commands. - ready: bool, - /// The state of the synthesizer. - state: SynthState, - /// The export state. - export_state: Option, - /// The export file path. - export_path: Option, - /// The exporter. - exporter: SharedExporter, - /// The buffer that the exporter writes to. - export_buffer: AudioBuffer, - /// If true, we need to send the export state. - send_export_state: bool, -} - -impl Synthesizer { - /// Start the synthesizer loop. - /// - /// - `recv_commands` Receive commands from the conn. - /// - `send_audio` Send audio samples to the player. - /// - `send_state` Send a state to the conn. - /// - `send_export` Send audio samples to an exporter. - /// - `send_time` Send the time to the conn. - /// - `send_sample` Send an audio sample to the conn. - /// - `exporter` The shared exporter. - pub(crate) fn start( - recv_commands: Receiver, - send_audio: Sender, - send_state: Sender, - send_export: Sender>, - send_time: Sender, - send_sample: Sender, - exporter: SharedExporter, - ) { - // Create the synthesizer. - let mut s = Synthesizer { - synth: Synth::default(), - soundfonts: HashMap::new(), - events_queue: vec![], - ready: true, - state: SynthState::default(), - export_path: None, - export_state: None, - exporter, - send_export_state: false, - export_buffer: [vec![], vec![]], - }; - s.synth.set_gain(127.0); - loop { - if s.ready { - // Try to receive commands. - match recv_commands.try_recv() { - Err(_) => (), - Ok(commands) => { - s.ready = false; - for command in commands.iter() { - match command { - Command::SetFramerate { framerate } => { - s.synth.set_sample_rate(*framerate as f32) - } - Command::PlayMusic { time } => { - s.events_queue.clear(); - s.state.time.music = true; - s.state.time.time = Some(*time); - } - // Stop all notes. - Command::StopMusic => { - s.state.programs.keys().for_each(|c| { - Synthesizer::send_event( - MidiEvent::AllNotesOff { channel: *c }, - &mut s.synth, - ) - }); - // Clear the queue of commands. - s.events_queue.clear(); - // Stop the time. - s.state.time.music = false; - s.state.time.time = None; - } - // Schedule a stop-all event. - Command::StopMusicAt { time } => { - s.state.programs.keys().for_each(|c| { - s.events_queue.push(QueuedEvent { - time: *time, - event: MidiEvent::AllNotesOff { channel: *c }, - }); - }); - s.sort_queue(); - } - // Turn off all sound. - Command::SoundOff => { - s.state.programs.keys().for_each(|c| { - Synthesizer::send_event( - MidiEvent::AllSoundOff { channel: *c }, - &mut s.synth, - ) - }); - } - // Note-on ASAP. Schedule a note-off as well. - Command::NoteOn { - channel, - key, - velocity, - } => { - let ch = *channel; - let k = *key; - Synthesizer::send_event( - MidiEvent::NoteOn { - channel: ch, - key: k, - vel: *velocity, - }, - &mut s.synth, - ); - s.state.time.time = Some(0); - s.sort_queue(); - } - // Schedule a note-on and a note-off. - Command::NoteOnAt { - channel, - key, - velocity, - start, - end, - } => { - let channel = *channel; - let key = *key; - s.events_queue.push(QueuedEvent { - time: *start, - event: MidiEvent::NoteOn { - channel, - key, - vel: *velocity, - }, - }); - s.events_queue.push(QueuedEvent { - time: *end, - event: MidiEvent::NoteOff { channel, key }, - }); - s.sort_queue(); - } - // Note-off ASAP. - Command::NoteOff { channel, key } => Synthesizer::send_event( - MidiEvent::NoteOff { - channel: *channel, - key: *key, - }, - &mut s.synth, - ), - // Program select. - Command::SetProgram { - channel, - path, - bank_index, - preset_index, - } => { - let sf = &s.soundfonts[path]; - let mut banks = sf.banks.keys().copied().collect::>(); - banks.sort(); - let bank = banks[*bank_index]; - let preset = sf.banks[&bank][*preset_index]; - let channel = *channel; - if s.synth.program_select(channel, sf.id, bank, preset).is_ok() - { - s.set_program(channel, path, bank, preset); - } - } - // Unset the program for this track. - Command::UnsetProgram { channel } => { - s.state.programs.remove(channel); - } - // Load SoundFont. - Command::LoadSoundFont { channel, path } => match &s - .soundfonts - .get(path) - { - // We already loaded this font. - Some(_) => s.set_program_default(*channel, path), - // Load the font. - None => match SoundFont::load(&mut File::open(path).unwrap()) { - Ok(font) => { - let banks = SoundFontBanks::new(font, &mut s.synth); - s.soundfonts.insert(path.clone(), banks); - // Set the default program. - s.set_program_default(*channel, path); - // Restore the other programs. - let programs = s.state.programs.clone(); - for program in - programs.iter().filter(|p| p.0 != channel) - { - s.synth - .program_select( - *program.0, - s.soundfonts[&program.1.path].id, - program.1.bank, - program.1.preset, - ) - .unwrap(); - } - } - Err(error) => { - panic!("Failed to load SoundFont: {:?}", error) - } - }, - }, - Command::SetGain { gain } => { - s.synth.set_gain(*gain as f32 / 127.0); - s.state.gain = *gain; - } - // Start to export audio. - Command::Export { path, state } => { - s.export_path = Some(path.clone()); - s.export_state = Some(*state); - // Clear the buffers. - s.export_buffer[0].clear(); - s.export_buffer[1].clear(); - } - // Send the export state. - Command::SendExportState => s.send_export_state = true, - } - } - // Try to send the state. - if send_state.send(s.state.clone()).is_ok() {} - } - } - } - - if let Some(time) = s.state.time.time { - // Execute any commands that are at t0 = t. - if !s.events_queue.is_empty() && s.events_queue[0].time == time { - s.events_queue - .iter() - .filter(|e| e.time == time) - .for_each(|e| { - // Stop time. - if let MidiEvent::AllNotesOff { channel: _ } = e.event { - s.state.time.time = None; - s.state.time.music = false; - } - // Send. - Synthesizer::send_event( - Synthesizer::copy_midi_event(&e.event), - &mut s.synth, - ) - }); - // Remove the events. - s.events_queue.retain(|e| e.time != time); - } - } - - // Either export audio or play the file. - match &mut s.export_state { - Some(export_state) => { - // Are we done exporting? - if export_state.exported >= export_state.samples { - let mut decaying = false; - for _ in 0..DECAY_CHUNK_SIZE { - // Read a sample. - let sample = s.synth.read_next(); - // Write the sample. - s.export_buffer[0].push(sample.0); - s.export_buffer[1].push(sample.1); - // There is still sound. - if sample.0 < SILENCE[0] - || sample.0 > SILENCE[1] - || sample.1 < SILENCE[0] - || sample.1 > SILENCE[1] - { - decaying = true; - } - } - // We're done! - if !decaying { - let exporter = s.exporter.lock(); - match exporter.export_type.get() { - ExportType::Mid => { - panic!("Tried exporting a .mid from the synthesizer") - } - // Export to a .wav file. - ExportType::Wav => { - exporter.wav(s.export_path.as_ref().unwrap(), &s.export_buffer); - } - ExportType::MP3 => { - exporter.mp3(s.export_path.as_ref().unwrap(), &s.export_buffer); - } - ExportType::Ogg => { - exporter.ogg(s.export_path.as_ref().unwrap(), &s.export_buffer) - } - ExportType::Flac => { - exporter.flac(s.export_path.as_ref().unwrap(), &s.export_buffer) - } - } - // Stop exporting. - s.export_state = None; - s.export_buffer[0].clear(); - s.export_buffer[1].clear(); - } - } else if let Some(time) = s.state.time.time.as_mut() { - // There are no more events or the next event is right now. Export 1 sample. - if s.events_queue.is_empty() || s.events_queue[0].time == *time { - *time += 1; - s.export_sample(); - } - // Export up to the next event. - else { - let dt = s.events_queue[0].time - *time; - let dtu = dt as usize; - let mut left = vec![0.0; dtu]; - let mut right = vec![0.0; dtu]; - s.synth.write((left.as_mut_slice(), right.as_mut_slice())); - s.export_buffer[0].append(&mut left); - s.export_buffer[1].append(&mut right); - *time += dt; - // Increment the number of exported samples. - s.export_state.as_mut().unwrap().exported += dt; - } - } else { - s.export_sample(); - } - // We're ready for a new message. - s.ready = true; - } - // Play. - None => { - // Get the sample. - let sample = s.synth.read_next(); - match send_audio.send(sample) { - // We sent a message. Increment the time. - Ok(_) => { - // Increment time. - if let Some(time) = s.state.time.time.as_mut() { - *time += 1; - } - // We're ready for a new message. - s.ready = true; - } - // Wait. - Err(_) => continue, - } - // Send the sample. - let _ = send_sample.try_send(sample); - // Send the time state. - let _ = send_time.try_send(s.state.time); - } - } - // Stop exporting. - if s.export_state.is_none() && s.export_path.is_some() { - s.export_path = None; - } - // Send the export state. - if s.send_export_state && send_export.send(s.export_state).is_ok() { - s.send_export_state = false; - } - } - } - - /// Send a MidiEvent to the Synth. We don't care if it succeeds or not. - fn send_event(event: MidiEvent, synth: &mut Synth) { - if synth.send_event(event).is_ok() {} - } - - /// Copy a MIDI event. It's very dumb that we have to do it this way but... ok fine. - fn copy_midi_event(event: &MidiEvent) -> MidiEvent { - match event { - MidiEvent::NoteOn { channel, key, vel } => MidiEvent::NoteOn { - channel: *channel, - key: *key, - vel: *vel, - }, - MidiEvent::NoteOff { channel, key } => MidiEvent::NoteOff { - channel: *channel, - key: *key, - }, - MidiEvent::ControlChange { - channel, - ctrl, - value, - } => MidiEvent::ControlChange { - channel: *channel, - ctrl: *ctrl, - value: *value, - }, - MidiEvent::AllNotesOff { channel } => MidiEvent::AllNotesOff { channel: *channel }, - MidiEvent::AllSoundOff { channel } => MidiEvent::AllSoundOff { channel: *channel }, - MidiEvent::PitchBend { channel, value } => MidiEvent::PitchBend { - channel: *channel, - value: *value, - }, - MidiEvent::ProgramChange { - channel, - program_id, - } => MidiEvent::ProgramChange { - channel: *channel, - program_id: *program_id, - }, - MidiEvent::ChannelPressure { channel, value } => MidiEvent::ChannelPressure { - channel: *channel, - value: *value, - }, - MidiEvent::PolyphonicKeyPressure { - channel, - key, - value, - } => MidiEvent::PolyphonicKeyPressure { - channel: *channel, - key: *key, - value: *value, - }, - MidiEvent::SystemReset => MidiEvent::SystemReset, - } - } - - /// Set the synthesizer program to a program. - fn set_program(&mut self, channel: u8, path: &PathBuf, bank: u32, preset: u8) { - let sf_banks = &self.soundfonts[path].banks; - // Get the bank info. - let mut banks: Vec = sf_banks.keys().copied().collect(); - banks.sort(); - let bank_index = banks.iter().position(|&b| b == bank).unwrap(); - let bank: u32 = banks[bank_index]; - // Get the preset info. - let presets = sf_banks[&bank].clone(); - let preset_index = presets.iter().position(|&p| p == preset).unwrap(); - let preset_name = self - .synth - .channel_preset(channel) - .unwrap() - .name() - .to_string(); - let num_banks = banks.len(); - let num_presets = presets.len(); - let program = Program { - path: path.clone(), - num_banks, - bank_index, - bank, - num_presets, - preset_index, - preset_name, - preset, - }; - // Remember the program. - self.state.programs.insert(channel, program); - } - - /// Set the synthesizer program to a default program. - fn set_program_default(&mut self, channel: u8, path: &PathBuf) { - let sf_banks = &self.soundfonts[path].banks; - // Get the bank info. - let mut banks: Vec = sf_banks.keys().copied().collect(); - banks.sort(); - let bank = banks[0]; - let preset = sf_banks[&bank][0]; - // Select the default program. - let id = self.soundfonts[path].id; - self.synth - .program_select(channel, id, bank, preset) - .unwrap(); - self.set_program(channel, path, bank, preset); - } - - /// Sort the events queue by time. - fn sort_queue(&mut self) { - self.events_queue.sort_by(|a, b| a.time.cmp(&b.time)) - } - - /// Push one sample to the export buffer. - fn export_sample(&mut self) { - let sample = self.synth.read_next(); - // Write the sample. - self.export_buffer[0].push(sample.0); - self.export_buffer[1].push(sample.1); - // Increment the number of exported samples. - self.export_state.as_mut().unwrap().exported += 1; - } -} diff --git a/audio/src/time_state.rs b/audio/src/time_state.rs deleted file mode 100644 index de5b305c..00000000 --- a/audio/src/time_state.rs +++ /dev/null @@ -1,10 +0,0 @@ -use serde::{Deserialize, Serialize}; - -/// Describes the state of audio playback. -#[derive(Copy, Clone, Default, Deserialize, Serialize)] -pub struct TimeState { - /// The current playback time in samples. - pub time: Option, - /// If true, we're playing music, as opposed to random user input. - pub music: bool, -} diff --git a/audio/src/timed_midi_event.rs b/audio/src/timed_midi_event.rs new file mode 100644 index 00000000..1a57990c --- /dev/null +++ b/audio/src/timed_midi_event.rs @@ -0,0 +1,68 @@ +use oxisynth::MidiEvent; +use std::cmp::Ordering; + +/// A MIDI event with a start time. +#[derive(Copy, Clone, Eq, PartialEq)] +pub(crate) struct TimedMidiEvent { + /// The event time in number of samples. + pub(crate) time: u64, + /// The event. + pub(crate) event: MidiEvent, +} + +impl Ord for TimedMidiEvent { + fn cmp(&self, other: &Self) -> Ordering { + match self.time.cmp(&other.time) { + Ordering::Less => Ordering::Less, + Ordering::Greater => Ordering::Greater, + Ordering::Equal => match (&self.event, &other.event) { + // Two note-on events are equal. + ( + MidiEvent::NoteOn { + channel: _, + key: _, + vel: _, + }, + MidiEvent::NoteOn { + channel: _, + key: _, + vel: _, + }, + ) => Ordering::Equal, + // Two note-off events are equal. + ( + MidiEvent::NoteOff { channel: _, key: _ }, + MidiEvent::NoteOff { channel: _, key: _ }, + ) => Ordering::Equal, + // Note-off events are always before all other events. + (MidiEvent::NoteOff { channel: _, key: _ }, _) => Ordering::Less, + // Note-on events are always after note-offs. + ( + MidiEvent::NoteOn { + channel: _, + key: _, + vel: _, + }, + MidiEvent::NoteOff { channel: _, key: _ }, + ) => Ordering::Greater, + // Note-on events are always before all other events except note-offs. + ( + MidiEvent::NoteOn { + channel: _, + key: _, + vel: _, + }, + _, + ) => Ordering::Less, + // All other events are equal. + _ => Ordering::Equal, + }, + } + } +} + +impl PartialOrd for TimedMidiEvent { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} diff --git a/audio/src/types.rs b/audio/src/types.rs index 674e8d82..edde0091 100644 --- a/audio/src/types.rs +++ b/audio/src/types.rs @@ -1,5 +1,8 @@ -use crate::exporter::Exporter; +use crate::export::ExportState; +use crate::midi_event_queue::MidiEventQueue; +use crate::play_state::PlayState; use crate::Command; +use oxisynth::Synth; use parking_lot::Mutex; use std::sync::Arc; @@ -9,5 +12,8 @@ pub type AudioMessage = (f32, f32); pub type CommandsMessage = Vec; /// Type alias for an audio buffer. pub(crate) type AudioBuffer = [Vec; 2]; -/// The exporter. -pub type SharedExporter = Arc>; +pub(crate) type SharedSynth = Arc>; +pub type SharedExportState = Arc>; +pub(crate) type SharedMidiEventQueue = Arc>; +pub type SharedPlayState = Arc>; +pub(crate) type SharedSample = Arc>; diff --git a/changelog.md b/changelog.md index c5346a14..9f53c932 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,13 @@ +# 0.2.x + +## 0.2.0 + +Cacophony uses a lot of CPU resources even when it's idle. It shouldn't do that! I reduced Cacophony's idle CPU usage by around 20-50% and by 15-50% while playing music; the exact percentage varies depending on the CPU and the OS. This update is the first big CPU optimization, and probably the most significant; I think all other subsequent optimizations will chip away at the problem. + +This update doesn't have any new features or bug fixes. In the future, I'm going to reserve major releases (0.x.0) for big new features, but I had to rewrite so much of Cacophony's code, and the results are such a big improvement, that I'm making this a major release anyway. + +*(Backend notes)* The problem was that the `Synthesizer` struct (which no longer exists) ran a very fast infinite loop to send samples to `Player`, and the loop needlessly consumed CPU resources. I replaced this loop with a bunch of `Arc>` values that are shared between `Conn` and `Player`. As a result of removing `Synthesizer` I had to reorganize all of the exporter code. There are a lot of small changes that I'm not going to list here because let's be real, no one reads verbose changelogs, but the most noticeable change is that `Exporter` is a field in `Conn` and is no longer shared (there is no `Arc>` anywhere in the code). This change affects a *lot* of the codebase, but it's mostly just refactoring with zero functional differences. + # 0.1.x ## 0.1.4 diff --git a/common/src/midi_track.rs b/common/src/midi_track.rs index 5e6b2243..0d9dd107 100644 --- a/common/src/midi_track.rs +++ b/common/src/midi_track.rs @@ -31,6 +31,24 @@ impl MidiTrack { pub fn get_end(&self) -> Option { self.notes.iter().map(|n| n.end).max() } + + /// Returns the track gain as a float between 0 and 1. + pub fn get_gain_f(&self) -> f32 { + self.gain as f32 / MAX_VOLUME as f32 + } + + /// Returns all notes in the track that can be played (they are after t0). + pub fn get_playback_notes(&self, start: u64) -> Vec { + let gain = self.get_gain_f(); + let mut notes = vec![]; + for note in self.notes.iter().filter(|n| n.start >= start) { + let mut n1 = *note; + n1.velocity = (n1.velocity as f32 * gain) as u8; + notes.push(n1); + } + notes.sort(); + notes + } } impl Clone for MidiTrack { diff --git a/common/src/music.rs b/common/src/music.rs index da9db0c0..3ab9ec98 100644 --- a/common/src/music.rs +++ b/common/src/music.rs @@ -26,4 +26,16 @@ impl Music { None => None, } } + + /// Returns all tracks that can be played. + pub fn get_playable_tracks(&self) -> Vec<&MidiTrack> { + // Get all tracks that can play music. + let tracks = match self.midi_tracks.iter().find(|t| t.solo) { + // Only include the solo track. + Some(solo) => vec![solo], + // Only include unmuted tracks. + None => self.midi_tracks.iter().filter(|t| !t.mute).collect(), + }; + tracks + } } diff --git a/data/text.csv b/data/text.csv index a3096dd3..a7a84f86 100644 --- a/data/text.csv +++ b/data/text.csv @@ -491,4 +491,6 @@ LINKS_PANEL_INPUT_TTS_0,Open links in your web browser. LINKS_PANEL_INPUT_TTS_1,\0 to open the Cacophony website. LINKS_PANEL_INPUT_TTS_2,\0 to open an invite link to the Cacophony Discord server. LINKS_PANEL_INPUT_TTS_3,\0 to open an Cacophony repo. -LINKS_PANEL_INPUT_TTS_4,\0 to close this panel. \ No newline at end of file +LINKS_PANEL_INPUT_TTS_4,\0 to close this panel. +EXPORT_PANEL_APPENDING_DECAY,Appending decay... +EXPORT_PANEL_WRITING,Writing to disk... \ No newline at end of file diff --git a/io/src/abc123.rs b/io/src/abc123.rs index bd5173c0..e69321a8 100644 --- a/io/src/abc123.rs +++ b/io/src/abc123.rs @@ -66,44 +66,6 @@ impl AlphanumericModifiable for U64orF32 { } } -/// Handle alphanumeric input for a shared exporter. -/// -/// - `f` A closure to modify a string, e.g. `|e| &mut e.metadata.title`. -/// - `input` The input state. This is used to check if alphanumeric input is allowed. -/// - `exporter` The exporter state. -pub(crate) fn update_shared_exporter( - f: F, - input: &Input, - exporter: &mut SharedExporter, -) -> bool -where - F: FnMut(&mut Exporter) -> &mut T, - T: Clone + AlphanumericModifiable, -{ - let mut ex = exporter.lock(); - update_exporter(f, input, &mut ex) -} - -/// Do something with a shared exporter when alphanumeric input is disabled. -/// -/// - `f` A closure to modify a string, e.g. `|e| &mut e.metadata.title`. -/// - `exporter` The exporter state. -pub(crate) fn on_disable_shared_exporter( - mut f: F, - exporter: &mut SharedExporter, - default_value: T, -) where - F: FnMut(&mut Exporter) -> &mut T, - T: Clone + AlphanumericModifiable, -{ - let mut ex = exporter.lock(); - let v = f(&mut ex); - // If the value is empty, set a default value. - if !v.is_valid() { - *v = default_value; - } -} - /// Handle alphanumeric input for the exporter. /// /// - `f` A closure to modify a string, e.g. `|e| &mut e.metadata.title`. diff --git a/io/src/export_panel.rs b/io/src/export_panel.rs index d8357aa4..813dd94d 100644 --- a/io/src/export_panel.rs +++ b/io/src/export_panel.rs @@ -1,4 +1,5 @@ use crate::panel::*; +use audio::export::ExportState; use common::PanelType; /// Are we done yet? @@ -29,28 +30,28 @@ impl Panel for ExportPanel { _: &mut TTS, _: &Text, _: &mut PathsState, - _: &mut SharedExporter, ) -> Option { // We're done. - if conn.export_state.is_none() { + let export_state = conn.export_state.lock(); + if *export_state == ExportState::NotExporting { state.panels = self.panels.clone(); state.focus.set(self.focus); } None } - fn on_disable_abc123(&mut self, _: &mut State, _: &mut SharedExporter) {} + fn on_disable_abc123(&mut self, _: &mut State, _: &mut Conn) {} fn update_abc123( &mut self, _: &mut State, _: &Input, - _: &mut SharedExporter, + _: &mut Conn, ) -> (Option, bool) { (None, false) } - fn allow_alphanumeric_input(&self, _: &State, _: &SharedExporter) -> bool { + fn allow_alphanumeric_input(&self, _: &State, _: &Conn) -> bool { false } diff --git a/io/src/export_settings_panel.rs b/io/src/export_settings_panel.rs index fd16481d..9cc57acf 100644 --- a/io/src/export_settings_panel.rs +++ b/io/src/export_settings_panel.rs @@ -1,6 +1,7 @@ use crate::abc123::{on_disable_exporter, update_exporter}; use crate::panel::*; -use audio::exporter::*; +use audio::export::{ExportSetting, ExportType, MultiFileSuffix}; +use audio::exporter::{Exporter, MP3_BIT_RATES}; use audio::Conn; use common::{IndexedValues, U64orF32}; use serde::de::DeserializeOwned; @@ -218,7 +219,7 @@ impl ExportSettingsPanel { input: &Input, tts: &mut TTS, text: &Text, - exporter: &mut SharedExporter, + exporter: &mut Exporter, ) -> Option where F: FnMut(&mut Exporter) -> &mut IndexedValues, @@ -226,8 +227,7 @@ impl ExportSettingsPanel { { // Status TTS. if input.happened(&InputEvent::StatusTTS) { - let mut ex = exporter.lock(); - let s = match &f(&mut ex).get() { + let s = match &f(exporter).get() { ExportSetting::Framerate => { TtsString::from(text.get("EXPORT_SETTINGS_PANEL_STATUS_TTS_FRAMERATE")) } @@ -235,7 +235,7 @@ impl ExportSettingsPanel { tooltips, "EXPORT_SETTINGS_PANEL_STATUS_TTS_TITLE_ABC123", "EXPORT_SETTINGS_PANEL_STATUS_TTS_TITLE_NO_ABC123", - &Some(ex.metadata.title.clone()), + &Some(exporter.metadata.title.clone()), state, input, text, @@ -244,7 +244,7 @@ impl ExportSettingsPanel { tooltips, "EXPORT_SETTINGS_PANEL_STATUS_TTS_ARTIST", "EXPORT_SETTINGS_PANEL_STATUS_TTS_ARTIST_NO_ABC123", - &ex.metadata.artist, + &exporter.metadata.artist, state, input, text, @@ -253,7 +253,7 @@ impl ExportSettingsPanel { tooltips, "EXPORT_SETTINGS_PANEL_STATUS_TTS_COPYRIGHT_ENABLED", "EXPORT_SETTINGS_PANEL_STATUS_TTS_COPYRIGHT_DISABLED", - ex.copyright, + exporter.copyright, input, text, ), @@ -261,7 +261,7 @@ impl ExportSettingsPanel { tooltips, "EXPORT_SETTINGS_PANEL_STATUS_TTS_ALBUM_ABC123", "EXPORT_SETTINGS_PANEL_STATUS_TTS_ALBUM_NO_ABC123", - &ex.metadata.album, + &exporter.metadata.album, state, input, text, @@ -270,7 +270,7 @@ impl ExportSettingsPanel { tooltips, "EXPORT_SETTINGS_PANEL_STATUS_TTS_GENRE_ABC123", "EXPORT_SETTINGS_PANEL_STATUS_TTS_GENRE_NO_ABC123", - &ex.metadata.genre, + &exporter.metadata.genre, state, input, text, @@ -279,26 +279,31 @@ impl ExportSettingsPanel { tooltips, "EXPORT_SETTINGS_PANEL_STATUS_TTS_COMMENT_ABC123", "EXPORT_SETTINGS_PANEL_STATUS_TTS_COMMENT_NO_ABC123", - &ex.metadata.comment, + &exporter.metadata.comment, state, input, text, ), - ExportSetting::Mp3BitRate => TtsString::from(text.get_with_values( - "EXPORT_SETTINGS_PANEL_STATUS_TTS_BIT_RATE", - &[&((MP3_BIT_RATES[ex.mp3_bit_rate.get()] as u16) as u32 * 1000).to_string()], - )), + ExportSetting::Mp3BitRate => TtsString::from( + text.get_with_values( + "EXPORT_SETTINGS_PANEL_STATUS_TTS_BIT_RATE", + &[ + &((MP3_BIT_RATES[exporter.mp3_bit_rate.get()] as u16) as u32 * 1000) + .to_string(), + ], + ), + ), ExportSetting::Mp3Quality => TtsString::from(text.get_with_values( "EXPORT_SETTINGS_PANEL_STATUS_TTS_QUALITY", - &[&ex.mp3_quality.get().to_string()], + &[&exporter.mp3_quality.get().to_string()], )), ExportSetting::OggQuality => TtsString::from(text.get_with_values( "EXPORT_SETTINGS_PANEL_STATUS_TTS_QUALITY", - &[&exporter.lock().ogg_quality.get().to_string()], + &[&exporter.ogg_quality.get().to_string()], )), ExportSetting::TrackNumber => TtsString::from(text.get_with_values( "EXPORT_SETTINGS_PANEL_STATUS_TTS_TRACK_NUMBER", - &[&match ex.metadata.track_number { + &[&match exporter.metadata.track_number { Some(track_number) => track_number.to_string(), None => text.get("NONE"), }], @@ -307,12 +312,12 @@ impl ExportSettingsPanel { tooltips, "EXPORT_SETTINGS_PANEL_STATUS_TTS_MULTI_FILE_ENABLED", "EXPORT_SETTINGS_PANEL_STATUS_TTS_MULTI_FILE_DISABLED", - ex.multi_file, + exporter.multi_file, input, text, ), ExportSetting::MultiFileSuffix => { - let key = match &ex.multi_file_suffix.get() { + let key = match &exporter.multi_file_suffix.get() { MultiFileSuffix::Preset => { "EXPORT_SETTINGS_PANEL_STATUS_TTS_MULTI_FILE_PRESET" } @@ -330,8 +335,7 @@ impl ExportSettingsPanel { } // Input TTS. else if input.happened(&InputEvent::InputTTS) { - let mut ex = exporter.lock(); - let s = match &f(&mut ex).get() { + let s = match &f(exporter).get() { ExportSetting::Framerate => Self::get_input_lr_tts( tooltips, "EXPORT_SETTINGS_PANEL_INPUT_TTS_FRAMERATE", @@ -429,57 +433,54 @@ impl ExportSettingsPanel { } // Previous setting. else if input.happened(&InputEvent::PreviousExportSetting) { - let mut ex = exporter.lock(); - let s = f(&mut ex); + let s = f(exporter); s.index.increment(false); } // Next setting. else if input.happened(&InputEvent::NextExportSetting) { - let mut ex = exporter.lock(); - let s = f(&mut ex); + let s = f(exporter); s.index.increment(true); } else { - let mut ex = exporter.lock(); - match &f(&mut ex).get() { + match &f(exporter).get() { // Framerate. ExportSetting::Framerate => { if input.happened(&InputEvent::PreviousExportSettingValue) { - Self::set_framerate(&mut ex, false); + Self::set_framerate(exporter, false); } else if input.happened(&InputEvent::NextExportSettingValue) { - Self::set_framerate(&mut ex, true); + Self::set_framerate(exporter, true); } } ExportSetting::Copyright => { if input.happened(&InputEvent::ToggleExportSettingBoolean) { - ex.copyright = !ex.copyright; + exporter.copyright = !exporter.copyright; } } ExportSetting::TrackNumber => { if input.happened(&InputEvent::PreviousExportSettingValue) { - Self::set_track_number(&mut ex, false); + Self::set_track_number(exporter, false); } else if input.happened(&InputEvent::NextExportSettingValue) { - Self::set_track_number(&mut ex, true); + Self::set_track_number(exporter, true); } } ExportSetting::Mp3BitRate => { - Self::set_index(|e| &mut e.mp3_bit_rate, input, &mut ex); + Self::set_index(|e| &mut e.mp3_bit_rate, input, exporter); } ExportSetting::Mp3Quality => { - Self::set_index(|e| &mut e.mp3_quality, input, &mut ex); + Self::set_index(|e| &mut e.mp3_quality, input, exporter); } ExportSetting::OggQuality => { - Self::set_index(|e| &mut e.ogg_quality, input, &mut ex); + Self::set_index(|e| &mut e.ogg_quality, input, exporter); } ExportSetting::MultiFile => { if input.happened(&InputEvent::ToggleExportSettingBoolean) { - ex.multi_file = !ex.multi_file; + exporter.multi_file = !exporter.multi_file; } } ExportSetting::MultiFileSuffix => { Self::set_index( |e: &mut Exporter| &mut e.multi_file_suffix.index, input, - &mut ex, + exporter, ); } _ => (), @@ -496,19 +497,18 @@ impl ExportSettingsPanel { fn update_settings_abc123( mut f: F, input: &Input, - exporter: &mut SharedExporter, + exporter: &mut Exporter, ) -> bool where F: FnMut(&mut Exporter) -> &mut IndexedValues, [ExportSetting; N]: Serialize + DeserializeOwned, { - let mut ex = exporter.lock(); - match &f(&mut ex).get() { - ExportSetting::Title => update_exporter(|e| &mut e.metadata.title, input, &mut ex), - ExportSetting::Artist => update_exporter(|e| &mut e.metadata.artist, input, &mut ex), - ExportSetting::Album => update_exporter(|e| &mut e.metadata.album, input, &mut ex), - ExportSetting::Genre => update_exporter(|e| &mut e.metadata.genre, input, &mut ex), - ExportSetting::Comment => update_exporter(|e| &mut e.metadata.comment, input, &mut ex), + match &f(exporter).get() { + ExportSetting::Title => update_exporter(|e| &mut e.metadata.title, input, exporter), + ExportSetting::Artist => update_exporter(|e| &mut e.metadata.artist, input, exporter), + ExportSetting::Album => update_exporter(|e| &mut e.metadata.album, input, exporter), + ExportSetting::Genre => update_exporter(|e| &mut e.metadata.genre, input, exporter), + ExportSetting::Comment => update_exporter(|e| &mut e.metadata.comment, input, exporter), _ => false, } } @@ -517,21 +517,22 @@ impl ExportSettingsPanel { /// /// - `f` A closure that returns a mutable reference to an `IndexValues` of export settings (corresponding to the export type). /// - `exporter` The exporter. This will have its framerate set. - fn disable_abc123(mut f: F, exporter: &mut SharedExporter) + fn disable_abc123(mut f: F, exporter: &mut Exporter) where F: FnMut(&mut Exporter) -> &mut IndexedValues, [ExportSetting; N]: Serialize + DeserializeOwned, { - let mut ex = exporter.lock(); - match &f(&mut ex).get() { + match &f(exporter).get() { ExportSetting::Title => { - on_disable_exporter(|e| &mut e.metadata.title, &mut ex, "My Music".to_string()) + on_disable_exporter(|e| &mut e.metadata.title, exporter, "My Music".to_string()) + } + ExportSetting::Artist => { + on_disable_exporter(|e| &mut e.metadata.artist, exporter, None) } - ExportSetting::Artist => on_disable_exporter(|e| &mut e.metadata.artist, &mut ex, None), - ExportSetting::Album => on_disable_exporter(|e| &mut e.metadata.album, &mut ex, None), - ExportSetting::Genre => on_disable_exporter(|e| &mut e.metadata.genre, &mut ex, None), + ExportSetting::Album => on_disable_exporter(|e| &mut e.metadata.album, exporter, None), + ExportSetting::Genre => on_disable_exporter(|e| &mut e.metadata.genre, exporter, None), ExportSetting::Comment => { - on_disable_exporter(|e| &mut e.metadata.comment, &mut ex, None) + on_disable_exporter(|e| &mut e.metadata.comment, exporter, None) } _ => (), } @@ -541,14 +542,13 @@ impl ExportSettingsPanel { /// /// - `f` A closure that returns a mutable reference to an `IndexValues` of export settings (corresponding to the export type). /// - `exporter` The exporter. This will have its framerate set. - fn allow_abc123(f: F, exporter: &SharedExporter) -> bool + fn allow_abc123(f: F, exporter: &Exporter) -> bool where F: Fn(&Exporter) -> &IndexedValues, [ExportSetting; N]: Serialize + DeserializeOwned, { - let ex = exporter.lock(); matches!( - &f(&ex).get(), + &f(exporter).get(), ExportSetting::Title | ExportSetting::Artist | ExportSetting::Album @@ -562,18 +562,17 @@ impl Panel for ExportSettingsPanel { fn update( &mut self, state: &mut State, - _: &mut Conn, + conn: &mut Conn, input: &Input, tts: &mut TTS, text: &Text, _: &mut PathsState, - exporter: &mut SharedExporter, ) -> Option { // Close this. if input.happened(&InputEvent::CloseOpenFile) { return Some(Snapshot::from_io_commands(vec![IOCommand::CloseOpenFile])); } - let export_type = exporter.lock().export_type.get(); + let export_type = conn.exporter.export_type.get(); match export_type { ExportType::Mid => Self::update_settings( |e| &mut e.mid_settings, @@ -582,7 +581,7 @@ impl Panel for ExportSettingsPanel { input, tts, text, - exporter, + &mut conn.exporter, ), ExportType::MP3 => Self::update_settings( |e| &mut e.mp3_settings, @@ -591,7 +590,7 @@ impl Panel for ExportSettingsPanel { input, tts, text, - exporter, + &mut conn.exporter, ), ExportType::Ogg => Self::update_settings( |e| &mut e.ogg_settings, @@ -600,7 +599,7 @@ impl Panel for ExportSettingsPanel { input, tts, text, - exporter, + &mut conn.exporter, ), ExportType::Flac => Self::update_settings( |e| &mut e.flac_settings, @@ -609,7 +608,7 @@ impl Panel for ExportSettingsPanel { input, tts, text, - exporter, + &mut conn.exporter, ), ExportType::Wav => Self::update_settings( |e| &mut e.wav_settings, @@ -618,7 +617,7 @@ impl Panel for ExportSettingsPanel { input, tts, text, - exporter, + &mut conn.exporter, ), } } @@ -627,48 +626,45 @@ impl Panel for ExportSettingsPanel { &mut self, _: &mut State, input: &Input, - exporter: &mut SharedExporter, + conn: &mut Conn, ) -> (Option, bool) { - let export_type = exporter.lock().export_type.get(); - let updated = match export_type { + let updated = match conn.exporter.export_type.get() { ExportType::Mid => { - Self::update_settings_abc123(|e| &mut e.mid_settings, input, exporter) + Self::update_settings_abc123(|e| &mut e.mid_settings, input, &mut conn.exporter) } ExportType::MP3 => { - Self::update_settings_abc123(|e| &mut e.mp3_settings, input, exporter) + Self::update_settings_abc123(|e| &mut e.mp3_settings, input, &mut conn.exporter) } ExportType::Ogg => { - Self::update_settings_abc123(|e| &mut e.ogg_settings, input, exporter) + Self::update_settings_abc123(|e| &mut e.ogg_settings, input, &mut conn.exporter) } ExportType::Flac => { - Self::update_settings_abc123(|e| &mut e.flac_settings, input, exporter) + Self::update_settings_abc123(|e| &mut e.flac_settings, input, &mut conn.exporter) } ExportType::Wav => { - Self::update_settings_abc123(|e| &mut e.wav_settings, input, exporter) + Self::update_settings_abc123(|e| &mut e.wav_settings, input, &mut conn.exporter) } }; (None, updated) } - fn on_disable_abc123(&mut self, _: &mut State, exporter: &mut SharedExporter) { - let export_type = exporter.lock().export_type.get(); - match export_type { - ExportType::Mid => Self::disable_abc123(|e| &mut e.mid_settings, exporter), - ExportType::MP3 => Self::disable_abc123(|e| &mut e.mp3_settings, exporter), - ExportType::Ogg => Self::disable_abc123(|e| &mut e.ogg_settings, exporter), - ExportType::Flac => Self::disable_abc123(|e| &mut e.flac_settings, exporter), - ExportType::Wav => Self::disable_abc123(|e| &mut e.wav_settings, exporter), + fn on_disable_abc123(&mut self, _: &mut State, conn: &mut Conn) { + match conn.exporter.export_type.get() { + ExportType::Mid => Self::disable_abc123(|e| &mut e.mid_settings, &mut conn.exporter), + ExportType::MP3 => Self::disable_abc123(|e| &mut e.mp3_settings, &mut conn.exporter), + ExportType::Ogg => Self::disable_abc123(|e| &mut e.ogg_settings, &mut conn.exporter), + ExportType::Wav => Self::disable_abc123(|e| &mut e.wav_settings, &mut conn.exporter), + ExportType::Flac => Self::disable_abc123(|e| &mut e.flac_settings, &mut conn.exporter), }; } - fn allow_alphanumeric_input(&self, _: &State, exporter: &SharedExporter) -> bool { - let export_type = exporter.lock().export_type.get(); - match export_type { - ExportType::Mid => Self::allow_abc123(|e| &e.mid_settings, exporter), - ExportType::MP3 => Self::allow_abc123(|e| &e.mp3_settings, exporter), - ExportType::Ogg => Self::allow_abc123(|e| &e.ogg_settings, exporter), - ExportType::Flac => Self::allow_abc123(|e| &e.flac_settings, exporter), - ExportType::Wav => Self::allow_abc123(|e: &Exporter| &e.wav_settings, exporter), + fn allow_alphanumeric_input(&self, _: &State, conn: &Conn) -> bool { + match conn.exporter.export_type.get() { + ExportType::Mid => Self::allow_abc123(|e| &e.mid_settings, &conn.exporter), + ExportType::MP3 => Self::allow_abc123(|e| &e.mp3_settings, &conn.exporter), + ExportType::Ogg => Self::allow_abc123(|e| &e.ogg_settings, &conn.exporter), + ExportType::Wav => Self::allow_abc123(|e| &e.wav_settings, &conn.exporter), + ExportType::Flac => Self::allow_abc123(|e| &e.flac_settings, &conn.exporter), } } diff --git a/io/src/import_midi.rs b/io/src/import_midi.rs index 2923e4cd..9a457a14 100644 --- a/io/src/import_midi.rs +++ b/io/src/import_midi.rs @@ -1,16 +1,11 @@ -use audio::{Command, Conn, SharedExporter}; +use audio::{Command, Conn}; use common::{MidiTrack, Music, Note, Paths, State, U64orF32}; use midly::{MetaMessage, MidiMessage, Smf, Timing, TrackEventKind}; use std::fs::read; use std::path::Path; use std::str::from_utf8; -pub(crate) fn import( - path: &Path, - state: &mut State, - conn: &mut Conn, - exporter: &mut SharedExporter, -) { +pub(crate) fn import(path: &Path, state: &mut State, conn: &mut Conn) { let bytes = read(path).unwrap(); let smf = Smf::parse(&bytes).unwrap(); let timing = match smf.header.timing { @@ -24,7 +19,7 @@ pub(crate) fn import( let c = i as u8; let mut track = MidiTrack::new(c); // Load the default SoundFont. - conn.send(vec![Command::LoadSoundFont { + conn.do_commands(&[Command::LoadSoundFont { channel: c, path: paths.default_soundfont_path.clone(), }]); @@ -40,9 +35,8 @@ pub(crate) fn import( TrackEventKind::Meta(message) => match message { MetaMessage::Copyright(data) => { if let Ok(copyright) = from_utf8(data) { - let mut exporter = exporter.lock(); - exporter.copyright = true; - exporter.metadata.artist = Some(copyright.to_string()); + conn.exporter.copyright = true; + conn.exporter.metadata.artist = Some(copyright.to_string()); } } MetaMessage::Tempo(data) => { @@ -55,8 +49,7 @@ pub(crate) fn import( } MetaMessage::Text(data) => { if let Ok(text) = from_utf8(data) { - let mut exporter = exporter.lock(); - exporter.metadata.comment = Some(text.to_string()) + conn.exporter.metadata.comment = Some(text.to_string()) } } _ => (), @@ -87,7 +80,7 @@ pub(crate) fn import( } // Set the preset. MidiMessage::ProgramChange { program } => { - conn.send(vec![Command::SetProgram { + conn.do_commands(&[Command::SetProgram { channel: track.channel, path: paths.default_soundfont_path.clone(), bank_index: conn diff --git a/io/src/io_command.rs b/io/src/io_command.rs index 2b973a2b..5cac8d61 100644 --- a/io/src/io_command.rs +++ b/io/src/io_command.rs @@ -1,5 +1,4 @@ use common::open_file::OpenFileType; -use std::path::PathBuf; /// Commands for the IO struct. #[derive(Clone)] @@ -7,7 +6,7 @@ pub(crate) enum IOCommand { /// Enable the open-file panel. EnableOpenFile(OpenFileType), /// Begin to export. - Export(PathBuf), + Export, /// Close the open-file panel. CloseOpenFile, /// Quit the application. diff --git a/io/src/lib.rs b/io/src/lib.rs index afd43778..5d4d05f4 100644 --- a/io/src/lib.rs +++ b/io/src/lib.rs @@ -5,21 +5,19 @@ //! Per frame, `IO` listens for user input via an `Input` (see the `input` crate), and then does any of the following: //! //! - Update `State` (see the `common` crate), for example add a new track. -//! - Send a list of `Command` to the `Conn` (see the `audio` crate). +//! - Update `Conn` (see the `audio` crate), for example to play notes. //! - Send an internal `IOCommand` to itself. //! - Play text-to-speech audio (see the `text` crate). //! -//! The first two options (state and command) will create a copy of the current `State` which will be added to an undo stack. +//! Certain operations will create a copy of the current `State` which will be added to an undo stack. //! Undoing an action reverts the app to that state, pops it from the undo stack, and pushes it to the redo stack. //! //! `IO` divides input listening into discrete panels, e.g. the music panel and the tracks panel. //! Each panel implements the `Panel` trait. -use audio::exporter::{Exporter, MultiFileSuffix}; -use audio::{Command, CommandsMessage, Conn, ExportState, SharedExporter}; -use common::{ - InputState, MidiTrack, Music, Note, PanelType, Paths, PathsState, SelectMode, State, MAX_VOLUME, -}; +use audio::export::ExportState; +use audio::Conn; +use common::{InputState, Music, PanelType, Paths, PathsState, SelectMode, State}; use edit::edit_file; use hashbrown::HashMap; use ini::Ini; @@ -58,16 +56,13 @@ use links_panel::LinksPanel; /// The maximum size of the undo stack. const MAX_UNDOS: usize = 100; -/// Commands that are queued for export. -type QueuedExportCommands = (CommandsMessage, Option); /// Parse user input and apply it to the application's various states as needed: /// /// - Play ad-hoc notes. /// - Modify the `State` and push the old version to the undo stack. /// - Modify the `PathsState`. -/// - Modify the `SynthState` and send commands through the `Conn`. -/// - Modify the `Exporter` and send a copy via a command to the `Conn`. +/// - Modify the `Conn`. pub struct IO { /// A stack of snapshots that can be popped to undo an action. undo: Vec, @@ -91,8 +86,6 @@ pub struct IO { quit_panel: QuitPanel, /// The links panel. links_panel: LinksPanel, - /// Queued commands that will be used to export audio to multiple files. - export_queue: Vec, /// The active panels prior to exporting audio. pre_export_panels: Vec, /// The index of the focused panel prior to exporting audio. @@ -192,7 +185,6 @@ impl IO { links_panel, redo: vec![], undo: vec![], - export_queue: vec![], pre_export_panels: vec![], pre_export_focus: 0, } @@ -206,10 +198,8 @@ impl IO { /// - `tts` Text-to-speech. /// - `text` The text. /// - `paths_state` Dynamic path data. - /// - `exporter` Export settings. /// /// Returns: An `Snapshot`. - #[allow(clippy::too_many_arguments)] pub fn update( &mut self, state: &mut State, @@ -218,7 +208,6 @@ impl IO { tts: &mut TTS, text: &mut Text, paths_state: &mut PathsState, - exporter: &mut SharedExporter, ) -> bool { if input.happened(&InputEvent::Quit) { // Enable the quit panel. @@ -231,21 +220,8 @@ impl IO { } } - // Export multiple files. - if conn.export_state.is_none() && !self.export_queue.is_empty() { - // Enable the panel. - self.export_panel - .enable(state, &self.pre_export_panels, self.pre_export_focus); - // Get the commands and state. - let export_commands = self.export_queue.remove(0); - // Set the state. - conn.export_state = export_commands.1; - // Send the commands. - conn.send(export_commands.0); - } - // Don't do anything while exporting. - if conn.export_state.is_some() { + if conn.exporting() { return false; } @@ -255,24 +231,24 @@ impl IO { let panel = self.get_panel(&state.panels[state.focus.get()]); // Toggle off alphanumeric input. - if panel.allow_alphanumeric_input(state, exporter) { + if panel.allow_alphanumeric_input(state, conn) { if input.happened(&InputEvent::ToggleAlphanumericInput) { let s0 = state.clone(); state.input.alphanumeric_input = false; // Do something on disable. - panel.on_disable_abc123(state, exporter); + panel.on_disable_abc123(state, conn); // There is always a snapshot (because we toggled off alphanumeric input). let snapshot = Some(Snapshot::from_states(s0, state)); // Apply the snapshot. - self.apply_snapshot(snapshot, state, conn, paths_state, exporter); + self.apply_snapshot(snapshot, state, conn, paths_state); return false; } // Try to do alphanumeric input. else { - let (snapshot, updated) = panel.update_abc123(state, input, exporter); + let (snapshot, updated) = panel.update_abc123(state, input, conn); // We applied alphanumeric input. if updated { - self.apply_snapshot(snapshot, state, conn, paths_state, exporter); + self.apply_snapshot(snapshot, state, conn, paths_state); return false; } } @@ -281,7 +257,7 @@ impl IO { // Apply alphanumeric input. else { let panel = self.get_panel(&state.panels[state.focus.get()]); - if panel.allow_alphanumeric_input(state, exporter) + if panel.allow_alphanumeric_input(state, conn) && input.happened(&InputEvent::ToggleAlphanumericInput) { let snapshot = Some(Snapshot::from_state_value( @@ -289,40 +265,18 @@ impl IO { true, state, )); - self.apply_snapshot(snapshot, state, conn, paths_state, exporter); + self.apply_snapshot(snapshot, state, conn, paths_state); return false; } else if let Some(track) = state.music.get_selected_track() { - let mut commands = vec![]; // Play notes. if !&input.note_on_messages.is_empty() && panel.allow_play_music() && conn.state.programs.get(&track.channel).is_some() { - let gain = track.gain as f64 / 127.0; - // Set the framerate for playback. - commands.push(Command::SetFramerate { - framerate: conn.framerate as u32, - }); - // Play the notes. - for note in input.note_on_messages.iter() { - // Set the volume. - let volume = (note[2] as f64 * gain) as u8; - commands.push(Command::NoteOn { - channel: track.channel, - key: note[1], - velocity: volume, - }); - } - } - // Note-offs. - for note_off in input.note_off_keys.iter() { - commands.push(Command::NoteOff { - channel: track.channel, - key: *note_off, - }); + conn.note_ons(state, &input.note_on_messages); } - if !commands.is_empty() { - conn.send(commands); + if !&input.note_off_keys.is_empty() { + conn.note_offs(state, &input.note_off_keys) } } } @@ -340,13 +294,7 @@ impl IO { match &paths_state.saves.try_get_path() { // Save to the existing path, Some(path) => { - Save::write( - &path.with_extension("cac"), - state, - conn, - paths_state, - exporter, - ); + Save::write(&path.with_extension("cac"), state, conn, paths_state); state.unsaved_changes = false; } // Set a new path. @@ -359,9 +307,12 @@ impl IO { } // Export. else if input.happened(&InputEvent::ExportFile) { + let export_state = *conn.export_state.lock(); // We aren't exporting already. - if conn.export_state.is_none() { - self.open_file_panel.export(state, paths_state, exporter) + if export_state == ExportState::NotExporting { + self.pre_export_focus = state.focus.get(); + self.pre_export_panels = state.panels.clone(); + self.open_file_panel.export(state, paths_state, conn) } } else if input.happened(&InputEvent::ImportMidi) { self.open_file_panel.import_midi(state, paths_state); @@ -387,7 +338,7 @@ impl IO { } // Send the commands. if let Some(commands) = undo.from_commands { - conn.send(commands); + conn.do_commands(&commands); } // Push to the redo stack. self.redo.push(redo); @@ -403,7 +354,7 @@ impl IO { } // Send the commands. if let Some(commands) = redo.from_commands { - conn.send(commands); + conn.do_commands(&commands); } // Push to the undo stack. self.undo.push(undo); @@ -445,9 +396,8 @@ impl IO { // Get the focused panel. let panel = self.get_panel(&state.panels[state.focus.get()]); // Update the focuses panel and potentially get a screenshot. - let snapshot = panel.update(state, conn, input, tts, text, paths_state, exporter); - let (applied, need_to_quit) = - self.apply_snapshot(snapshot, state, conn, paths_state, exporter); + let snapshot = panel.update(state, conn, input, tts, text, paths_state); + let (applied, need_to_quit) = self.apply_snapshot(snapshot, state, conn, paths_state); // Quit while we're ahead. if need_to_quit { return true; @@ -460,15 +410,7 @@ impl IO { let panel = self.get_panel(&state.panels[state.focus.get()]); // Play music. if panel.allow_play_music() && input.happened(&InputEvent::PlayStop) { - match conn.state.time.music { - // Stop playing. - true => conn.send(vec![Command::StopMusic]), - false => { - conn.send( - combine_tracks_to_commands(state, conn.framerate, state.time.playback).0, - ); - } - } + conn.set_music(state); } // We're not done yet. false @@ -481,9 +423,8 @@ impl IO { state: &mut State, conn: &mut Conn, paths_state: &mut PathsState, - exporter: &mut SharedExporter, ) { - Save::read(save_path, state, conn, paths_state, exporter); + Save::read(save_path, state, conn, paths_state); // Set the saves directory. paths_state.saves = FileAndDirectory::new_path(save_path.to_path_buf()); } @@ -513,7 +454,6 @@ impl IO { state: &mut State, conn: &mut Conn, paths_state: &mut PathsState, - exporter: &mut SharedExporter, ) -> (bool, bool) { // Push an undo state generated by the focused panel. if let Some(snapshot) = snapshot { @@ -538,8 +478,13 @@ impl IO { } }, // Export. - IOCommand::Export(path) => { - self.export(path, state, conn, &mut exporter.lock()) + IOCommand::Export => { + self.export_panel.enable( + state, + &self.pre_export_panels, + self.pre_export_focus, + ); + conn.start_export(state, paths_state); } // Close the open-file panel. IOCommand::CloseOpenFile => self.open_file_panel.disable(state), @@ -568,202 +513,6 @@ impl IO { self.undo.remove(0); } } - - /// Begin to export audio. - /// - /// - `path` The output path. - /// - `state` The state. - /// - `conn` The audio conn. - /// - `exporter` The exporter. - fn export(&mut self, path: &Path, state: &mut State, conn: &mut Conn, exporter: &mut Exporter) { - self.pre_export_panels = state.panels.clone(); - self.pre_export_focus = state.focus.get(); - // Enable the export panel. - self.export_panel - .enable(state, &self.pre_export_panels, self.pre_export_focus); - // Export multiple files. - if exporter.multi_file { - self.queue_multi_file_export(path, state, conn, exporter); - } else { - // Get commands and an end time. - let (track_commands, t1) = - combine_tracks_to_commands(state, exporter.framerate.get_f(), 0); - // Define the export state. - let export_state: ExportState = ExportState::new(t1); - conn.export_state = Some(export_state); - // Set the framerate. - // Sound-off. Set the framerate. Export. - let mut commands = vec![ - Command::SoundOff, - Command::Export { - path: path.to_path_buf(), - state: export_state, - }, - ]; - commands.extend(track_commands); - // Send the commands. - conn.send(commands); - } - } - - /// Enqueue multi-file export commands. - /// - /// - `path` The root path, without tracks-specific suffixes. - /// - `state` The state. - /// - `conn` The audio connection. - /// - `exporter` The exporter. - fn queue_multi_file_export( - &mut self, - path: &Path, - state: &State, - conn: &Conn, - exporter: &mut Exporter, - ) { - self.export_queue.clear(); - let e0 = exporter.clone(); - // Get base path information. - let extension = path.extension().unwrap().to_str().unwrap(); - let filename_base = path.file_stem().unwrap().to_str().unwrap(); - let directory = path.parent().unwrap(); - // Get the framerate. - let framerate_f = exporter.framerate.get_f(); - let framerate_u = exporter.framerate.get_u() as u32; - // Start playing music. - let t0 = state.time.ppq_to_samples(0, framerate_f); - let mut paths = vec![]; - // Get playable tracks. - for track in get_playable_tracks(&state.music) { - let mut t1 = t0; - // Export to wav. - exporter.export_type.index.set(0); - // Start to play music. - let mut commands = vec![ - Command::SetFramerate { - framerate: framerate_u, - }, - Command::PlayMusic { time: t0 }, - ]; - let notes = get_playback_notes(track); - for note in notes.iter() { - // Convert the start and duration to sample lengths. - let start = state.time.ppq_to_samples(note.start, framerate_f); - if start < t0 { - continue; - } - let end = state.time.ppq_to_samples(note.end, framerate_f); - if end > t1 { - t1 = end; - } - // Add the command. - commands.push(Command::NoteOnAt { - channel: track.channel, - key: note.note, - velocity: note.velocity, - start, - end, - }) - } - // Get the path for this track. - let suffix = match exporter.multi_file_suffix.get() { - MultiFileSuffix::Channel => track.channel.to_string(), - MultiFileSuffix::Preset => conn - .state - .programs - .get(&track.channel) - .unwrap() - .preset_name - .clone(), - MultiFileSuffix::ChannelAndPreset => format!( - "{}_{}", - track.channel, - conn.state.programs.get(&track.channel).unwrap().preset_name - ), - }; - // Get the path. - let track_path = directory.join(format!("{}_{}.{}", filename_base, suffix, extension)); - paths.push(track_path.clone()); - // Get the export state. - let export_state = ExportState::new(t1); - // Export. - commands.extend([ - Command::SoundOff, - Command::Export { - path: track_path, - state: export_state, - }, - ]); - self.export_queue.push((commands, Some(export_state))); - } - *exporter = e0; - } -} - -/// Returns all tracks that can be played. -fn get_playable_tracks(music: &Music) -> Vec<&MidiTrack> { - // Get all tracks that can play music. - let tracks = match music.midi_tracks.iter().find(|t| t.solo) { - // Only include the solo track. - Some(solo) => vec![solo], - // Only include unmuted tracks. - None => music.midi_tracks.iter().filter(|t| !t.mute).collect(), - }; - tracks -} - -/// Returns all notes in the track that can be played (they are after t0). -fn get_playback_notes(track: &MidiTrack) -> Vec { - let gain = track.gain as f64 / MAX_VOLUME as f64; - let mut notes = vec![]; - for note in track.notes.iter() { - let mut n1 = *note; - n1.velocity = (n1.velocity as f64 * gain) as u8; - notes.push(n1); - } - notes.sort(); - notes -} - -/// Converts all playable tracks to note-on commands. -fn combine_tracks_to_commands( - state: &State, - framerate: f32, - start_time: u64, -) -> (CommandsMessage, u64) { - // Start playing music. - let t0 = state.time.ppq_to_samples(start_time, framerate); - let mut t1 = t0; - let mut commands = vec![ - Command::PlayMusic { time: t0 }, - Command::SetFramerate { - framerate: framerate as u32, - }, - ]; - // Get playable tracks. - for track in get_playable_tracks(&state.music) { - let notes = get_playback_notes(track); - for note in notes.iter() { - // Convert the start and duration to sample lengths. - let start = state.time.ppq_to_samples(note.start, framerate); - if start < t0 { - continue; - } - let end = state.time.ppq_to_samples(note.end, framerate); - if end > t1 { - t1 = end; - } - // Add the command. - commands.push(Command::NoteOnAt { - channel: track.channel, - key: note.note, - velocity: note.velocity, - start, - end, - }) - } - } - // All-off. - commands.push(Command::StopMusicAt { time: t1 }); - (commands, t1) } /// Try to select a track, given user input. diff --git a/io/src/links_panel.rs b/io/src/links_panel.rs index 6ad77de4..f295c88a 100644 --- a/io/src/links_panel.rs +++ b/io/src/links_panel.rs @@ -53,7 +53,6 @@ impl Panel for LinksPanel { tts: &mut TTS, text: &Text, _: &mut PathsState, - _: &mut SharedExporter, ) -> Option { if input.happened(&InputEvent::InputTTS) { tts.enqueue(TtsString::from(text.get_ref("LINKS_PANEL_INPUT_TTS_0"))); @@ -96,7 +95,7 @@ impl Panel for LinksPanel { None } - fn allow_alphanumeric_input(&self, _: &State, _: &SharedExporter) -> bool { + fn allow_alphanumeric_input(&self, _: &State, _: &Conn) -> bool { false } @@ -104,13 +103,13 @@ impl Panel for LinksPanel { false } - fn on_disable_abc123(&mut self, _: &mut State, _: &mut SharedExporter) {} + fn on_disable_abc123(&mut self, _: &mut State, _: &mut Conn) {} fn update_abc123( &mut self, _: &mut State, _: &Input, - _: &mut SharedExporter, + _: &mut Conn, ) -> (Option, bool) { (None, false) } diff --git a/io/src/music_panel.rs b/io/src/music_panel.rs index 9acd30b3..08a43707 100644 --- a/io/src/music_panel.rs +++ b/io/src/music_panel.rs @@ -1,6 +1,4 @@ -use crate::abc123::{ - on_disable_shared_exporter, on_disable_state, update_shared_exporter, update_state, -}; +use crate::abc123::{on_disable_exporter, on_disable_state, update_exporter, update_state}; use crate::panel::*; use common::music_panel_field::*; use common::{U64orF32, DEFAULT_BPM, MAX_VOLUME}; @@ -37,7 +35,6 @@ impl Panel for MusicPanel { tts: &mut TTS, text: &Text, _: &mut PathsState, - exporter: &mut SharedExporter, ) -> Option { // Cycle fields. if input.happened(&InputEvent::NextMusicPanelField) { @@ -53,11 +50,10 @@ impl Panel for MusicPanel { } // Panel TTS. else if input.happened(&InputEvent::StatusTTS) { - let ex = exporter.lock(); tts.enqueue(text.get_with_values( "MUSIC_PANEL_STATUS_TTS", &[ - &ex.metadata.title, + &conn.exporter.metadata.title, &state.time.bpm.to_string(), &conn.state.gain.to_string(), ], @@ -150,7 +146,7 @@ impl Panel for MusicPanel { &mut self, state: &mut State, input: &Input, - exporter: &mut SharedExporter, + conn: &mut Conn, ) -> (Option, bool) { match state.music_panel_field.get_ref() { MusicPanelField::BPM => { @@ -161,26 +157,26 @@ impl Panel for MusicPanel { MusicPanelField::Gain => (None, false), MusicPanelField::Name => ( None, - update_shared_exporter(|e| &mut e.metadata.title, input, exporter), + update_exporter(|e| &mut e.metadata.title, input, &mut conn.exporter), ), } } - fn on_disable_abc123(&mut self, state: &mut State, exporter: &mut SharedExporter) { + fn on_disable_abc123(&mut self, state: &mut State, conn: &mut Conn) { match state.music_panel_field.get_ref() { MusicPanelField::BPM => { on_disable_state(|s| &mut s.time.bpm, state, U64orF32::from(DEFAULT_BPM)) } MusicPanelField::Gain => (), - MusicPanelField::Name => on_disable_shared_exporter( + MusicPanelField::Name => on_disable_exporter( |e| &mut e.metadata.title, - exporter, + &mut conn.exporter, "My Music".to_string(), ), } } - fn allow_alphanumeric_input(&self, state: &State, _: &SharedExporter) -> bool { + fn allow_alphanumeric_input(&self, state: &State, _: &Conn) -> bool { match state.music_panel_field.get_ref() { MusicPanelField::BPM => true, MusicPanelField::Gain => false, diff --git a/io/src/open_file_panel.rs b/io/src/open_file_panel.rs index 7085e4ca..402db72e 100644 --- a/io/src/open_file_panel.rs +++ b/io/src/open_file_panel.rs @@ -1,7 +1,8 @@ use super::import_midi::import; use crate::panel::*; use crate::Save; -use audio::exporter::ExportType; +use audio::export::ExportType; +use audio::exporter::Exporter; use common::open_file::*; use common::PanelType; use text::get_file_name_no_ex; @@ -58,14 +59,8 @@ impl OpenFilePanel { } /// Enable a panel for setting the export path. - pub fn export( - &mut self, - state: &mut State, - paths_state: &mut PathsState, - exporter: &SharedExporter, - ) { - let ex = exporter.lock(); - let extension = ex.export_type.get().into(); + pub fn export(&mut self, state: &mut State, paths_state: &mut PathsState, conn: &Conn) { + let extension = conn.exporter.export_type.get().into(); let open_file_type = OpenFileType::Export; paths_state .children @@ -81,12 +76,9 @@ impl OpenFilePanel { self.enable(OpenFileType::ImportMidi, state, paths_state); } - fn get_extension(&self, paths_state: &PathsState, exporter: &SharedExporter) -> Extension { + fn get_extension(&self, paths_state: &PathsState, exporter: &Exporter) -> Extension { match paths_state.open_file_type { - OpenFileType::Export => { - let ex = exporter.lock(); - ex.export_type.get().into() - } + OpenFileType::Export => exporter.export_type.get().into(), OpenFileType::ReadSave | OpenFileType::WriteSave => Extension::Cac, OpenFileType::SoundFont => Extension::Sf2, OpenFileType::ImportMidi => Extension::Mid, @@ -120,7 +112,6 @@ impl Panel for OpenFilePanel { tts: &mut TTS, text: &Text, paths_state: &mut PathsState, - exporter: &mut SharedExporter, ) -> Option { match &paths_state.open_file_type { OpenFileType::SoundFont | OpenFileType::ReadSave => (), @@ -147,8 +138,7 @@ impl Panel for OpenFilePanel { s.push(' '); // Export file type. if paths_state.open_file_type == OpenFileType::Export { - let ex = exporter.lock(); - let e = ex.export_type.get(); + let e = conn.exporter.export_type.get(); let extension: Extension = e.into(); let export_type = extension.to_str(false); s.push_str( @@ -197,8 +187,7 @@ impl Panel for OpenFilePanel { } // Set export type. if paths_state.open_file_type == OpenFileType::Export { - let ex = exporter.lock(); - let mut index = ex.export_type; + let mut index = conn.exporter.export_type; index.index.increment(true); let e = index.get(); let extension: Extension = e.into(); @@ -254,11 +243,11 @@ impl Panel for OpenFilePanel { } // Go up a directory. else if input.happened(&InputEvent::UpDirectory) { - paths_state.up_directory(&self.get_extension(paths_state, exporter)); + paths_state.up_directory(&self.get_extension(paths_state, &conn.exporter)); } // Go down a directory. else if input.happened(&InputEvent::DownDirectory) { - paths_state.down_directory(&self.get_extension(paths_state, exporter)); + paths_state.down_directory(&self.get_extension(paths_state, &conn.exporter)); } // Scroll up. else if input.happened(&InputEvent::PreviousPath) { @@ -273,12 +262,11 @@ impl Panel for OpenFilePanel { && input.happened(&InputEvent::CycleExportType) { // Set the extension. - let mut ex = exporter.lock(); - ex.export_type.index.increment(true); + conn.exporter.export_type.index.increment(true); // Set the children. paths_state.children.set( &paths_state.exports.directory.path, - &ex.export_type.get().into(), + &conn.exporter.export_type.get().into(), None, ); } @@ -294,7 +282,7 @@ impl Panel for OpenFilePanel { // Get the path. let path = paths_state.children.children[selected].path.clone(); // Read the save file. - Save::read(&path, state, conn, paths_state, exporter); + Save::read(&path, state, conn, paths_state); // Set the saves directory. paths_state.saves = FileAndDirectory::new_path(path); } @@ -334,7 +322,6 @@ impl Panel for OpenFilePanel { state, conn, paths_state, - exporter, ); } } @@ -346,14 +333,13 @@ impl Panel for OpenFilePanel { self.disable(state); // Append the extension. let mut filename = filename.clone(); - let ex = exporter.lock(); filename.push_str( - >::into(ex.export_type.get()) + >::into(conn.exporter.export_type.get()) .to_str(true), ); // Export to a .mid file. - if ex.export_type.get() == ExportType::Mid { - ex.mid( + if conn.exporter.export_type.get() == ExportType::Mid { + conn.exporter.mid( &paths_state.exports.directory.path.join(filename), &state.music, &state.time, @@ -362,16 +348,14 @@ impl Panel for OpenFilePanel { } // Export an audio file. else { - return Some(Snapshot::from_io_commands(vec![IOCommand::Export( - paths_state.exports.directory.path.join(filename), - )])); + return Some(Snapshot::from_io_commands(vec![IOCommand::Export])); } } } OpenFileType::ImportMidi => { if let Some(selected) = paths_state.children.selected { let path = paths_state.children.children[selected].path.clone(); - import(&path, state, conn, exporter); + import(&path, state, conn); state.unsaved_changes = true; self.disable(state); } @@ -385,19 +369,19 @@ impl Panel for OpenFilePanel { None } - fn on_disable_abc123(&mut self, _: &mut State, _: &mut SharedExporter) {} + fn on_disable_abc123(&mut self, _: &mut State, _: &mut Conn) {} fn update_abc123( &mut self, _: &mut State, _: &Input, - _: &mut SharedExporter, + _: &mut Conn, ) -> (Option, bool) { // There is alphanumeric input in this struct, obviously, but we won't handle it here because we don't need to toggle it on/off. (None, false) } - fn allow_alphanumeric_input(&self, _: &State, _: &SharedExporter) -> bool { + fn allow_alphanumeric_input(&self, _: &State, _: &Conn) -> bool { false } diff --git a/io/src/panel.rs b/io/src/panel.rs index 8b25263d..57c2ae67 100644 --- a/io/src/panel.rs +++ b/io/src/panel.rs @@ -1,7 +1,7 @@ pub(crate) use crate::io_command::IOCommand; pub(crate) use crate::popup::Popup; pub(crate) use crate::Snapshot; -pub(crate) use audio::{Command, Conn, SharedExporter}; +pub(crate) use audio::{Command, Conn}; pub(crate) use common::{Index, PathsState, State}; pub(crate) use input::{Input, InputEvent}; pub(crate) use text::{Enqueable, Text, Tooltips, TtsString, TTS}; @@ -16,10 +16,8 @@ pub(crate) trait Panel { /// - `tts` Text-to-speech. /// - `text` The text. /// - `paths_state` Dynamic path data. - /// - `exporter` Export settings. /// /// Returns: An `Snapshot`. - #[allow(clippy::too_many_arguments)] fn update( &mut self, state: &mut State, @@ -28,34 +26,33 @@ pub(crate) trait Panel { tts: &mut TTS, text: &Text, paths_state: &mut PathsState, - exporter: &mut SharedExporter, ) -> Option; /// Apply panel-specific updates to the state if alphanumeric input is enabled. /// /// - `state` The state of the app. /// - `input` Input events, key presses, etc. - /// - `exporter` Export settings. + /// - `conn` The audio connection. /// /// Returns: An `Snapshot` and true if something (potentially not included in the snaphot) updated. fn update_abc123( &mut self, state: &mut State, input: &Input, - exporter: &mut SharedExporter, + conn: &mut Conn, ) -> (Option, bool); /// Do something when alphanumeric input is disabled. /// /// - `state` The state of the app. - /// - `exporter` Export settings. - fn on_disable_abc123(&mut self, state: &mut State, exporter: &mut SharedExporter); + /// - `conn` The audio connection. + fn on_disable_abc123(&mut self, state: &mut State, conn: &mut Conn); /// If true, allow the user to toggle alphanumeric input. /// /// - `state` The state. - /// - `exporter` Export settings. - fn allow_alphanumeric_input(&self, state: &State, exporter: &SharedExporter) -> bool; + /// - `conn` The audio connection. + fn allow_alphanumeric_input(&self, state: &State, conn: &Conn) -> bool; /// Returns true if we can play music. fn allow_play_music(&self) -> bool; diff --git a/io/src/piano_roll/edit.rs b/io/src/piano_roll/edit.rs index 9925c4bd..8c6fa211 100644 --- a/io/src/piano_roll/edit.rs +++ b/io/src/piano_roll/edit.rs @@ -31,7 +31,6 @@ impl Panel for Edit { _: &mut TTS, _: &Text, _: &mut PathsState, - _: &mut SharedExporter, ) -> Option { // Do nothing if there is no track. if state.music.selected.is_none() { @@ -138,18 +137,18 @@ impl Panel for Edit { } } - fn on_disable_abc123(&mut self, _: &mut State, _: &mut SharedExporter) {} + fn on_disable_abc123(&mut self, _: &mut State, _: &mut Conn) {} fn update_abc123( &mut self, _: &mut State, _: &Input, - _: &mut SharedExporter, + _: &mut Conn, ) -> (Option, bool) { (None, false) } - fn allow_alphanumeric_input(&self, _: &State, _: &SharedExporter) -> bool { + fn allow_alphanumeric_input(&self, _: &State, _: &Conn) -> bool { false } diff --git a/io/src/piano_roll/piano_roll_panel.rs b/io/src/piano_roll/piano_roll_panel.rs index e2d5edb4..d9b65b7c 100644 --- a/io/src/piano_roll/piano_roll_panel.rs +++ b/io/src/piano_roll/piano_roll_panel.rs @@ -141,7 +141,6 @@ impl Panel for PianoRollPanel { tts: &mut TTS, text: &Text, paths_state: &mut PathsState, - exporter: &mut SharedExporter, ) -> Option { // Select a track. if !state.view.single_track { @@ -483,38 +482,29 @@ impl Panel for PianoRollPanel { // Sub-panel actions. let mode = state.piano_roll_mode; match mode { - PianoRollMode::Edit => { - self.edit - .update(state, conn, input, tts, text, paths_state, exporter) - } + PianoRollMode::Edit => self.edit.update(state, conn, input, tts, text, paths_state), PianoRollMode::Select => { self.select - .update(state, conn, input, tts, text, paths_state, exporter) - } - PianoRollMode::Time => { - self.time - .update(state, conn, input, tts, text, paths_state, exporter) - } - PianoRollMode::View => { - self.view - .update(state, conn, input, tts, text, paths_state, exporter) + .update(state, conn, input, tts, text, paths_state) } + PianoRollMode::Time => self.time.update(state, conn, input, tts, text, paths_state), + PianoRollMode::View => self.view.update(state, conn, input, tts, text, paths_state), } } } - fn on_disable_abc123(&mut self, _: &mut State, _: &mut SharedExporter) {} + fn on_disable_abc123(&mut self, _: &mut State, _: &mut Conn) {} fn update_abc123( &mut self, _: &mut State, _: &Input, - _: &mut SharedExporter, + _: &mut Conn, ) -> (Option, bool) { (None, false) } - fn allow_alphanumeric_input(&self, _: &State, _: &SharedExporter) -> bool { + fn allow_alphanumeric_input(&self, _: &State, _: &Conn) -> bool { false } diff --git a/io/src/piano_roll/select.rs b/io/src/piano_roll/select.rs index 53f91733..16ae7a13 100644 --- a/io/src/piano_roll/select.rs +++ b/io/src/piano_roll/select.rs @@ -66,7 +66,6 @@ impl Panel for Select { _: &mut TTS, _: &Text, _: &mut PathsState, - _: &mut SharedExporter, ) -> Option { match state.music.get_selected_track() { None => None, @@ -316,18 +315,18 @@ impl Panel for Select { } } - fn on_disable_abc123(&mut self, _: &mut State, _: &mut SharedExporter) {} + fn on_disable_abc123(&mut self, _: &mut State, _: &mut Conn) {} fn update_abc123( &mut self, _: &mut State, _: &Input, - _: &mut SharedExporter, + _: &mut Conn, ) -> (Option, bool) { (None, false) } - fn allow_alphanumeric_input(&self, _: &State, _: &SharedExporter) -> bool { + fn allow_alphanumeric_input(&self, _: &State, _: &Conn) -> bool { false } diff --git a/io/src/piano_roll/time.rs b/io/src/piano_roll/time.rs index a9602f32..28a5b7ca 100644 --- a/io/src/piano_roll/time.rs +++ b/io/src/piano_roll/time.rs @@ -69,7 +69,6 @@ impl Panel for Time { _: &mut TTS, _: &Text, _: &mut PathsState, - _: &mut SharedExporter, ) -> Option { // Do nothing if there is no track. if state.music.selected.is_none() { @@ -142,18 +141,18 @@ impl Panel for Time { } } - fn on_disable_abc123(&mut self, _: &mut State, _: &mut SharedExporter) {} + fn on_disable_abc123(&mut self, _: &mut State, _: &mut Conn) {} fn update_abc123( &mut self, _: &mut State, _: &Input, - _: &mut SharedExporter, + _: &mut Conn, ) -> (Option, bool) { (None, false) } - fn allow_alphanumeric_input(&self, _: &State, _: &SharedExporter) -> bool { + fn allow_alphanumeric_input(&self, _: &State, _: &Conn) -> bool { false } diff --git a/io/src/piano_roll/view.rs b/io/src/piano_roll/view.rs index 7a457462..3b406ffc 100644 --- a/io/src/piano_roll/view.rs +++ b/io/src/piano_roll/view.rs @@ -62,7 +62,6 @@ impl Panel for View { _: &mut TTS, _: &Text, _: &mut PathsState, - _: &mut SharedExporter, ) -> Option { // Do nothing if there is no track. if state.music.selected.is_none() { @@ -138,18 +137,18 @@ impl Panel for View { } } - fn on_disable_abc123(&mut self, _: &mut State, _: &mut SharedExporter) {} + fn on_disable_abc123(&mut self, _: &mut State, _: &mut Conn) {} fn update_abc123( &mut self, _: &mut State, _: &Input, - _: &mut SharedExporter, + _: &mut Conn, ) -> (Option, bool) { (None, false) } - fn allow_alphanumeric_input(&self, _: &State, _: &SharedExporter) -> bool { + fn allow_alphanumeric_input(&self, _: &State, _: &Conn) -> bool { false } diff --git a/io/src/quit_panel.rs b/io/src/quit_panel.rs index 413259f2..b3cc9206 100644 --- a/io/src/quit_panel.rs +++ b/io/src/quit_panel.rs @@ -23,7 +23,6 @@ impl Panel for QuitPanel { tts: &mut TTS, text: &Text, _: &mut PathsState, - _: &mut SharedExporter, ) -> Option { if input.happened(&InputEvent::QuitPanelYes) { Some(Snapshot::from_io_commands(vec![IOCommand::Quit])) @@ -42,7 +41,7 @@ impl Panel for QuitPanel { } } - fn allow_alphanumeric_input(&self, _: &State, _: &SharedExporter) -> bool { + fn allow_alphanumeric_input(&self, _: &State, _: &Conn) -> bool { false } @@ -50,13 +49,13 @@ impl Panel for QuitPanel { false } - fn on_disable_abc123(&mut self, _: &mut State, _: &mut SharedExporter) {} + fn on_disable_abc123(&mut self, _: &mut State, _: &mut Conn) {} fn update_abc123( &mut self, _: &mut State, _: &Input, - _: &mut SharedExporter, + _: &mut Conn, ) -> (Option, bool) { (None, false) } diff --git a/io/src/save.rs b/io/src/save.rs index 15bd13e5..614f81b0 100644 --- a/io/src/save.rs +++ b/io/src/save.rs @@ -1,5 +1,4 @@ use audio::exporter::Exporter; -use audio::SharedExporter; use audio::*; use common::{PathsState, State}; use regex::Regex; @@ -23,6 +22,7 @@ pub(crate) struct Save { paths_state: PathsState, /// The exporter state. exporter: Exporter, + /// The version string. #[serde(default = "default_version")] version: String, } @@ -34,20 +34,13 @@ impl Save { /// - `state` The app state. /// - `conn` The audio connection. Its `SynthState` will be serialized. /// - `paths_state` The paths state. - /// - `exporter` The exporter. - pub fn write( - path: &PathBuf, - state: &State, - conn: &Conn, - paths_state: &PathsState, - exporter: &SharedExporter, - ) { + pub fn write(path: &PathBuf, state: &State, conn: &Conn, paths_state: &PathsState) { // Convert the state to something that can be serialized. let save = Save { state: state.clone(), synth_state: conn.state.clone(), paths_state: paths_state.clone(), - exporter: exporter.lock().clone(), + exporter: conn.exporter.clone(), version: common::VERSION.to_string(), }; // Try to open the file. @@ -77,14 +70,7 @@ impl Save { /// - `state` The app state, which will be set to a deserialized version. /// - `conn` The audio connection. Its `SynthState` will be set via commands derived from a deserialized version. /// - `paths_state` The paths state, which will be set to a deserialized version. - /// - `exporter` The exporter. - pub fn read( - path: &Path, - state: &mut State, - conn: &mut Conn, - paths_state: &mut PathsState, - exporter: &mut SharedExporter, - ) { + pub fn read(path: &Path, state: &mut State, conn: &mut Conn, paths_state: &mut PathsState) { match File::open(path) { Ok(mut file) => { let mut string = String::new(); @@ -102,8 +88,7 @@ impl Save { *paths_state = s.paths_state; // Set the exporter. - let mut ex = exporter.lock(); - *ex = s.exporter; + conn.exporter = s.exporter; // Set the synthesizer. // Set the gain. @@ -139,7 +124,7 @@ impl Save { conn.state = s.synth_state; // Send the commands. - conn.send(commands); + conn.do_commands(&commands); } Err(error) => panic!("{} {}", READ_ERROR, error), } diff --git a/io/src/snapshot.rs b/io/src/snapshot.rs index 8e0d8736..d8fefc6a 100644 --- a/io/src/snapshot.rs +++ b/io/src/snapshot.rs @@ -71,7 +71,7 @@ impl Snapshot { to_commands: Some(to_commands.clone()), ..Default::default() }; - conn.send(to_commands); + conn.do_commands(&to_commands); snapshot } @@ -96,7 +96,7 @@ impl Snapshot { to_commands: Some(to_commands.clone()), io_commands: None, }; - conn.send(to_commands); + conn.do_commands(&to_commands); snapshot } diff --git a/io/src/tracks_panel.rs b/io/src/tracks_panel.rs index 06e9b1bd..aadcf417 100644 --- a/io/src/tracks_panel.rs +++ b/io/src/tracks_panel.rs @@ -94,7 +94,6 @@ impl Panel for TracksPanel { tts: &mut TTS, text: &Text, _: &mut PathsState, - _: &mut SharedExporter, ) -> Option { // Status TTS. if input.happened(&InputEvent::StatusTTS) { @@ -342,18 +341,18 @@ impl Panel for TracksPanel { } } - fn on_disable_abc123(&mut self, _: &mut State, _: &mut SharedExporter) {} + fn on_disable_abc123(&mut self, _: &mut State, _: &mut Conn) {} fn update_abc123( &mut self, _: &mut State, _: &Input, - _: &mut SharedExporter, + _: &mut Conn, ) -> (Option, bool) { (None, false) } - fn allow_alphanumeric_input(&self, _: &State, _: &SharedExporter) -> bool { + fn allow_alphanumeric_input(&self, _: &State, _: &Conn) -> bool { false } diff --git a/render/src/drawable.rs b/render/src/drawable.rs index fbb892d9..f8aed43b 100644 --- a/render/src/drawable.rs +++ b/render/src/drawable.rs @@ -1,5 +1,5 @@ pub(crate) use crate::Renderer; -pub(crate) use audio::{Conn, SharedExporter}; +pub(crate) use audio::Conn; pub(crate) use common::{PathsState, State}; pub(crate) use input::Input; pub(crate) use text::Text; @@ -13,7 +13,6 @@ pub(crate) trait Drawable { /// - `conn` The synthesizer-player connection. /// - `text` The text. /// - `paths_state` The file paths state. - /// - `exporter` The exporter state. fn update( &self, renderer: &Renderer, @@ -21,6 +20,5 @@ pub(crate) trait Drawable { conn: &Conn, text: &Text, paths_state: &PathsState, - exporter: &SharedExporter, ); } diff --git a/render/src/export_panel.rs b/render/src/export_panel.rs index 24ff1347..9aaa4bb2 100644 --- a/render/src/export_panel.rs +++ b/render/src/export_panel.rs @@ -1,5 +1,6 @@ use crate::panel::*; use crate::Popup; +use audio::export::ExportState; use macroquad::prelude::*; /// Are we done yet? @@ -8,6 +9,8 @@ pub(crate) struct ExportPanel { panel: Panel, /// The popup handler. pub popup: Popup, + decaying_label: Label, + writing_label: Label, } impl ExportPanel { @@ -19,40 +22,62 @@ impl ExportPanel { let x = window_grid_size[0] / 2 - w / 2; let panel = Panel::new(PanelType::ExportState, [x, y], [w, h], text); let popup = Popup::new(PanelType::ExportState); - Self { panel, popup } + let decaying = text.get("EXPORT_PANEL_APPENDING_DECAY"); + let decaying_label = Label::new( + [ + panel.rect.position[0] + panel.rect.size[0] / 2 + - decaying.chars().count() as u32 / 2, + panel.rect.position[1] + 1, + ], + decaying, + ); + let writing = text.get("EXPORT_PANEL_WRITING"); + let writing_label = Label::new( + [ + panel.rect.position[0] + panel.rect.size[0] / 2 + - writing.chars().count() as u32 / 2, + panel.rect.position[1] + 1, + ], + writing, + ); + Self { + panel, + popup, + decaying_label, + writing_label, + } } } impl Drawable for ExportPanel { - fn update( - &self, - renderer: &Renderer, - _: &State, - conn: &Conn, - _: &Text, - _: &PathsState, - _: &SharedExporter, - ) { - if conn.export_state.is_none() { - return; - } + fn update(&self, renderer: &Renderer, _: &State, conn: &Conn, _: &Text, _: &PathsState) { self.popup.update(renderer); self.panel.update(true, renderer); - // Get the string. - let export_state = conn.export_state.unwrap(); - let mut s = export_state.exported.to_string(); - s.push('/'); - s.push_str(&export_state.samples.to_string()); - - // Draw the string. - let w = s.chars().count() as u32; - let x = self.panel.rect.position[0] + self.panel.rect.size[0] / 2 - w / 2; - let y = self.panel.rect.position[1] + 1; - let label = Label { - position: [x, y], - text: s, - }; - renderer.text(&label, &ColorKey::FocusDefault); + let export_state = conn.export_state.lock(); + match *export_state { + ExportState::WritingWav { + total_samples, + exported_samples, + } => { + let samples = format!("{}/{}", exported_samples, total_samples); + // Draw the string. + let w = samples.chars().count() as u32; + let x = self.panel.rect.position[0] + self.panel.rect.size[0] / 2 - w / 2; + let y = self.panel.rect.position[1] + 1; + let label = Label { + position: [x, y], + text: samples, + }; + renderer.text(&label, &ColorKey::FocusDefault); + } + ExportState::AppendingDecay => { + renderer.text(&self.decaying_label, &ColorKey::FocusDefault); + } + ExportState::WritingToDisk => { + renderer.text(&self.writing_label, &ColorKey::FocusDefault); + } + _ => (), + } } } diff --git a/render/src/export_settings_panel.rs b/render/src/export_settings_panel.rs index 5f0699e6..0a3f245d 100644 --- a/render/src/export_settings_panel.rs +++ b/render/src/export_settings_panel.rs @@ -1,6 +1,7 @@ use crate::panel::*; use crate::Focus; -use audio::exporter::*; +use audio::export::{ExportSetting, ExportType, MultiFileSuffix}; +use audio::exporter::{Exporter, MP3_BIT_RATES}; use common::IndexedValues; use serde::de::DeserializeOwned; use serde::Serialize; @@ -311,26 +312,17 @@ impl ExportSettingsPanel { } impl Drawable for ExportSettingsPanel { - fn update( - &self, - renderer: &Renderer, - state: &State, - _: &Conn, - text: &Text, - _: &PathsState, - exporter: &SharedExporter, - ) { + fn update(&self, renderer: &Renderer, state: &State, conn: &Conn, text: &Text, _: &PathsState) { // Get the focus. let focus = state.panels[state.focus.get()] == PanelType::ExportSettings; - let ex = exporter.lock(); // Get the height of the panel. - let e = ex.export_type.get(); + let e = conn.exporter.export_type.get(); let h = match &e { - ExportType::Wav => ex.wav_settings.index.get_length() + 2, - ExportType::Mid => ex.mid_settings.index.get_length() + 1, - ExportType::MP3 => ex.mp3_settings.index.get_length() + 3, - ExportType::Ogg => ex.ogg_settings.index.get_length() + 3, - ExportType::Flac => ex.flac_settings.index.get_length() + 3, + ExportType::Wav => conn.exporter.wav_settings.index.get_length() + 2, + ExportType::Mid => conn.exporter.mid_settings.index.get_length() + 1, + ExportType::MP3 => conn.exporter.mp3_settings.index.get_length() + 3, + ExportType::Ogg => conn.exporter.ogg_settings.index.get_length() + 3, + ExportType::Flac => conn.exporter.flac_settings.index.get_length() + 3, } as u32 + 1; @@ -347,22 +339,47 @@ impl Drawable for ExportSettingsPanel { renderer.text(&self.title, &color); // Draw the fields. - match &ex.export_type.get() { - ExportType::Wav => { - self.update_settings(|e| &e.wav_settings, renderer, state, text, &ex, focus) - } - ExportType::Mid => { - self.update_settings(|e| &e.mid_settings, renderer, state, text, &ex, focus) - } - ExportType::MP3 => { - self.update_settings(|e| &e.mp3_settings, renderer, state, text, &ex, focus) - } - ExportType::Ogg => { - self.update_settings(|e| &e.ogg_settings, renderer, state, text, &ex, focus) - } - ExportType::Flac => { - self.update_settings(|e| &e.flac_settings, renderer, state, text, &ex, focus) - } + match &conn.exporter.export_type.get() { + ExportType::Wav => self.update_settings( + |e| &e.wav_settings, + renderer, + state, + text, + &conn.exporter, + focus, + ), + ExportType::Mid => self.update_settings( + |e| &e.mid_settings, + renderer, + state, + text, + &conn.exporter, + focus, + ), + ExportType::MP3 => self.update_settings( + |e| &e.mp3_settings, + renderer, + state, + text, + &conn.exporter, + focus, + ), + ExportType::Ogg => self.update_settings( + |e| &e.ogg_settings, + renderer, + state, + text, + &conn.exporter, + focus, + ), + ExportType::Flac => self.update_settings( + |e| &e.flac_settings, + renderer, + state, + text, + &conn.exporter, + focus, + ), } } } diff --git a/render/src/links_panel.rs b/render/src/links_panel.rs index 9674abbd..d1d80e73 100644 --- a/render/src/links_panel.rs +++ b/render/src/links_panel.rs @@ -99,15 +99,7 @@ impl LinksPanel { } impl Drawable for LinksPanel { - fn update( - &self, - renderer: &Renderer, - _: &State, - _: &Conn, - _: &Text, - _: &PathsState, - _: &SharedExporter, - ) { + fn update(&self, renderer: &Renderer, _: &State, _: &Conn, _: &Text, _: &PathsState) { self.popup.update(renderer); self.panel.update(true, renderer); self.labels diff --git a/render/src/main_menu.rs b/render/src/main_menu.rs index 5d9dbe62..70aff6f6 100644 --- a/render/src/main_menu.rs +++ b/render/src/main_menu.rs @@ -280,13 +280,12 @@ impl MainMenu { /// Get a sample, set lerp targets, and draw bars. pub fn late_update(&mut self, renderer: &Renderer, conn: &Conn) { // Set the power bar lerp targets from the sample. - if let Some(sample) = conn.sample { - if self.time % POWER_BAR_DELTA == 0 { - self.set_lerp_target(0, sample.0); - self.set_lerp_target(1, sample.1); - } - self.time += 1; + if self.time % POWER_BAR_DELTA == 0 { + let sample = *conn.sample.lock(); + self.set_lerp_target(0, sample.0); + self.set_lerp_target(1, sample.1); } + self.time += 1; // Draw each bar. self.draw_sample_power(0, renderer); self.draw_sample_power(1, renderer); @@ -294,15 +293,7 @@ impl MainMenu { } impl Drawable for MainMenu { - fn update( - &self, - renderer: &Renderer, - state: &State, - _: &Conn, - _: &Text, - _: &PathsState, - _: &SharedExporter, - ) { + fn update(&self, renderer: &Renderer, state: &State, _: &Conn, _: &Text, _: &PathsState) { self.panel.update_ex(&COLOR, renderer); if state.unsaved_changes { renderer.rectangle(&self.title_changes.rect, &ColorKey::Background); diff --git a/render/src/music_panel.rs b/render/src/music_panel.rs index 3387c8ad..a3fd169d 100644 --- a/render/src/music_panel.rs +++ b/render/src/music_panel.rs @@ -63,15 +63,7 @@ impl MusicPanel { } impl Drawable for MusicPanel { - fn update( - &self, - renderer: &Renderer, - state: &State, - conn: &Conn, - _: &Text, - _: &PathsState, - exporter: &SharedExporter, - ) { + fn update(&self, renderer: &Renderer, state: &State, conn: &Conn, _: &Text, _: &PathsState) { // Get the focus, let focus = self.panel.has_focus(state); // Draw the rect. @@ -92,9 +84,8 @@ impl Drawable for MusicPanel { } } // Draw the name. - let ex = exporter.lock(); renderer.text_ref( - &self.name.to_label(&ex.metadata.title), + &self.name.to_label(&conn.exporter.metadata.title), &Renderer::get_value_color([focus, name_focus]), ); diff --git a/render/src/open_file_panel.rs b/render/src/open_file_panel.rs index 640fc6af..1ade543a 100644 --- a/render/src/open_file_panel.rs +++ b/render/src/open_file_panel.rs @@ -141,10 +141,9 @@ impl Drawable for OpenFilePanel { &self, renderer: &Renderer, state: &State, - _: &Conn, + conn: &Conn, _: &Text, paths_state: &PathsState, - exporter: &SharedExporter, ) { let focus = self.panel.has_focus(state); self.popup.update(renderer); @@ -246,11 +245,7 @@ impl Drawable for OpenFilePanel { let ext = match paths_state.open_file_type { OpenFileType::ReadSave | OpenFileType::WriteSave => Extension::Cac, OpenFileType::SoundFont => Extension::Sf2, - OpenFileType::Export => { - let ex = exporter.lock(); - let e = ex.export_type.get(); - e.into() - } + OpenFileType::Export => conn.exporter.export_type.get().into(), OpenFileType::ImportMidi => Extension::Mid, }; extension.push_str(ext.to_str(true)); diff --git a/render/src/panel.rs b/render/src/panel.rs index ab70baa3..31aa0a4a 100644 --- a/render/src/panel.rs +++ b/render/src/panel.rs @@ -1,7 +1,6 @@ pub(crate) use crate::drawable::*; pub(crate) use crate::field_params::*; pub(crate) use crate::ColorKey; -pub(crate) use audio::SharedExporter; pub(crate) use common::sizes::*; pub(crate) use common::PanelType; use common::VERSION; diff --git a/render/src/panels.rs b/render/src/panels.rs index 2929c727..b17d0fe7 100644 --- a/render/src/panels.rs +++ b/render/src/panels.rs @@ -70,7 +70,6 @@ impl Panels { /// - `conn` The synthesizer-player connection. /// - `text` The text. /// - `paths_state` The state of the file paths. - /// - `exporter` The exporter. pub fn update( &self, renderer: &Renderer, @@ -78,11 +77,10 @@ impl Panels { conn: &Conn, text: &Text, paths_state: &PathsState, - exporter: &SharedExporter, ) { // Draw the main panel. self.main_menu - .update(renderer, state, conn, text, paths_state, exporter); + .update(renderer, state, conn, text, paths_state); for panel_type in &state.panels { // Get the panel. let panel: &dyn Drawable = match panel_type { @@ -97,7 +95,7 @@ impl Panels { PanelType::Links => &self.links_panel, }; // Draw the panel. - panel.update(renderer, state, conn, text, paths_state, exporter); + panel.update(renderer, state, conn, text, paths_state); } } diff --git a/render/src/piano_roll_panel.rs b/render/src/piano_roll_panel.rs index 33018ed6..4ad3bcd3 100644 --- a/render/src/piano_roll_panel.rs +++ b/render/src/piano_roll_panel.rs @@ -1,4 +1,6 @@ use crate::panel::*; +use audio::play_state::PlayState; +use audio::SharedPlayState; mod piano_roll_rows; use piano_roll_rows::PianoRollRows; mod multi_track; @@ -166,41 +168,34 @@ impl PianoRollPanel { /// Otherwise, this returns a view delta that has been moved to include the current playback time. fn get_view_dt(state: &State, conn: &Conn) -> [u64; 2] { let dt = [state.view.dt[0], state.view.dt[1]]; - if conn.state.time.music { - match conn.state.time.time { - Some(time) => { - let time_ppq = state.time.samples_to_ppq(time, conn.framerate); - // The time is in range - if time_ppq >= dt[0] && time_ppq <= dt[1] { - dt - } else { - let delta = dt[1] - dt[0]; - // This is maybe not the best way to round, but it gets the job done! - let t0 = (time_ppq / delta) * delta; - let t1 = t0 + delta; - [t0, t1] - } + let play_state = Self::get_play_state(&conn.play_state); + match play_state { + // We are playing music. + PlayState::Playing(samples) => { + let time_ppq = state.time.samples_to_ppq(samples, conn.framerate); + // The time is in range + if time_ppq >= dt[0] && time_ppq <= dt[1] { + dt + } else { + let delta = dt[1] - dt[0]; + // This is maybe not the best way to round, but it gets the job done! + let t0 = (time_ppq / delta) * delta; + let t1 = t0 + delta; + [t0, t1] } - None => dt, } - } - // If there is no music playing, just use the "actual" view. - else { - dt + // If there is no music playing, just use the "actual" view. + _ => dt, } } + + fn get_play_state(play_state: &SharedPlayState) -> PlayState { + *play_state.lock() + } } impl Drawable for PianoRollPanel { - fn update( - &self, - renderer: &Renderer, - state: &State, - conn: &Conn, - text: &Text, - _: &PathsState, - _: &SharedExporter, - ) { + fn update(&self, renderer: &Renderer, state: &State, conn: &Conn, text: &Text, _: &PathsState) { let panel = if state.view.single_track { &self.panel_single_track } else { @@ -381,18 +376,17 @@ impl Drawable for PianoRollPanel { position: [selection_x, self.time_y], }; // Current playback time. - if conn.state.time.music { - if let Some(music_time) = conn.state.time.time { - let music_time_string = - ppq_to_string(state.time.samples_to_ppq(music_time, conn.framerate)); - let music_time_x = - selection_x + selection_label.text.chars().count() as u32 + TIME_PADDING; - let music_time_label = Label { - text: music_time_string, - position: [music_time_x, self.time_y], - }; - renderer.text(&music_time_label, &Renderer::get_key_color(focus)); - } + let play_state = Self::get_play_state(&conn.play_state); + if let PlayState::Playing(samples) = play_state { + let music_time_string = + ppq_to_string(state.time.samples_to_ppq(samples, conn.framerate)); + let music_time_x = + selection_x + selection_label.text.chars().count() as u32 + TIME_PADDING; + let music_time_label = Label { + text: music_time_string, + position: [music_time_x, self.time_y], + }; + renderer.text(&music_time_label, &Renderer::get_key_color(focus)); } renderer.text( &selection_label, @@ -420,10 +414,7 @@ impl Drawable for PianoRollPanel { }; renderer.text(&dt_label, &Renderer::get_key_color(focus)); - if state.view.single_track { - } - // Multi-track. - else { + if !state.view.single_track { self.multi_track.update(dt, renderer, state, conn); } @@ -445,32 +436,30 @@ impl Drawable for PianoRollPanel { &dt, ); // Show where we are in the music. - if conn.state.time.music { - if let Some(music_time) = conn.state.time.time { - let music_time = state.time.samples_to_ppq(music_time, conn.framerate); - if music_time >= dt[0].get_u() && music_time <= dt[1].get_u() { - let x = ViewableNotes::get_note_x( - music_time, - ViewableNotes::get_pulses_per_pixel(&dt, self.piano_roll_rows_rect[2]), - self.piano_roll_rows_rect[0], - &dt, - ); - let music_color = if focus { - ColorKey::FocusDefault + if let PlayState::Playing(samples) = play_state { + let music_time = state.time.samples_to_ppq(samples, conn.framerate); + if music_time >= dt[0].get_u() && music_time <= dt[1].get_u() { + let x = ViewableNotes::get_note_x( + music_time, + ViewableNotes::get_pulses_per_pixel(&dt, self.piano_roll_rows_rect[2]), + self.piano_roll_rows_rect[0], + &dt, + ); + let music_color = if focus { + ColorKey::FocusDefault + } else { + ColorKey::NoFocus + }; + renderer.vertical_line_pixel( + x, + self.piano_roll_rows_rect[1], + if state.view.single_track { + self.time_line_bottoms[0] } else { - ColorKey::NoFocus - }; - renderer.vertical_line_pixel( - x, - self.piano_roll_rows_rect[1], - if state.view.single_track { - self.time_line_bottoms[0] - } else { - self.time_line_bottoms[1] - }, - &music_color, - ); - } + self.time_line_bottoms[1] + }, + &music_color, + ); } } } diff --git a/render/src/piano_roll_panel/viewable_notes.rs b/render/src/piano_roll_panel/viewable_notes.rs index d9741656..21d40bca 100644 --- a/render/src/piano_roll_panel/viewable_notes.rs +++ b/render/src/piano_roll_panel/viewable_notes.rs @@ -1,4 +1,5 @@ use crate::panel::*; +use audio::play_state::PlayState; use common::*; /// A viewable note. @@ -76,13 +77,9 @@ impl<'a> ViewableNotes<'a> { ) -> Self { let pulses_per_pixel = Self::get_pulses_per_pixel(&dt, w); // Get any notes being played. - let playtime = match conn.state.time.music { - true => conn - .state - .time - .time - .map(|time| state.time.samples_to_ppq(time, conn.framerate)), - false => None, + let playtime = match *conn.play_state.lock() { + PlayState::Playing(time) => Some(state.time.samples_to_ppq(time, conn.framerate)), + _ => None, }; // Get the selected notes. diff --git a/render/src/quit_panel.rs b/render/src/quit_panel.rs index 5ddd3bc1..6c3a8db4 100644 --- a/render/src/quit_panel.rs +++ b/render/src/quit_panel.rs @@ -48,15 +48,7 @@ impl QuitPanel { } impl Drawable for QuitPanel { - fn update( - &self, - renderer: &Renderer, - _: &State, - _: &Conn, - _: &Text, - _: &PathsState, - _: &SharedExporter, - ) { + fn update(&self, renderer: &Renderer, _: &State, _: &Conn, _: &Text, _: &PathsState) { self.popup.update(renderer); self.panel.update(true, renderer); renderer.text(&self.labels[0], &LABEL_COLOR); diff --git a/render/src/tracks_panel.rs b/render/src/tracks_panel.rs index 9fec8459..3da648f4 100644 --- a/render/src/tracks_panel.rs +++ b/render/src/tracks_panel.rs @@ -61,15 +61,7 @@ impl TracksPanel { } impl Drawable for TracksPanel { - fn update( - &self, - renderer: &Renderer, - state: &State, - conn: &Conn, - text: &Text, - _: &PathsState, - _: &SharedExporter, - ) { + fn update(&self, renderer: &Renderer, state: &State, conn: &Conn, text: &Text, _: &PathsState) { // Get the focus, let focus = self.panel.has_focus(state); // Draw the panel. diff --git a/src/main.rs b/src/main.rs index 1f9120ed..2e62e4a6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,6 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] -use audio::connect; -use audio::exporter::Exporter; +use audio::Conn; use clap::Parser; use common::args::Args; use common::config::{load, parse_bool}; @@ -63,11 +62,8 @@ async fn main() { // Get the input object. let mut input = Input::new(&config, &args); - // Create the exporter. - let mut exporter = Exporter::new_shared(); - // Create the audio connection. - let mut conn = connect(&exporter); + let mut conn = Conn::default(); // Create the state. let mut state = State::new(&config); @@ -110,13 +106,7 @@ async fn main() { // Open the initial save file if set. if let Some(save_path) = args.file { - io.load_save( - &save_path, - &mut state, - &mut conn, - &mut paths_state, - &mut exporter, - ); + io.load_save(&save_path, &mut state, &mut conn, &mut paths_state); } // Begin. @@ -126,14 +116,14 @@ async fn main() { clear_background(CLEAR_COLOR); // Draw. - panels.update(&renderer, &state, &conn, &text, &paths_state, &exporter); + panels.update(&renderer, &state, &conn, &text, &paths_state); // Draw subtitles. draw_subtitles(&renderer, &tts); // If we're exporting audio, don't allow input. - if conn.export_state.is_none() { - // Update the input state. + if !conn.exporting() { + // Update the user input state input.update(&state); // Modify the state. @@ -144,14 +134,10 @@ async fn main() { &mut tts, &mut text, &mut paths_state, - &mut exporter, ); } if !done { - // Update time itself. - conn.update(); - // Update the subtitles. tts.update();