From 1a6308fcea2969745aed7d6a3d71ee48e9c27190 Mon Sep 17 00:00:00 2001 From: Esther Alter Date: Mon, 13 Nov 2023 08:56:25 -0500 Subject: [PATCH 01/52] derive copy, clone, eq, partialeq for MidiEvent --- Cargo.lock | 4 ++-- Cargo.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5da4c0ea..81bcd05a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1561,7 +1561,7 @@ checksum = "8d91edf4fbb970279443471345a4e8c491bf05bb283b3e6c88e4e606fd8c181b" [[package]] name = "oxisynth" version = "0.0.3" -source = "git+https://github.com/PolyMeilex/OxiSynth.git?branch=master#3353ef0a7102ae192d58e6c6af7db6f562780ecb" +source = "git+https://github.com/subalterngames/OxiSynth.git?branch=midi_event_copy_clone#547d62343119c0bdea24ce8b65e63ae7162f1e6d" dependencies = [ "bitflags 1.3.2", "byte-slice-cast", @@ -1982,7 +1982,7 @@ checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" [[package]] name = "soundfont" version = "0.0.2" -source = "git+https://github.com/PolyMeilex/OxiSynth.git?branch=master#3353ef0a7102ae192d58e6c6af7db6f562780ecb" +source = "git+https://github.com/subalterngames/OxiSynth.git?branch=midi_event_copy_clone#547d62343119c0bdea24ce8b65e63ae7162f1e6d" dependencies = [ "riff", ] diff --git a/Cargo.toml b/Cargo.toml index 967dffca..b222f034 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,8 +68,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" From 6ca288a913d4a752e1dd60c382eec09cea1c992d Mon Sep 17 00:00:00 2001 From: Esther Alter Date: Mon, 13 Nov 2023 09:19:46 -0500 Subject: [PATCH 02/52] TimedMidiEvent and MidiEventQueue --- audio/src/export.rs | 0 audio/src/lib.rs | 3 +++ audio/src/midi_event_queue.rs | 31 +++++++++++++++++++++++++++++++ audio/src/timed_midi_event.rs | 10 ++++++++++ 4 files changed, 44 insertions(+) create mode 100644 audio/src/export.rs create mode 100644 audio/src/midi_event_queue.rs create mode 100644 audio/src/timed_midi_event.rs diff --git a/audio/src/export.rs b/audio/src/export.rs new file mode 100644 index 00000000..e69de29b diff --git a/audio/src/lib.rs b/audio/src/lib.rs index 0ccf5e6d..8c86e194 100644 --- a/audio/src/lib.rs +++ b/audio/src/lib.rs @@ -25,8 +25,11 @@ mod command; mod conn; +mod export; mod export_state; pub mod exporter; +pub(crate) mod midi_event_queue; +pub(crate) mod timed_midi_event; mod player; mod program; mod synth_state; diff --git a/audio/src/midi_event_queue.rs b/audio/src/midi_event_queue.rs new file mode 100644 index 00000000..7c80eacc --- /dev/null +++ b/audio/src/midi_event_queue.rs @@ -0,0 +1,31 @@ +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. This will also sort the internal list of events. + /// + /// - `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 }); + // Sort the events. + self.events.sort_by(|a, b| a.time.cmp(&b.time)) + } + + /// Dequeue any events that start at `time`. + pub(crate) fn dequeue(&mut self, time: u64) -> Vec { + let midi_events = self.events.iter().filter(|e| e.time == time).map(|e| e.event).collect::>(); + if !midi_events.is_empty() { + self.events.retain(|e| e.time != time); + } + midi_events + } +} \ No newline at end of file diff --git a/audio/src/timed_midi_event.rs b/audio/src/timed_midi_event.rs new file mode 100644 index 00000000..0a1df257 --- /dev/null +++ b/audio/src/timed_midi_event.rs @@ -0,0 +1,10 @@ +use oxisynth::MidiEvent; + +/// 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, +} \ No newline at end of file From f0d5372dd056998ce4f97ef6c412424614c54b07 Mon Sep 17 00:00:00 2001 From: Esther Alter Date: Mon, 13 Nov 2023 09:50:56 -0500 Subject: [PATCH 03/52] Player --- audio/src/export.rs | 1 + audio/src/lib.rs | 6 +- audio/src/midi_event_queue.rs | 17 ++++-- audio/src/player.rs | 102 +++++++++++++++++++++++----------- audio/src/timed_midi_event.rs | 2 +- audio/src/types.rs | 7 ++- 6 files changed, 95 insertions(+), 40 deletions(-) diff --git a/audio/src/export.rs b/audio/src/export.rs index e69de29b..8b137891 100644 --- a/audio/src/export.rs +++ b/audio/src/export.rs @@ -0,0 +1 @@ + diff --git a/audio/src/lib.rs b/audio/src/lib.rs index 8c86e194..36e52049 100644 --- a/audio/src/lib.rs +++ b/audio/src/lib.rs @@ -29,20 +29,20 @@ mod export; mod export_state; pub mod exporter; pub(crate) mod midi_event_queue; -pub(crate) mod timed_midi_event; 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}; +pub(crate) use crate::types::{AudioBuffer, SharedMidiEventQueue, SharedTimeState}; +pub use crate::types::{AudioMessage, CommandsMessage, SharedExporter, SharedSynth}; use crossbeam_channel::{bounded, unbounded}; pub use export_state::ExportState; use player::Player; diff --git a/audio/src/midi_event_queue.rs b/audio/src/midi_event_queue.rs index 7c80eacc..59758824 100644 --- a/audio/src/midi_event_queue.rs +++ b/audio/src/midi_event_queue.rs @@ -5,12 +5,12 @@ use oxisynth::MidiEvent; #[derive(Default)] pub(crate) struct MidiEventQueue { /// The events. Assume that this is sorted. - events: Vec + events: Vec, } impl MidiEventQueue { /// Enqueue a new MIDI event. This will also sort the internal list of events. - /// + /// /// - `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) { @@ -22,10 +22,19 @@ impl MidiEventQueue { /// Dequeue any events that start at `time`. pub(crate) fn dequeue(&mut self, time: u64) -> Vec { - let midi_events = self.events.iter().filter(|e| e.time == time).map(|e| e.event).collect::>(); + let midi_events = self + .events + .iter() + .filter(|e| e.time == time) + .map(|e| e.event) + .collect::>(); if !midi_events.is_empty() { self.events.retain(|e| e.time != time); } midi_events } -} \ No newline at end of file + + pub(crate) fn is_empty(&self) -> bool { + self.events.is_empty() + } +} diff --git a/audio/src/player.rs b/audio/src/player.rs index 6cf8ac0a..b504a881 100644 --- a/audio/src/player.rs +++ b/audio/src/player.rs @@ -1,7 +1,6 @@ -use crate::AudioMessage; +use crate::{SharedMidiEventQueue, SharedSynth, SharedTimeState}; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; use cpal::*; -use crossbeam_channel::Receiver; const ERROR_MESSAGE: &str = "Failed to create an audio output stream: "; @@ -17,7 +16,11 @@ pub(crate) struct Player { } impl Player { - pub(crate) fn new(recv: Receiver) -> Option { + pub(crate) fn new( + midi_event_queue: SharedMidiEventQueue, + time_state: SharedTimeState, + synth: SharedSynth, + ) -> Option { // Get the host. let host = default_host(); // Try to get an output device. @@ -40,17 +43,14 @@ impl Player { 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, + time_state, + synth, + ); Some(Self { _host: host, _stream: stream, @@ -62,34 +62,74 @@ 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, + time_state: SharedTimeState, + synth: SharedSynth, + ) -> 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 audio_buffers = [vec![0.0; 1], vec![0.0; 1]]; + // 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() { + let data_callback = move |output: &mut [f32], _: &OutputCallbackInfo| { + let mut time_state = time_state.lock(); + let mut midi_event_queue = midi_event_queue.lock(); + // There are no more events. Fill the buffer and advance time. + if midi_event_queue.is_empty() { + let mut synth = synth.lock(); + let len = output.len(); + + // Resize the buffers. + if len > audio_buffers[0].len() { + audio_buffers[0].resize(len, 0.0); + audio_buffers[1].resize(len, 0.0); + } + + // Write the samples. + synth.write((&mut audio_buffers[0][0..len], &mut audio_buffers[1][0..len])); + + // Advance time. + if let Some(time) = time_state.time { + time_state.time = Some(time + len as u64); + } + } else { + // Iterate through the number of samples. + for frame in output.chunks_mut(channels) { + // We're playing music. Advance to the next events. + if let Some(time) = time_state.time { + // Dequeue events. + let events = midi_event_queue.dequeue(time); + // Send the MIDI events to the synth. + if !events.is_empty() { + let mut synth = synth.lock(); + for event in events { + if synth.send_event(event).is_ok() {} + } + } + // Advance time by one sample. + time_state.time = Some(time + 1) + } + // Get the next sample. + let mut synth = synth.lock(); + // Get the sample. + let (left, right) = synth.read_next(); + + // Add the 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)]; + frame[0] = left; + frame[1] = right; + } + // Add for more than one channel. This is slower. + else { + let channels = [left, right]; for (id, sample) in frame.iter_mut().enumerate() { *sample = channels[id % 2]; } diff --git a/audio/src/timed_midi_event.rs b/audio/src/timed_midi_event.rs index 0a1df257..988c97e9 100644 --- a/audio/src/timed_midi_event.rs +++ b/audio/src/timed_midi_event.rs @@ -7,4 +7,4 @@ pub(crate) struct TimedMidiEvent { pub(crate) time: u64, /// The event. pub(crate) event: MidiEvent, -} \ No newline at end of file +} diff --git a/audio/src/types.rs b/audio/src/types.rs index 674e8d82..eb54a664 100644 --- a/audio/src/types.rs +++ b/audio/src/types.rs @@ -1,5 +1,7 @@ use crate::exporter::Exporter; -use crate::Command; +use crate::midi_event_queue::MidiEventQueue; +use crate::{Command, TimeState}; +use oxisynth::Synth; use parking_lot::Mutex; use std::sync::Arc; @@ -11,3 +13,6 @@ pub type CommandsMessage = Vec; pub(crate) type AudioBuffer = [Vec; 2]; /// The exporter. pub type SharedExporter = Arc>; +pub type SharedSynth = Arc>; +pub(crate) type SharedMidiEventQueue = Arc>; +pub(crate) type SharedTimeState = Arc>; From eb59c712dbe4bcf3d27444e701004cf5d69ceef0 Mon Sep 17 00:00:00 2001 From: Esther Alter Date: Mon, 13 Nov 2023 09:51:26 -0500 Subject: [PATCH 04/52] minor improvement --- audio/src/player.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/audio/src/player.rs b/audio/src/player.rs index b504a881..2253c17a 100644 --- a/audio/src/player.rs +++ b/audio/src/player.rs @@ -83,7 +83,6 @@ impl Player { let mut midi_event_queue = midi_event_queue.lock(); // There are no more events. Fill the buffer and advance time. if midi_event_queue.is_empty() { - let mut synth = synth.lock(); let len = output.len(); // Resize the buffers. @@ -93,6 +92,7 @@ impl Player { } // Write the samples. + let mut synth = synth.lock(); synth.write((&mut audio_buffers[0][0..len], &mut audio_buffers[1][0..len])); // Advance time. From 7a826f1c8b2b2cf62cab014fd803a8be30195e14 Mon Sep 17 00:00:00 2001 From: Esther Alter Date: Mon, 13 Nov 2023 09:51:36 -0500 Subject: [PATCH 05/52] mut --- audio/src/player.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/audio/src/player.rs b/audio/src/player.rs index 2253c17a..1bc8ff90 100644 --- a/audio/src/player.rs +++ b/audio/src/player.rs @@ -75,7 +75,7 @@ impl Player { let two_channels = channels == 2; - let audio_buffers = [vec![0.0; 1], vec![0.0; 1]]; + let mut audio_buffers = [vec![0.0; 1], vec![0.0; 1]]; // Define the data callback used by cpal. Move `stream_send` into the closure. let data_callback = move |output: &mut [f32], _: &OutputCallbackInfo| { From b9e96bba0358f3b2a41217eb926619149d3cdb2b Mon Sep 17 00:00:00 2001 From: Esther Alter Date: Mon, 13 Nov 2023 10:19:09 -0500 Subject: [PATCH 06/52] Conn --- audio/src/conn.rs | 76 ++++++++++++++++++++++++++++------- audio/src/lib.rs | 2 +- audio/src/midi_event_queue.rs | 7 +++- audio/src/player.rs | 13 +++++- audio/src/types.rs | 1 + common/src/midi_track.rs | 13 ++++++ common/src/music.rs | 12 ++++++ io/src/lib.rs | 23 ----------- 8 files changed, 105 insertions(+), 42 deletions(-) diff --git a/audio/src/conn.rs b/audio/src/conn.rs index bb3e1724..740aceec 100644 --- a/audio/src/conn.rs +++ b/audio/src/conn.rs @@ -1,5 +1,10 @@ -use crate::{AudioMessage, Command, CommandsMessage, ExportState, Player, SynthState, TimeState}; +use crate::{ + AudioMessage, Command, CommandsMessage, ExportState, Player, SharedMidiEventQueue, SharedSynth, + SharedTimeState, SynthState, TimeState, types::SharedSample, +}; +use common::State; use crossbeam_channel::{Receiver, Sender}; +use oxisynth::MidiEvent; /// The connects used by an external function. pub struct Conn { @@ -8,7 +13,7 @@ pub struct Conn { /// The current export state, if any. pub export_state: Option, /// The playback framerate. - pub framerate: f32, + framerate: f32, /// The audio player. This is here so we don't drop it. _player: Option, /// Send commands to the synthesizer. @@ -17,12 +22,13 @@ pub struct Conn { 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, + pub sample: SharedSample, + synth: SharedSynth, + midi_event_queue: SharedMidiEventQueue, + time_state: SharedTimeState, } impl Conn { @@ -33,6 +39,10 @@ impl Conn { recv_export: Receiver>, recv_time: Receiver, recv_sample: Receiver, + synth: SharedSynth, + midi_event_queue: SharedMidiEventQueue, + time_state: SharedTimeState, + sample: SharedSample ) -> Self { let framerate = match &player { Some(player) => player.framerate as f32, @@ -45,10 +55,12 @@ impl Conn { send_commands, recv, recv_export, - recv_time, recv_sample, framerate, - sample: None, + sample, + synth, + midi_event_queue, + time_state } } @@ -68,13 +80,6 @@ impl Conn { /// Call this once per frame. pub fn update(&mut self) { - if let Ok(time) = self.recv_time.try_recv() { - self.state.time = time; - } - 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]); @@ -83,4 +88,47 @@ impl Conn { } } } + + /// Schedule MIDI events and start to play music. + pub fn schedule_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); + + // Enqueue note events. + let mut midi_event_queue = self.midi_event_queue.lock(); + for track in state.music.midi_tracks { + for note in track.get_playback_notes(start) { + // 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(); + + // Set time itself. + let mut time_state = self.time_state.lock(); + time_state.music = true; + time_state.time = Some(start); + } } diff --git a/audio/src/lib.rs b/audio/src/lib.rs index 36e52049..2223521a 100644 --- a/audio/src/lib.rs +++ b/audio/src/lib.rs @@ -41,7 +41,7 @@ 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, SharedMidiEventQueue, SharedTimeState}; +pub(crate) use crate::types::{AudioBuffer, SharedMidiEventQueue, SharedTimeState, SharedSample}; pub use crate::types::{AudioMessage, CommandsMessage, SharedExporter, SharedSynth}; use crossbeam_channel::{bounded, unbounded}; pub use export_state::ExportState; diff --git a/audio/src/midi_event_queue.rs b/audio/src/midi_event_queue.rs index 59758824..afca1e9d 100644 --- a/audio/src/midi_event_queue.rs +++ b/audio/src/midi_event_queue.rs @@ -9,14 +9,17 @@ pub(crate) struct MidiEventQueue { } impl MidiEventQueue { - /// Enqueue a new MIDI event. This will also sort the internal list of events. + /// 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 }); - // Sort the events. + } + + /// Sort the list of events by start time. + pub(crate) fn sort(&mut self) { self.events.sort_by(|a, b| a.time.cmp(&b.time)) } diff --git a/audio/src/player.rs b/audio/src/player.rs index 1bc8ff90..75d2aa82 100644 --- a/audio/src/player.rs +++ b/audio/src/player.rs @@ -1,3 +1,4 @@ +use crate::types::SharedSample; use crate::{SharedMidiEventQueue, SharedSynth, SharedTimeState}; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; use cpal::*; @@ -20,6 +21,7 @@ impl Player { midi_event_queue: SharedMidiEventQueue, time_state: SharedTimeState, synth: SharedSynth, + sample: SharedSample ) -> Option { // Get the host. let host = default_host(); @@ -50,6 +52,7 @@ impl Player { midi_event_queue, time_state, synth, + sample ); Some(Self { _host: host, @@ -69,6 +72,7 @@ impl Player { midi_event_queue: SharedMidiEventQueue, time_state: SharedTimeState, synth: SharedSynth, + sample: SharedSample ) -> Option { // Define the error callback. let err_callback = |err| println!("Stream error: {}", err); @@ -95,9 +99,10 @@ impl Player { let mut synth = synth.lock(); synth.write((&mut audio_buffers[0][0..len], &mut audio_buffers[1][0..len])); - // Advance time. + // Stop time. if let Some(time) = time_state.time { - time_state.time = Some(time + len as u64); + time_state.music = false; + time_state.time = None; } } else { // Iterate through the number of samples. @@ -136,6 +141,10 @@ impl Player { } } } + // 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. diff --git a/audio/src/types.rs b/audio/src/types.rs index eb54a664..53748271 100644 --- a/audio/src/types.rs +++ b/audio/src/types.rs @@ -16,3 +16,4 @@ pub type SharedExporter = Arc>; pub type SharedSynth = Arc>; pub(crate) type SharedMidiEventQueue = Arc>; pub(crate) type SharedTimeState = Arc>; +pub(crate) type SharedSample = Arc>; diff --git a/common/src/midi_track.rs b/common/src/midi_track.rs index 5e6b2243..026eb11e 100644 --- a/common/src/midi_track.rs +++ b/common/src/midi_track.rs @@ -31,6 +31,19 @@ impl MidiTrack { pub fn get_end(&self) -> Option { self.notes.iter().map(|n| n.end).max() } + + /// 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.gain as f64 / MAX_VOLUME as f64; + let mut notes = vec![]; + for note in self.notes.iter().filter(|n| n.start >= start) { + let mut n1 = *note; + n1.velocity = (n1.velocity as f64 * 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/io/src/lib.rs b/io/src/lib.rs index a2950833..f1eeb7da 100644 --- a/io/src/lib.rs +++ b/io/src/lib.rs @@ -702,30 +702,7 @@ impl IO { } } -/// 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( From 090157e1bbe8ab655b8b324c65f6dfba0e8de6fa Mon Sep 17 00:00:00 2001 From: Esther Alter Date: Mon, 13 Nov 2023 11:56:05 -0500 Subject: [PATCH 07/52] conn --- audio/src/command.rs | 29 ------ audio/src/conn.rs | 232 ++++++++++++++++++++++++++++++++++--------- audio/src/lib.rs | 9 +- audio/src/player.rs | 6 +- io/src/lib.rs | 2 - 5 files changed, 188 insertions(+), 90 deletions(-) diff --git a/audio/src/command.rs b/audio/src/command.rs index d361330d..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,10 +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, - /// Append silence to all but the longest audio. - AppendSilences { paths: Vec }, } diff --git a/audio/src/conn.rs b/audio/src/conn.rs index 740aceec..d92bf9ac 100644 --- a/audio/src/conn.rs +++ b/audio/src/conn.rs @@ -1,90 +1,182 @@ +use std::sync::Arc; + use crate::{ - AudioMessage, Command, CommandsMessage, ExportState, Player, SharedMidiEventQueue, SharedSynth, - SharedTimeState, SynthState, TimeState, types::SharedSample, + midi_event_queue::MidiEventQueue, types::SharedSample, Command, ExportState, + Player, SharedMidiEventQueue, SharedSynth, SharedTimeState, SynthState, TimeState, Program }; -use common::State; -use crossbeam_channel::{Receiver, Sender}; -use oxisynth::MidiEvent; +use common::{State, MAX_VOLUME}; +use crossbeam_channel::Receiver; +use hashbrown::HashMap; +use oxisynth::{MidiEvent, SoundFont, SoundFontId, Synth}; +use parking_lot::Mutex; +use std::path::{Path, PathBuf}; +use std::fs::File; + +/// 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..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 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, /// The playback framerate. 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 an audio sample. - recv_sample: Receiver, /// The most recent sample. pub sample: SharedSample, synth: SharedSynth, midi_event_queue: SharedMidiEventQueue, time_state: SharedTimeState, + soundfonts: HashMap, + pub state: SynthState, } impl Conn { pub(crate) fn new( - player: Option, - send_commands: Sender, - recv: Receiver, recv_export: Receiver>, - recv_time: Receiver, - recv_sample: Receiver, - synth: SharedSynth, - midi_event_queue: SharedMidiEventQueue, - time_state: SharedTimeState, - sample: SharedSample ) -> Self { + // Set the synthesizer. + let mut synth = Synth::default(); + synth.set_gain(MAX_VOLUME as f32); + let synth = Arc::new(Mutex::new(synth)); + + // Create other shared data. + let time_state = Arc::new(Mutex::new(TimeState::default())); + let midi_event_queue = Arc::new(Mutex::new(MidiEventQueue::default())); + let sample = Arc::new(Mutex::new((0.0, 0.0))); + + // Create the player. + let player_synth = Arc::clone(&synth); + let player_time_state = Arc::clone(&time_state); + let player_midi_event_queue = Arc::clone(&midi_event_queue); + let player_sample = Arc::clone(&sample); + let player = Player::new( + player_midi_event_queue, + player_time_state, + player_synth, + player_sample, + ); + + // Get the framerate. let framerate = match &player { Some(player) => player.framerate as f32, None => 0.0, }; Self { - state: SynthState::default(), export_state: None, _player: player, - send_commands, - recv, - recv_export, - recv_sample, framerate, sample, synth, midi_event_queue, - time_state + time_state, + soundfonts: HashMap::default(), + state: SynthState::default() } } - /// 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), - } - // Update the state. - if let Ok(state) = self.recv.recv() { - self.state = state; + /// Do all note-on and note-off events created by user input on this app frame. + pub fn do_note_events(&mut self, state: &State, note_ons: &[[u8; 3]], note_offs: &[u8]) { + if let Some(track) = state.music.get_selected_track() { + let mut synth = self.synth.lock(); + for note_on in note_ons.iter() { + if synth + .send_event(MidiEvent::NoteOn { + channel: track.channel, + key: note_on[1], + vel: note_on[2], + }) + .is_ok() + {} + } + for note_off in note_offs.iter() { + if synth + .send_event(MidiEvent::NoteOff { + channel: track.channel, + key: *note_off, + }) + .is_ok() + {} + } } } - /// Call this once per frame. - pub fn update(&mut self) { - // 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 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(); + let mut synth = self.synth.lock(); + for program in programs.iter().filter(|p| p.0 != channel) { + 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 mut banks = soundfont.banks.keys().copied().collect::>(); + let bank = banks[*bank_index]; + let preset = soundfont.banks[&bank][*preset_index]; + let channel = *channel; + let mut synth = self.synth.lock(); + if synth.program_select(channel, soundfont.id, bank, preset).is_ok() + { + self.set_program(channel, path, bank, preset); + } + } + Command::UnsetProgram { channel } => { + self.state.programs.remove(channel); + } + Command::SetGain { gain } => { + let mut synth = self.synth.lock(); + synth.set_gain(*gain as f32 / 127.0); + self.state.gain = *gain; + } } } } @@ -131,4 +223,48 @@ impl Conn { time_state.music = true; time_state.time = Some(start); } + + /// 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; + let mut synth = self.synth.lock(); + synth.program_select(channel, id, bank, preset).unwrap(); + self.set_program(channel, path, bank, preset); + } + + /// Set the synthesizer program to a program. + fn set_program(&mut self, channel: u8, path: &Path, bank: u32, preset: u8) { + 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 mut synth = self.synth.lock(); + 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); + } } diff --git a/audio/src/lib.rs b/audio/src/lib.rs index 2223521a..eece0ccc 100644 --- a/audio/src/lib.rs +++ b/audio/src/lib.rs @@ -41,7 +41,7 @@ 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, SharedMidiEventQueue, SharedTimeState, SharedSample}; +pub(crate) use crate::types::{AudioBuffer, SharedMidiEventQueue, SharedSample, SharedTimeState}; pub use crate::types::{AudioMessage, CommandsMessage, SharedExporter, SharedSynth}; use crossbeam_channel::{bounded, unbounded}; pub use export_state::ExportState; @@ -71,16 +71,9 @@ pub fn connect(exporter: &SharedExporter) -> Conn { 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, ) } diff --git a/audio/src/player.rs b/audio/src/player.rs index 75d2aa82..ab3180ad 100644 --- a/audio/src/player.rs +++ b/audio/src/player.rs @@ -21,7 +21,7 @@ impl Player { midi_event_queue: SharedMidiEventQueue, time_state: SharedTimeState, synth: SharedSynth, - sample: SharedSample + sample: SharedSample, ) -> Option { // Get the host. let host = default_host(); @@ -52,7 +52,7 @@ impl Player { midi_event_queue, time_state, synth, - sample + sample, ); Some(Self { _host: host, @@ -72,7 +72,7 @@ impl Player { midi_event_queue: SharedMidiEventQueue, time_state: SharedTimeState, synth: SharedSynth, - sample: SharedSample + sample: SharedSample, ) -> Option { // Define the error callback. let err_callback = |err| println!("Stream error: {}", err); diff --git a/io/src/lib.rs b/io/src/lib.rs index f1eeb7da..0967e577 100644 --- a/io/src/lib.rs +++ b/io/src/lib.rs @@ -702,8 +702,6 @@ impl IO { } } - - /// Converts all playable tracks to note-on commands. fn combine_tracks_to_commands( state: &State, From 1fbe9030243388d6ea2c4360d0fd5971fc14dfb0 Mon Sep 17 00:00:00 2001 From: Esther Alter Date: Mon, 13 Nov 2023 14:40:59 -0500 Subject: [PATCH 08/52] Audio crate compiles? --- audio/src/conn.rs | 331 +++++++++-- audio/src/export.rs | 10 + audio/src/export/export_state.rs | 14 + audio/src/{exporter => export}/export_type.rs | 0 audio/src/export/exportable.rs | 7 + audio/src/{exporter => export}/metadata.rs | 0 .../{exporter => export}/multi_file_suffix.rs | 0 audio/src/export_state.rs | 17 - audio/src/exporter.rs | 72 +-- audio/src/lib.rs | 71 +-- audio/src/midi_event_queue.rs | 8 + audio/src/player.rs | 15 +- audio/src/synthesizer.rs | 539 ------------------ audio/src/types.rs | 2 + 14 files changed, 338 insertions(+), 748 deletions(-) create mode 100644 audio/src/export/export_state.rs rename audio/src/{exporter => export}/export_type.rs (100%) create mode 100644 audio/src/export/exportable.rs rename audio/src/{exporter => export}/metadata.rs (100%) rename audio/src/{exporter => export}/multi_file_suffix.rs (100%) delete mode 100644 audio/src/export_state.rs delete mode 100644 audio/src/synthesizer.rs diff --git a/audio/src/conn.rs b/audio/src/conn.rs index d92bf9ac..2d6782f0 100644 --- a/audio/src/conn.rs +++ b/audio/src/conn.rs @@ -1,16 +1,26 @@ use std::sync::Arc; +use std::thread::spawn; +use crate::export::{ExportState, ExportType, Exportable, MultiFileSuffix}; +use crate::exporter::Exporter; +use crate::SharedExportState; use crate::{ - midi_event_queue::MidiEventQueue, types::SharedSample, Command, ExportState, - Player, SharedMidiEventQueue, SharedSynth, SharedTimeState, SynthState, TimeState, Program + midi_event_queue::MidiEventQueue, types::SharedSample, Command, Player, Program, + SharedMidiEventQueue, SharedSynth, SharedTimeState, SynthState, TimeState, }; -use common::{State, MAX_VOLUME}; -use crossbeam_channel::Receiver; +use common::open_file::Extension; +use common::{MidiTrack, PathsState, State, Time, MAX_VOLUME}; use hashbrown::HashMap; use oxisynth::{MidiEvent, SoundFont, SoundFontId, Synth}; use parking_lot::Mutex; -use std::path::{Path, PathBuf}; use std::fs::File; +use std::path::{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 = 1e-7; /// A convenient wrapper for a SoundFont. struct SoundFontBanks { @@ -39,7 +49,7 @@ impl SoundFontBanks { /// The connects used by an external function. pub struct Conn { /// The current export state, if any. - pub export_state: Option, + pub export_state: SharedExportState, /// The playback framerate. framerate: f32, /// The audio player. This is here so we don't drop it. @@ -51,12 +61,11 @@ pub struct Conn { time_state: SharedTimeState, soundfonts: HashMap, pub state: SynthState, + pub exporter: Exporter, } -impl Conn { - pub(crate) fn new( - recv_export: Receiver>, - ) -> Self { +impl Default for Conn { + fn default() -> Self { // Set the synthesizer. let mut synth = Synth::default(); synth.set_gain(MAX_VOLUME as f32); @@ -85,7 +94,7 @@ impl Conn { None => 0.0, }; Self { - export_state: None, + export_state: Arc::new(Mutex::new(ExportState::Done)), _player: player, framerate, sample, @@ -93,10 +102,13 @@ impl Conn { midi_event_queue, time_state, soundfonts: HashMap::default(), - state: SynthState::default() + state: SynthState::default(), + exporter: Exporter::default(), } } +} +impl Conn { /// Do all note-on and note-off events created by user input on this app frame. pub fn do_note_events(&mut self, state: &State, note_ons: &[[u8; 3]], note_offs: &[u8]) { if let Some(track) = state.music.get_selected_track() { @@ -157,24 +169,25 @@ impl Conn { }, } } - Command::SetProgram { channel, path, bank_index, preset_index } => { + Command::SetProgram { + channel, + path, + bank_index, + preset_index, + } => { let soundfont = &self.soundfonts[path]; - let mut banks = soundfont.banks.keys().copied().collect::>(); + let banks = soundfont.banks.keys().copied().collect::>(); let bank = banks[*bank_index]; let preset = soundfont.banks[&bank][*preset_index]; let channel = *channel; - let mut synth = self.synth.lock(); - if synth.program_select(channel, soundfont.id, bank, preset).is_ok() - { - self.set_program(channel, path, bank, preset); - } + 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 / 127.0); + synth.set_gain(*gain as f32 / MAX_VOLUME as f32); self.state.gain = *gain; } } @@ -194,7 +207,7 @@ impl Conn { // Enqueue note events. let mut midi_event_queue = self.midi_event_queue.lock(); - for track in state.music.midi_tracks { + for track in state.music.midi_tracks.iter() { for note in track.get_playback_notes(start) { // Note-on event. midi_event_queue.enqueue( @@ -234,37 +247,257 @@ impl Conn { let preset = soundfont.banks[&bank][0]; // Select the default program. let id = self.soundfonts[path].id; - let mut synth = self.synth.lock(); - synth.program_select(channel, id, bank, preset).unwrap(); - self.set_program(channel, path, bank, preset); + 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) { - 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(); + fn set_program(&mut self, channel: u8, path: &Path, bank: u32, preset: u8, id: SoundFontId) { let mut synth = self.synth.lock(); - 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); + 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 synth = self.synth.lock(); + 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 gain = self.state.gain as f32 / MAX_VOLUME as f32; + let mut exportables = vec![]; + let tracks = state.music.get_playable_tracks(); + + // 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; + 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: events, + total_samples: t1, + suffix, + }); + } + } + // Export all tracks combined. + else { + for track in tracks { + let mut events = MidiEventQueue::default(); + let mut t1 = 0; + self.enqueue_track_events(track, &state.time, &mut events, &mut t1, gain); + events.sort(); + // Add an exportable. + exportables.push(Exportable { + events: 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(); + spawn(move || Self::export(exportables, export_state, synth, exporter, path)); + } + + fn enqueue_track_events( + &self, + track: &MidiTrack, + time: &Time, + events: &mut MidiEventQueue, + t1: &mut u64, + gain: f32, + ) { + // Turn off the sound. + events.enqueue( + 0, + MidiEvent::AllSoundOff { + channel: track.channel, + }, + ); + for note in track.notes.iter() { + // Note-on. + events.enqueue( + time.ppq_to_samples(note.start, self.framerate), + MidiEvent::NoteOn { + channel: track.channel, + key: note.note, + vel: (note.velocity as f32 * gain) as u8, + }, + ); + let end = time.ppq_to_samples(note.start, self.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, + ) { + let mut decay_left = [0.0f32; DECAY_CHUNK_SIZE]; + let mut decay_right = [0.0f32; DECAY_CHUNK_SIZE]; + 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(); + let mut t0 = 0; + // Process each event. + while let Some(t) = exportable.events.get_next_time() { + // Get and send each event at this time. + for event in exportable.events.dequeue(t).iter() { + if synth.send_event(*event).is_ok() {} + } + // Write a sample. + if t0 == t { + let sample = synth.read_next(); + let t = t as usize; + left[t] = sample.0; + right[t] = sample.1; + } + // Write a block of samples. + else { + let t0u = t0 as usize; + let t1u = t as usize; + synth.write((left[t0u..t1u].as_mut(), right[t0u..t1u].as_mut())); + } + // Set the export state. + Self::set_export_state_wav(exportable, &export_state, t); + // Set the start time. + t0 = t; + } + // Append decaying silence. + Self::set_export_state(&export_state, ExportState::AppendingSilence); + let mut decaying = true; + while decaying { + // Write to the decay chunks. + synth.write((decay_left.as_mut_slice(), decay_right.as_mut_slice())); + // If the decay chunks are totally silent then we're not decaying anymore. + decaying = decay_left.iter().any(|s| s.abs() > SILENCE) + || decay_right.iter().any(|s| s.abs() > SILENCE); + // Add the silence. + if decaying { + left.extend_from_slice(&decay_left); + right.extend_from_slice(&decay_right); + } + } + // Convert. + Self::set_export_state(&export_state, ExportState::WritingToDisk); + let suffix = exportable.suffix.clone().unwrap(); + let path = if exporter.multi_file { + path.parent() + .unwrap() + .join(format!( + "{}_{}{}", + path.file_name().unwrap().to_str().unwrap(), + suffix, + extension.to_str(true) + )) + .to_path_buf() + } else { + path.clone() + }; + 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); + } + } + // Done. + Self::set_export_state(&export_state, ExportState::Done); + } + } + + /// 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/export.rs b/audio/src/export.rs index 8b137891..19c34846 100644 --- a/audio/src/export.rs +++ b/audio/src/export.rs @@ -1 +1,11 @@ +mod export_state; +mod export_type; +mod exportable; +mod metadata; +mod multi_file_suffix; +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/export/export_state.rs b/audio/src/export/export_state.rs new file mode 100644 index 00000000..f4999a0a --- /dev/null +++ b/audio/src/export/export_state.rs @@ -0,0 +1,14 @@ +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub enum ExportState { + /// Writing samples to a wav buffer. + WritingWav { + total_samples: u64, + exported_samples: u64, + }, + /// Writing silence to a wav buffer while the audio decays. + AppendingSilence, + /// 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 c1097783..0671311f 100644 --- a/audio/src/exporter.rs +++ b/audio/src/exporter.rs @@ -1,27 +1,22 @@ -mod export_type; -use common::IndexedValues; -use serde::{Deserialize, Serialize}; -mod metadata; -mod multi_file_suffix; +use crate::export::{ExportType, Metadata, MultiFileSuffix}; use crate::{AudioBuffer, SharedExporter, 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 hound::*; use id3::*; -pub use metadata::*; 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, PathBuf}; +use std::path::Path; use vorbis_encoder::Encoder; mod export_setting; pub use export_setting::ExportSetting; @@ -491,65 +486,6 @@ impl Exporter { .expect("Failed to write samples to file."); } - /// Open a bunch of wav files. Get the longest one. Export to the correct export type, appending silence to the shorter waveforms. - pub(crate) fn append_silences(&self, paths: &[PathBuf]) { - // Get the longest file. - let max_time = paths - .iter() - .map(|p| WavReader::open(p).unwrap().duration()) - .max() - .unwrap(); - let export_type = self.export_type.get(); - // Open the files. - for path in paths.iter() { - let time = WavReader::open(path).unwrap().duration(); - let silence = max_time - time; - if silence == 0 { - continue; - } - match export_type { - ExportType::Wav => { - // Write silence. - let mut writer = WavWriter::append(path).unwrap(); - let mut i16_writer = writer.get_i16_writer(silence * NUM_CHANNELS as u32); - for _ in 0..silence { - i16_writer.write_sample(0); - i16_writer.write_sample(0); - } - i16_writer.flush().unwrap(); - writer.finalize().unwrap(); - } - ExportType::Mid => (), - // Encode to mp3. - ExportType::MP3 => { - self.mp3( - path, - &Self::samples_and_silence_to_i16_buffer(path, silence), - ); - } - ExportType::Ogg => { - let mut buffer = Self::samples_and_silence_to_i16_buffer(path, silence); - let mut samples = buffer[0].clone(); - samples.append(&mut buffer[1]); - self.ogg_i16(path, &samples); - } - } - } - } - - /// Read a .wav file into a samples buffer. Append some silence. - fn samples_and_silence_to_i16_buffer(path: &Path, silence: u32) -> [Vec; NUM_CHANNELS] { - let reader = WavReader::open(path).unwrap(); - let samples = reader.into_samples::(); - let mut buffer: [Vec; NUM_CHANNELS] = [vec![], vec![]]; - for sample in samples.filter_map(|s| s.ok()).enumerate() { - buffer[usize::from(sample.0 % 2 != 0)].push(sample.1); - } - buffer[0].append(&mut vec![0; silence as usize]); - buffer[1].append(&mut vec![0; silence as usize]); - buffer - } - /// Converts a PPQ value into a MIDI time delta and resets `ppq` to zero. fn get_delta_time(ppq: &mut u64) -> u28 { // Get the dt. diff --git a/audio/src/lib.rs b/audio/src/lib.rs index eece0ccc..3f9aa3f0 100644 --- a/audio/src/lib.rs +++ b/audio/src/lib.rs @@ -26,13 +26,11 @@ mod command; mod conn; mod export; -mod export_state; pub mod exporter; pub(crate) mod midi_event_queue; mod player; mod program; mod synth_state; -mod synthesizer; mod time_state; pub(crate) mod timed_midi_event; mod types; @@ -41,69 +39,8 @@ 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, SharedMidiEventQueue, SharedSample, SharedTimeState}; -pub use crate::types::{AudioMessage, CommandsMessage, SharedExporter, SharedSynth}; -use crossbeam_channel::{bounded, unbounded}; -pub use export_state::ExportState; +pub(crate) use crate::types::{AudioBuffer, SharedMidiEventQueue, SharedTimeState}; +pub use crate::types::{ + AudioMessage, CommandsMessage, SharedExportState, SharedExporter, SharedSynth, +}; 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, - ) - }); - // Get the conn. - Conn::new( - recv_export, - ) -} - -#[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 index afca1e9d..fc9b4de7 100644 --- a/audio/src/midi_event_queue.rs +++ b/audio/src/midi_event_queue.rs @@ -18,6 +18,14 @@ impl MidiEventQueue { 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_by(|a, b| a.time.cmp(&b.time)) diff --git a/audio/src/player.rs b/audio/src/player.rs index ab3180ad..a46b887d 100644 --- a/audio/src/player.rs +++ b/audio/src/player.rs @@ -39,7 +39,6 @@ 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; @@ -78,8 +77,8 @@ impl Player { let err_callback = |err| println!("Stream error: {}", err); let two_channels = channels == 2; - - let mut audio_buffers = [vec![0.0; 1], vec![0.0; 1]]; + let mut left = vec![0.0; 1]; + let mut right = vec![0.0; 1]; // Define the data callback used by cpal. Move `stream_send` into the closure. let data_callback = move |output: &mut [f32], _: &OutputCallbackInfo| { @@ -90,17 +89,17 @@ impl Player { let len = output.len(); // Resize the buffers. - if len > audio_buffers[0].len() { - audio_buffers[0].resize(len, 0.0); - audio_buffers[1].resize(len, 0.0); + if len > left.len() { + left.resize(len, 0.0); + right.resize(len, 0.0); } // Write the samples. let mut synth = synth.lock(); - synth.write((&mut audio_buffers[0][0..len], &mut audio_buffers[1][0..len])); + synth.write((left[0..len].as_mut(), right[0..len].as_mut())); // Stop time. - if let Some(time) = time_state.time { + if time_state.time.is_some() { time_state.music = false; time_state.time = None; } diff --git a/audio/src/synthesizer.rs b/audio/src/synthesizer.rs deleted file mode 100644 index e29d2f70..00000000 --- a/audio/src/synthesizer.rs +++ /dev/null @@ -1,539 +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, - // Append silence to the end of exported audio. - Command::AppendSilences { paths } => { - let exporter = s.exporter.lock(); - exporter.append_silences(paths); - } - } - } - // 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) - } - } - // 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. - if send_sample.try_send(sample).is_ok() {} - // Send the time state. - if send_time.try_send(s.state.time).is_ok() {} - } - } - // 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/types.rs b/audio/src/types.rs index 53748271..40e806f0 100644 --- a/audio/src/types.rs +++ b/audio/src/types.rs @@ -1,3 +1,4 @@ +use crate::export::ExportState; use crate::exporter::Exporter; use crate::midi_event_queue::MidiEventQueue; use crate::{Command, TimeState}; @@ -14,6 +15,7 @@ pub(crate) type AudioBuffer = [Vec; 2]; /// The exporter. pub type SharedExporter = Arc>; pub type SharedSynth = Arc>; +pub type SharedExportState = Arc>; pub(crate) type SharedMidiEventQueue = Arc>; pub(crate) type SharedTimeState = Arc>; pub(crate) type SharedSample = Arc>; From 681b946fa9cabc3c4a4a69ca5096c161cad67748 Mon Sep 17 00:00:00 2001 From: Esther Alter Date: Mon, 13 Nov 2023 16:26:53 -0500 Subject: [PATCH 09/52] io compiles --- Cargo.lock | 1 - audio/Cargo.toml | 1 - audio/src/conn.rs | 77 +++++-- audio/src/export.rs | 2 + audio/src/export/export_state.rs | 1 + audio/src/exporter.rs | 13 +- audio/src/exporter/export_setting.rs | 20 -- audio/src/lib.rs | 6 +- audio/src/types.rs | 3 - io/src/abc123.rs | 38 ---- io/src/export_panel.rs | 11 +- io/src/export_settings_panel.rs | 164 +++++++------- io/src/import_midi.rs | 21 +- io/src/io_command.rs | 3 +- io/src/lib.rs | 295 +++----------------------- io/src/links_panel.rs | 7 +- io/src/music_panel.rs | 20 +- io/src/open_file_panel.rs | 66 ++---- io/src/panel.rs | 17 +- io/src/piano_roll/edit.rs | 7 +- io/src/piano_roll/piano_roll_panel.rs | 24 +-- io/src/piano_roll/select.rs | 7 +- io/src/piano_roll/time.rs | 7 +- io/src/piano_roll/view.rs | 7 +- io/src/quit_panel.rs | 7 +- io/src/save.rs | 29 +-- io/src/snapshot.rs | 4 +- io/src/tracks_panel.rs | 7 +- 28 files changed, 275 insertions(+), 590 deletions(-) delete mode 100644 audio/src/exporter/export_setting.rs diff --git a/Cargo.lock b/Cargo.lock index 81bcd05a..c85aecbc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -101,7 +101,6 @@ dependencies = [ "chrono", "common", "cpal", - "crossbeam-channel", "hashbrown 0.13.2", "hound", "id3", diff --git a/audio/Cargo.toml b/audio/Cargo.toml index 4016eafd..0a0b1e3f 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/conn.rs b/audio/src/conn.rs index 2d6782f0..22cc68c2 100644 --- a/audio/src/conn.rs +++ b/audio/src/conn.rs @@ -9,7 +9,7 @@ use crate::{ SharedMidiEventQueue, SharedSynth, SharedTimeState, SynthState, TimeState, }; use common::open_file::Extension; -use common::{MidiTrack, PathsState, State, Time, MAX_VOLUME}; +use common::{MidiTrack, Music, PathsState, State, Time, MAX_VOLUME}; use hashbrown::HashMap; use oxisynth::{MidiEvent, SoundFont, SoundFontId, Synth}; use parking_lot::Mutex; @@ -94,7 +94,7 @@ impl Default for Conn { None => 0.0, }; Self { - export_state: Arc::new(Mutex::new(ExportState::Done)), + export_state: Arc::new(Mutex::new(ExportState::NotExporting)), _player: player, framerate, sample, @@ -109,20 +109,30 @@ impl Default for Conn { } impl Conn { - /// Do all note-on and note-off events created by user input on this app frame. - pub fn do_note_events(&mut self, state: &State, note_ons: &[[u8; 3]], note_offs: &[u8]) { + /// 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() { let mut synth = self.synth.lock(); + synth.set_sample_rate(self.framerate); + let gain = track.gain as f32 / MAX_VOLUME as f32; for note_on in note_ons.iter() { if synth .send_event(MidiEvent::NoteOn { channel: track.channel, key: note_on[1], - vel: note_on[2], + vel: (note_on[2] as f32 * gain) as u8, }) .is_ok() {} } + } + } + + /// 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() { + let mut synth = self.synth.lock(); + synth.set_sample_rate(self.framerate); for note_off in note_offs.iter() { if synth .send_event(MidiEvent::NoteOff { @@ -194,8 +204,22 @@ impl Conn { } } + /// Start to play music if music isn't playing. Stop music if music is playing. + pub fn set_music(&mut self, state: &State) { + let music = self.time_state.lock().music.clone(); + if music { + self.stop_music(&state.music) + } else { + self.start_music(state) + } + } + + pub fn exporting(&self) -> bool { + *self.export_state.lock() != ExportState::NotExporting + } + /// Schedule MIDI events and start to play music. - pub fn schedule_music(&mut self, state: &State) { + fn start_music(&mut self, state: &State) { // Get the start time. let start = state .time @@ -208,6 +232,7 @@ impl Conn { // Enqueue note events. let mut midi_event_queue = self.midi_event_queue.lock(); for track in state.music.midi_tracks.iter() { + let gain = track.gain as f32 / MAX_VOLUME as f32; for note in track.get_playback_notes(start) { // Note-on event. midi_event_queue.enqueue( @@ -215,7 +240,7 @@ impl Conn { MidiEvent::NoteOn { channel: track.channel, key: note.note, - vel: note.velocity, + vel: (note.velocity as f32 * gain) as u8, }, ); // Note-off event. @@ -237,6 +262,28 @@ impl Conn { time_state.time = Some(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() + {} + if synth + .send_event(MidiEvent::AllSoundOff { + channel: track.channel, + }) + .is_ok() + {} + } + let mut time_state = self.time_state.lock(); + time_state.music = false; + time_state.time = None; + } + /// Set the synthesizer program to a default program. fn set_program_default(&mut self, channel: u8, path: &Path) { let soundfont = &self.soundfonts[path]; @@ -285,6 +332,7 @@ impl Conn { let gain = self.state.gain as f32 / MAX_VOLUME as f32; 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 { @@ -431,7 +479,7 @@ impl Conn { .unwrap() .join(format!( "{}_{}{}", - path.file_name().unwrap().to_str().unwrap(), + path.file_stem().unwrap().to_str().unwrap(), suffix, extension.to_str(true) )) @@ -458,6 +506,14 @@ impl Conn { // Done. Self::set_export_state(&export_state, ExportState::Done); } + Self::set_export_state(&export_state, ExportState::NotExporting); + } + + /// 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. @@ -474,10 +530,7 @@ impl Conn { } /// Set the export state. - fn set_export_state( - export_state: &SharedExportState, - state: ExportState, - ) { + fn set_export_state(export_state: &SharedExportState, state: ExportState) { let mut export_state = export_state.lock(); *export_state = state; } diff --git a/audio/src/export.rs b/audio/src/export.rs index 19c34846..262c8afe 100644 --- a/audio/src/export.rs +++ b/audio/src/export.rs @@ -1,9 +1,11 @@ +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; diff --git a/audio/src/export/export_state.rs b/audio/src/export/export_state.rs index f4999a0a..70001cf1 100644 --- a/audio/src/export/export_state.rs +++ b/audio/src/export/export_state.rs @@ -1,5 +1,6 @@ #[derive(Copy, Clone, Eq, PartialEq, Debug)] pub enum ExportState { + NotExporting, /// Writing samples to a wav buffer. WritingWav { total_samples: u64, diff --git a/audio/src/exporter.rs b/audio/src/exporter.rs index 0671311f..e2e37db8 100644 --- a/audio/src/exporter.rs +++ b/audio/src/exporter.rs @@ -1,5 +1,5 @@ -use crate::export::{ExportType, Metadata, MultiFileSuffix}; -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; @@ -18,10 +18,6 @@ use std::io::Read; use std::io::{Cursor, Write}; use std::path::Path; use vorbis_encoder::Encoder; -mod export_setting; -pub use export_setting::ExportSetting; -use parking_lot::Mutex; -use std::sync::Arc; /// The number of channels. const NUM_CHANNELS: usize = 2; @@ -183,11 +179,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/exporter/export_setting.rs b/audio/src/exporter/export_setting.rs deleted file mode 100644 index ac991e99..00000000 --- a/audio/src/exporter/export_setting.rs +++ /dev/null @@ -1,20 +0,0 @@ -use serde::{Deserialize, Serialize}; - -/// Enum values for export settings. -#[derive(Debug, Eq, PartialEq, Copy, Clone, Default, Deserialize, Serialize)] -pub enum ExportSetting { - #[default] - Framerate, - Title, - Artist, - Copyright, - Album, - TrackNumber, - Genre, - Comment, - Mp3BitRate, - Mp3Quality, - OggQuality, - MultiFile, - MultiFileSuffix, -} diff --git a/audio/src/lib.rs b/audio/src/lib.rs index 3f9aa3f0..fba1498c 100644 --- a/audio/src/lib.rs +++ b/audio/src/lib.rs @@ -25,7 +25,7 @@ mod command; mod conn; -mod export; +pub mod export; pub mod exporter; pub(crate) mod midi_event_queue; mod player; @@ -40,7 +40,5 @@ use crate::program::Program; pub use crate::synth_state::SynthState; use crate::time_state::TimeState; pub(crate) use crate::types::{AudioBuffer, SharedMidiEventQueue, SharedTimeState}; -pub use crate::types::{ - AudioMessage, CommandsMessage, SharedExportState, SharedExporter, SharedSynth, -}; +pub use crate::types::{AudioMessage, CommandsMessage, SharedExportState, SharedSynth}; use player::Player; diff --git a/audio/src/types.rs b/audio/src/types.rs index 40e806f0..57ee1e8c 100644 --- a/audio/src/types.rs +++ b/audio/src/types.rs @@ -1,5 +1,4 @@ use crate::export::ExportState; -use crate::exporter::Exporter; use crate::midi_event_queue::MidiEventQueue; use crate::{Command, TimeState}; use oxisynth::Synth; @@ -12,8 +11,6 @@ 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 type SharedSynth = Arc>; pub type SharedExportState = Arc>; pub(crate) type SharedMidiEventQueue = Arc>; 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 f2f00f54..db3a2d97 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::Wav => Self::update_settings( |e| &mut e.wav_settings, @@ -609,7 +608,7 @@ impl Panel for ExportSettingsPanel { input, tts, text, - exporter, + &mut conn.exporter, ), } } @@ -618,43 +617,40 @@ 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::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::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), }; } - 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::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: &Exporter| &e.wav_settings, &conn.exporter), } } diff --git a/io/src/import_midi.rs b/io/src/import_midi.rs index 2923e4cd..00b68e9d 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(&vec![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(&vec![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 0967e577..166948bc 100644 --- a/io/src/lib.rs +++ b/io/src/lib.rs @@ -15,11 +15,9 @@ //! `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,8 +56,6 @@ 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: /// @@ -91,8 +87,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 +186,6 @@ impl IO { links_panel, redo: vec![], undo: vec![], - export_queue: vec![], pre_export_panels: vec![], pre_export_focus: 0, } @@ -206,10 +199,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 +209,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 +221,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 +232,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 +258,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 +266,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 +295,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 +308,12 @@ impl IO { } // Export. else if input.happened(&InputEvent::ExportFile) { + let export_state = conn.export_state.lock().clone(); // 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 +339,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 +355,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 +397,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 +411,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 +424,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 +455,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 +479,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,181 +514,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; - self.export_queue.push(( - vec![Command::StopMusic, Command::AppendSilences { paths }], - None, - )); - } -} - -/// 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 f26ab82a..670421de 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,32 +333,25 @@ 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), ); - match &ex.export_type.get() { + match &conn.exporter.export_type.get() { // Export to a .wav file. ExportType::Wav => { - 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])); } // Export to a .mp3 file. ExportType::MP3 => { - 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])); } ExportType::Ogg => { - 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])); } // Export to a .mid file. ExportType::Mid => { - ex.mid( + conn.exporter.mid( &paths_state.exports.directory.path.join(filename), &state.music, &state.time, @@ -384,7 +364,7 @@ impl Panel for OpenFilePanel { 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); } @@ -398,19 +378,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 18966a99..ac96ed3a 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 serde::{Deserialize, Serialize}; @@ -22,6 +21,8 @@ pub(crate) struct Save { paths_state: PathsState, /// The exporter state. exporter: Exporter, + /// The version string. + version: String, } impl Save { @@ -31,20 +32,14 @@ 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. match OpenOptions::new() @@ -73,14 +68,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(); @@ -96,8 +84,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. @@ -133,7 +120,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 } From 3430485927d6f25c9fa91a38f630691f93f8b5d7 Mon Sep 17 00:00:00 2001 From: Esther Alter Date: Mon, 13 Nov 2023 16:27:02 -0500 Subject: [PATCH 10/52] export_setting --- audio/src/export/export_setting.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 audio/src/export/export_setting.rs diff --git a/audio/src/export/export_setting.rs b/audio/src/export/export_setting.rs new file mode 100644 index 00000000..ac991e99 --- /dev/null +++ b/audio/src/export/export_setting.rs @@ -0,0 +1,20 @@ +use serde::{Deserialize, Serialize}; + +/// Enum values for export settings. +#[derive(Debug, Eq, PartialEq, Copy, Clone, Default, Deserialize, Serialize)] +pub enum ExportSetting { + #[default] + Framerate, + Title, + Artist, + Copyright, + Album, + TrackNumber, + Genre, + Comment, + Mp3BitRate, + Mp3Quality, + OggQuality, + MultiFile, + MultiFileSuffix, +} From 30c7521cb181178f6bfcb88787894bc0f19e8deb Mon Sep 17 00:00:00 2001 From: Esther Alter Date: Mon, 13 Nov 2023 16:37:18 -0500 Subject: [PATCH 11/52] render --- audio/src/conn.rs | 2 +- data/text.csv | 4 +- render/src/drawable.rs | 4 +- render/src/export_panel.rs | 81 +++++++++++++++++++---------- render/src/export_settings_panel.rs | 70 ++++++++++++++----------- render/src/links_panel.rs | 10 +--- render/src/main_menu.rs | 21 +++----- render/src/music_panel.rs | 13 +---- render/src/open_file_panel.rs | 9 +--- render/src/panel.rs | 1 - render/src/panels.rs | 6 +-- render/src/piano_roll_panel.rs | 10 +--- render/src/quit_panel.rs | 10 +--- render/src/tracks_panel.rs | 10 +--- 14 files changed, 115 insertions(+), 136 deletions(-) diff --git a/audio/src/conn.rs b/audio/src/conn.rs index 22cc68c2..8ba5e622 100644 --- a/audio/src/conn.rs +++ b/audio/src/conn.rs @@ -51,7 +51,7 @@ pub struct Conn { /// The current export state, if any. pub export_state: SharedExportState, /// The playback framerate. - framerate: f32, + pub framerate: f32, /// The audio player. This is here so we don't drop it. _player: Option, /// The most recent sample. 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/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..167ac7e1 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::AppendingSilence => { + 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 67eec9db..52590be6 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; @@ -308,25 +309,16 @@ 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::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, } as u32 + 1; @@ -343,19 +335,39 @@ 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) - } + 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, + ), } } } 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..fd2b3e0f 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; + let sample = conn.sample.lock().clone(); + if self.time % POWER_BAR_DELTA == 0 { + 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..13aa7fde 100644 --- a/render/src/piano_roll_panel.rs +++ b/render/src/piano_roll_panel.rs @@ -192,15 +192,7 @@ impl PianoRollPanel { } 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 { 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. From af090906395157099cb2883d4e497b5c1db591dd Mon Sep 17 00:00:00 2001 From: Esther Alter Date: Mon, 13 Nov 2023 16:39:58 -0500 Subject: [PATCH 12/52] main --- src/main.rs | 26 ++++++-------------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/src/main.rs b/src/main.rs index fb19eada..f29e3cfc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,8 +2,7 @@ use std::path::PathBuf; -use audio::connect; -use audio::exporter::Exporter; +use audio::Conn; use clap::Parser; use common::config::{load, parse_bool}; use common::sizes::get_window_pixel_size; @@ -85,11 +84,8 @@ async fn main() { // Get the input object. let mut input = Input::new(&config); - // 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); @@ -132,13 +128,7 @@ async fn main() { // Open the initial save file if set. if let Some(save_path) = cli.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. @@ -148,14 +138,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. @@ -166,14 +156,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(); From a878a3f56be581f012982b69947040603febce2a Mon Sep 17 00:00:00 2001 From: Esther Alter Date: Mon, 13 Nov 2023 16:44:21 -0500 Subject: [PATCH 13/52] clippy --- audio/src/conn.rs | 10 ++++------ io/src/import_midi.rs | 4 ++-- io/src/lib.rs | 2 +- render/src/main_menu.rs | 2 +- text/src/tts.rs | 20 +++++++------------- 5 files changed, 15 insertions(+), 23 deletions(-) diff --git a/audio/src/conn.rs b/audio/src/conn.rs index 8ba5e622..d2cd988e 100644 --- a/audio/src/conn.rs +++ b/audio/src/conn.rs @@ -206,7 +206,7 @@ impl Conn { /// Start to play music if music isn't playing. Stop music if music is playing. pub fn set_music(&mut self, state: &State) { - let music = self.time_state.lock().music.clone(); + let music = self.time_state.lock().music; if music { self.stop_music(&state.music) } else { @@ -270,9 +270,7 @@ impl Conn { .send_event(MidiEvent::AllNotesOff { channel: track.channel, }) - .is_ok() - {} - if synth + .is_ok() && synth .send_event(MidiEvent::AllSoundOff { channel: track.channel, }) @@ -344,7 +342,7 @@ impl Conn { let suffix = Some(self.get_export_file_suffix(track)); // Add an exportable. exportables.push(Exportable { - events: events, + events, total_samples: t1, suffix, }); @@ -359,7 +357,7 @@ impl Conn { events.sort(); // Add an exportable. exportables.push(Exportable { - events: events, + events, total_samples: t1, suffix: None, }); diff --git a/io/src/import_midi.rs b/io/src/import_midi.rs index 00b68e9d..9a457a14 100644 --- a/io/src/import_midi.rs +++ b/io/src/import_midi.rs @@ -19,7 +19,7 @@ pub(crate) fn import(path: &Path, state: &mut State, conn: &mut Conn) { let c = i as u8; let mut track = MidiTrack::new(c); // Load the default SoundFont. - conn.do_commands(&vec![Command::LoadSoundFont { + conn.do_commands(&[Command::LoadSoundFont { channel: c, path: paths.default_soundfont_path.clone(), }]); @@ -80,7 +80,7 @@ pub(crate) fn import(path: &Path, state: &mut State, conn: &mut Conn) { } // Set the preset. MidiMessage::ProgramChange { program } => { - conn.do_commands(&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/lib.rs b/io/src/lib.rs index 166948bc..07a6dc0b 100644 --- a/io/src/lib.rs +++ b/io/src/lib.rs @@ -308,7 +308,7 @@ impl IO { } // Export. else if input.happened(&InputEvent::ExportFile) { - let export_state = conn.export_state.lock().clone(); + let export_state = *conn.export_state.lock(); // We aren't exporting already. if export_state == ExportState::NotExporting { self.pre_export_focus = state.focus.get(); diff --git a/render/src/main_menu.rs b/render/src/main_menu.rs index fd2b3e0f..acd3d5d9 100644 --- a/render/src/main_menu.rs +++ b/render/src/main_menu.rs @@ -280,7 +280,7 @@ 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. - let sample = conn.sample.lock().clone(); + let sample = *conn.sample.lock(); if self.time % POWER_BAR_DELTA == 0 { self.set_lerp_target(0, sample.0); self.set_lerp_target(1, sample.1); diff --git a/text/src/tts.rs b/text/src/tts.rs index aa7f8e8b..453c54c7 100644 --- a/text/src/tts.rs +++ b/text/src/tts.rs @@ -32,18 +32,12 @@ impl TTS { let callbacks = tts.supported_features().utterance_callbacks && !cfg!(target_os = "macos"); if callbacks { - if tts - .on_utterance_begin(Some(Box::new(on_utterance_begin))) - .is_ok() - {} - if tts - .on_utterance_end(Some(Box::new(on_utterance_end))) - .is_ok() - {} - if tts - .on_utterance_stop(Some(Box::new(on_utterance_end))) - .is_ok() - {} + let _ = tts + .on_utterance_begin(Some(Box::new(on_utterance_begin))); + let _ = tts + .on_utterance_end(Some(Box::new(on_utterance_end))); + let _ = tts + .on_utterance_stop(Some(Box::new(on_utterance_end))); } // Try to set the voice. if let Ok(voices) = tts.voices() { @@ -103,7 +97,7 @@ impl TTS { } else { "rate_linux" }; - if tts.set_rate(parse(section, rate_key)).is_ok() {} + let _ = tts.set_rate(parse(section, rate_key)); (Some(tts), callbacks) } Err(_) => (None, false), From 8abe3132112a79194898448605b7b77f0e44a71d Mon Sep 17 00:00:00 2001 From: Esther Alter Date: Tue, 14 Nov 2023 09:35:47 -0500 Subject: [PATCH 14/52] hmmmm --- audio/src/conn.rs | 61 ++++++++----- audio/src/decayer.rs | 34 +++++++ audio/src/export/export_state.rs | 4 +- audio/src/lib.rs | 6 +- audio/src/play_state.rs | 9 ++ audio/src/player.rs | 147 ++++++++++++++++++++----------- audio/src/types.rs | 2 + render/src/export_panel.rs | 2 +- text/src/tts.rs | 9 +- 9 files changed, 191 insertions(+), 83 deletions(-) create mode 100644 audio/src/decayer.rs create mode 100644 audio/src/play_state.rs diff --git a/audio/src/conn.rs b/audio/src/conn.rs index d2cd988e..4619e219 100644 --- a/audio/src/conn.rs +++ b/audio/src/conn.rs @@ -1,8 +1,11 @@ use std::sync::Arc; use std::thread::spawn; +use crate::decayer::{Decayer, DECAY_CHUNK_SIZE}; 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, @@ -16,12 +19,6 @@ use parking_lot::Mutex; use std::fs::File; use std::path::{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 = 1e-7; - /// A convenient wrapper for a SoundFont. struct SoundFontBanks { id: SoundFontId, @@ -62,6 +59,7 @@ pub struct Conn { soundfonts: HashMap, pub state: SynthState, pub exporter: Exporter, + play_state: SharedPlayState, } impl Default for Conn { @@ -75,17 +73,20 @@ impl Default for Conn { let time_state = Arc::new(Mutex::new(TimeState::default())); 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_time_state = Arc::clone(&time_state); 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_time_state, player_synth, player_sample, + player_play_state, ); // Get the framerate. @@ -104,6 +105,7 @@ impl Default for Conn { soundfonts: HashMap::default(), state: SynthState::default(), exporter: Exporter::default(), + play_state, } } } @@ -125,6 +127,11 @@ impl Conn { .is_ok() {} } + if !note_ons.is_empty() { + // Play music. + let mut play_state = self.play_state.lock(); + *play_state = PlayState::Decaying; + } } } @@ -142,6 +149,11 @@ impl Conn { .is_ok() {} } + if !note_offs.is_empty() { + // Play music. + let mut play_state = self.play_state.lock(); + *play_state = PlayState::Decaying; + } } } @@ -260,6 +272,9 @@ impl Conn { let mut time_state = self.time_state.lock(); time_state.music = true; time_state.time = Some(start); + // Play music. + let mut play_state = self.play_state.lock(); + *play_state = PlayState::Playing; } /// Stop ongoing music. @@ -270,16 +285,20 @@ impl Conn { .send_event(MidiEvent::AllNotesOff { channel: track.channel, }) - .is_ok() && synth - .send_event(MidiEvent::AllSoundOff { - channel: track.channel, - }) .is_ok() + && synth + .send_event(MidiEvent::AllSoundOff { + channel: track.channel, + }) + .is_ok() {} } let mut time_state = self.time_state.lock(); time_state.music = false; time_state.time = None; + // Let the audio decay. + let mut play_state = self.play_state.lock(); + *play_state = PlayState::Decaying; } /// Set the synthesizer program to a default program. @@ -418,8 +437,7 @@ impl Conn { exporter: Exporter, path: PathBuf, ) { - let mut decay_left = [0.0f32; DECAY_CHUNK_SIZE]; - let mut decay_right = [0.0f32; DECAY_CHUNK_SIZE]; + 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; @@ -455,18 +473,13 @@ impl Conn { t0 = t; } // Append decaying silence. - Self::set_export_state(&export_state, ExportState::AppendingSilence); - let mut decaying = true; - while decaying { - // Write to the decay chunks. - synth.write((decay_left.as_mut_slice(), decay_right.as_mut_slice())); - // If the decay chunks are totally silent then we're not decaying anymore. - decaying = decay_left.iter().any(|s| s.abs() > SILENCE) - || decay_right.iter().any(|s| s.abs() > SILENCE); - // Add the silence. - if decaying { - left.extend_from_slice(&decay_left); - right.extend_from_slice(&decay_right); + Self::set_export_state(&export_state, ExportState::AppendingDecay); + while decayer.decaying { + decayer.decay(&mut synth, DECAY_CHUNK_SIZE); + // Add the decay. + if decayer.decaying { + left.extend_from_slice(&decayer.left); + right.extend_from_slice(&decayer.right); } } // Convert. diff --git a/audio/src/decayer.rs b/audio/src/decayer.rs new file mode 100644 index 00000000..ccdbff5a --- /dev/null +++ b/audio/src/decayer.rs @@ -0,0 +1,34 @@ +use oxisynth::Synth; + +/// Export this many bytes per decay chunk. +pub(crate) 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 = 1e-7; + +/// Write audio samples during a decay. +pub(crate) struct Decayer { + pub left: [f32; DECAY_CHUNK_SIZE], + pub right: [f32; DECAY_CHUNK_SIZE], + pub decaying: bool, +} + +impl Default for Decayer { + fn default() -> Self { + Self { + left: [0.0; DECAY_CHUNK_SIZE], + right: [0.0; DECAY_CHUNK_SIZE], + decaying: false, + } + } +} + +impl Decayer { + pub fn decay(&mut self, synth: &mut Synth, len: usize) { + // Write to the decay chunks. + synth.write((self.left[0..len].as_mut(), self.right[0..len].as_mut())); + // If the decay chunks are totally silent then we're not decaying anymore. + self.decaying = self.left.iter().any(|s| s.abs() > SILENCE) + || self.right.iter().any(|s| s.abs() > SILENCE); + } +} diff --git a/audio/src/export/export_state.rs b/audio/src/export/export_state.rs index 70001cf1..f3324b53 100644 --- a/audio/src/export/export_state.rs +++ b/audio/src/export/export_state.rs @@ -6,8 +6,8 @@ pub enum ExportState { total_samples: u64, exported_samples: u64, }, - /// Writing silence to a wav buffer while the audio decays. - AppendingSilence, + /// 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. diff --git a/audio/src/lib.rs b/audio/src/lib.rs index fba1498c..99423ff5 100644 --- a/audio/src/lib.rs +++ b/audio/src/lib.rs @@ -25,9 +25,11 @@ mod command; mod conn; +mod decayer; pub mod export; pub mod exporter; pub(crate) mod midi_event_queue; +mod play_state; mod player; mod program; mod synth_state; @@ -39,6 +41,8 @@ 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, SharedMidiEventQueue, SharedTimeState}; +pub(crate) use crate::types::{ + AudioBuffer, SharedMidiEventQueue, SharedPlayState, SharedTimeState, +}; pub use crate::types::{AudioMessage, CommandsMessage, SharedExportState, SharedSynth}; use player::Player; diff --git a/audio/src/play_state.rs b/audio/src/play_state.rs new file mode 100644 index 00000000..ca2895e3 --- /dev/null +++ b/audio/src/play_state.rs @@ -0,0 +1,9 @@ +#[derive(Copy, Clone, Eq, PartialEq)] +pub(crate) enum PlayState { + /// Not playing any audio. + NotPlaying, + /// Playing music. There are queued events. + Playing, + /// There are no more events. Audio is decaying. + Decaying, +} diff --git a/audio/src/player.rs b/audio/src/player.rs index a46b887d..c13a99b7 100644 --- a/audio/src/player.rs +++ b/audio/src/player.rs @@ -1,5 +1,7 @@ +use crate::decayer::Decayer; +use crate::play_state::PlayState; use crate::types::SharedSample; -use crate::{SharedMidiEventQueue, SharedSynth, SharedTimeState}; +use crate::{SharedMidiEventQueue, SharedPlayState, SharedSynth, SharedTimeState}; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; use cpal::*; @@ -22,6 +24,7 @@ impl Player { time_state: SharedTimeState, synth: SharedSynth, sample: SharedSample, + play_state: SharedPlayState, ) -> Option { // Get the host. let host = default_host(); @@ -52,6 +55,7 @@ impl Player { time_state, synth, sample, + play_state, ); Some(Self { _host: host, @@ -72,6 +76,7 @@ impl Player { time_state: SharedTimeState, synth: SharedSynth, sample: SharedSample, + play_state: SharedPlayState, ) -> Option { // Define the error callback. let err_callback = |err| println!("Stream error: {}", err); @@ -79,63 +84,107 @@ impl Player { let two_channels = channels == 2; let mut left = vec![0.0; 1]; let mut right = vec![0.0; 1]; + let mut decayer = Decayer::default(); // Define the data callback used by cpal. Move `stream_send` into the closure. let data_callback = move |output: &mut [f32], _: &OutputCallbackInfo| { - let mut time_state = time_state.lock(); - let mut midi_event_queue = midi_event_queue.lock(); - // There are no more events. Fill the buffer and advance time. - if midi_event_queue.is_empty() { - let len = output.len(); - - // Resize the buffers. - if len > left.len() { - left.resize(len, 0.0); - right.resize(len, 0.0); - } - - // Write the samples. - let mut synth = synth.lock(); - synth.write((left[0..len].as_mut(), right[0..len].as_mut())); - - // Stop time. - if time_state.time.is_some() { - time_state.music = false; - time_state.time = None; - } - } else { - // Iterate through the number of samples. - for frame in output.chunks_mut(channels) { - // We're playing music. Advance to the next events. - if let Some(time) = time_state.time { - // Dequeue events. - let events = midi_event_queue.dequeue(time); - // Send the MIDI events to the synth. - if !events.is_empty() { - let mut synth = synth.lock(); - for event in events { - if synth.send_event(event).is_ok() {} + let ps = *play_state.lock(); + match ps { + // Assume that there is no audio and do nothing. + PlayState::NotPlaying => (), + // Add decay. + PlayState::Decaying => { + let mut synth = synth.lock(); + // Write the decay block. + decayer.decay(&mut synth, output.len() / channels); + // Set the decay block. + if decayer.decaying { + for (frame, (left, right)) in output + .chunks_mut(channels) + .zip(decayer.left.iter().zip(decayer.right)) + { + // Add the sample. + // This is almost certainly more performant than the code in the `else` block. + if two_channels { + frame[0] = *left; + frame[1] = right; + } + // Add for more than one channel. This is slower. + else { + let channels = [*left, right]; + for (id, sample) in frame.iter_mut().enumerate() { + *sample = channels[id % 2]; + } } } - // Advance time by one sample. - time_state.time = Some(time + 1) } - // Get the next sample. - let mut synth = synth.lock(); - // Get the sample. - let (left, right) = synth.read_next(); + // Done decaying. + else { + let mut play_state = play_state.lock(); + *play_state = PlayState::NotPlaying; + } + } + // Playing music. + PlayState::Playing => { + let len = output.len(); + let mut time_state = time_state.lock(); + let mut midi_event_queue = midi_event_queue.lock(); + // There are no more events. Fill the buffer and advance time. + if midi_event_queue.is_empty() { + // Resize the buffers. + if len > left.len() { + left.resize(len, 0.0); + right.resize(len, 0.0); + } - // Add the sample. - // This is almost certainly more performant than the code in the `else` block. - if two_channels { - frame[0] = left; - frame[1] = right; + // Write the samples. + let mut synth = synth.lock(); + synth.write((left[0..len].as_mut(), right[0..len].as_mut())); + + // Stop time. + if time_state.time.is_some() { + time_state.music = false; + time_state.time = None; + let mut play_state = play_state.lock(); + *play_state = PlayState::Decaying; + } } - // Add for more than one channel. This is slower. + // There are MIDI events. else { - let channels = [left, right]; - for (id, sample) in frame.iter_mut().enumerate() { - *sample = channels[id % 2]; + // Iterate through the number of samples. + for frame in output.chunks_mut(channels) { + // We're playing music. Advance to the next events. + if let Some(time) = time_state.time { + // Dequeue events. + let events = midi_event_queue.dequeue(time); + // Send the MIDI events to the synth. + if !events.is_empty() { + let mut synth = synth.lock(); + for event in events { + if synth.send_event(event).is_ok() {} + } + } + // Advance time by one sample. + time_state.time = Some(time + 1) + } + // Get the next sample. + let mut synth = synth.lock(); + // Get the sample. + let (left, right) = synth.read_next(); + + // Add the sample. + // This is almost certainly more performant than the code in the `else` block. + if two_channels { + frame[0] = left; + frame[1] = right; + } + // Add for more than one channel. This is slower. + else { + let channels = [left, right]; + for (id, sample) in frame.iter_mut().enumerate() { + *sample = channels[id % 2]; + } + } } } } diff --git a/audio/src/types.rs b/audio/src/types.rs index 57ee1e8c..c463f0a3 100644 --- a/audio/src/types.rs +++ b/audio/src/types.rs @@ -1,5 +1,6 @@ use crate::export::ExportState; use crate::midi_event_queue::MidiEventQueue; +use crate::play_state::PlayState; use crate::{Command, TimeState}; use oxisynth::Synth; use parking_lot::Mutex; @@ -14,5 +15,6 @@ pub(crate) type AudioBuffer = [Vec; 2]; pub type SharedSynth = Arc>; pub type SharedExportState = Arc>; pub(crate) type SharedMidiEventQueue = Arc>; +pub(crate) type SharedPlayState = Arc>; pub(crate) type SharedTimeState = Arc>; pub(crate) type SharedSample = Arc>; diff --git a/render/src/export_panel.rs b/render/src/export_panel.rs index 167ac7e1..9aaa4bb2 100644 --- a/render/src/export_panel.rs +++ b/render/src/export_panel.rs @@ -71,7 +71,7 @@ impl Drawable for ExportPanel { }; renderer.text(&label, &ColorKey::FocusDefault); } - ExportState::AppendingSilence => { + ExportState::AppendingDecay => { renderer.text(&self.decaying_label, &ColorKey::FocusDefault); } ExportState::WritingToDisk => { diff --git a/text/src/tts.rs b/text/src/tts.rs index 453c54c7..3f97dc96 100644 --- a/text/src/tts.rs +++ b/text/src/tts.rs @@ -32,12 +32,9 @@ impl TTS { let callbacks = tts.supported_features().utterance_callbacks && !cfg!(target_os = "macos"); if callbacks { - let _ = tts - .on_utterance_begin(Some(Box::new(on_utterance_begin))); - let _ = tts - .on_utterance_end(Some(Box::new(on_utterance_end))); - let _ = tts - .on_utterance_stop(Some(Box::new(on_utterance_end))); + let _ = tts.on_utterance_begin(Some(Box::new(on_utterance_begin))); + let _ = tts.on_utterance_end(Some(Box::new(on_utterance_end))); + let _ = tts.on_utterance_stop(Some(Box::new(on_utterance_end))); } // Try to set the voice. if let Ok(voices) = tts.voices() { From 698c7761563a2218570cbd24e4fdfff7ce4590a1 Mon Sep 17 00:00:00 2001 From: Esther Alter Date: Tue, 14 Nov 2023 10:02:21 -0500 Subject: [PATCH 15/52] compiles!! --- audio/src/conn.rs | 48 ++++++++++++++++++++++----------------------- audio/src/player.rs | 1 + 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/audio/src/conn.rs b/audio/src/conn.rs index 4619e219..81105a6e 100644 --- a/audio/src/conn.rs +++ b/audio/src/conn.rs @@ -114,20 +114,17 @@ 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() { - let mut synth = self.synth.lock(); - synth.set_sample_rate(self.framerate); - let gain = track.gain as f32 / MAX_VOLUME as f32; - for note_on in note_ons.iter() { - if synth - .send_event(MidiEvent::NoteOn { + if !note_ons.is_empty() { + let mut synth = self.synth.lock(); + synth.set_sample_rate(self.framerate); + 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, - }) - .is_ok() - {} - } - if !note_ons.is_empty() { + }); + } // Play music. let mut play_state = self.play_state.lock(); *play_state = PlayState::Decaying; @@ -138,18 +135,18 @@ impl Conn { /// 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() { - let mut synth = self.synth.lock(); - synth.set_sample_rate(self.framerate); - for note_off in note_offs.iter() { - if synth - .send_event(MidiEvent::NoteOff { - channel: track.channel, - key: *note_off, - }) - .is_ok() - {} - } if !note_offs.is_empty() { + let mut synth = self.synth.lock(); + synth.set_sample_rate(self.framerate); + for note_off in note_offs.iter() { + if synth + .send_event(MidiEvent::NoteOff { + channel: track.channel, + key: *note_off, + }) + .is_ok() + {} + } // Play music. let mut play_state = self.play_state.lock(); *play_state = PlayState::Decaying; @@ -163,7 +160,9 @@ impl Conn { Command::LoadSoundFont { channel, path } => { match &self.soundfonts.get(path) { // We already loaded this font. - Some(_) => self.set_program_default(*channel, path), + Some(_) => { + self.set_program_default(*channel, path); + } // Load the font. None => match SoundFont::load(&mut File::open(path).unwrap()) { Ok(font) => { @@ -173,8 +172,8 @@ impl Conn { self.set_program_default(*channel, path); // Restore the other programs. let programs = self.state.programs.clone(); - let mut synth = self.synth.lock(); for program in programs.iter().filter(|p| p.0 != channel) { + let mut synth = self.synth.lock(); synth .program_select( *program.0, @@ -326,7 +325,6 @@ impl Conn { .iter() .position(|&p| p == preset) .unwrap(); - let synth = self.synth.lock(); let preset_name = synth.channel_preset(channel).unwrap().name().to_string(); let num_banks = soundfont.banks.len(); let num_presets = soundfont.banks[&bank].len(); diff --git a/audio/src/player.rs b/audio/src/player.rs index c13a99b7..70c8debf 100644 --- a/audio/src/player.rs +++ b/audio/src/player.rs @@ -68,6 +68,7 @@ impl Player { } /// Start running the stream. + #[allow(clippy::too_many_arguments)] fn run( channels: usize, device: Device, From 3b521e75c7c4176842b15cdead4ca7da1a9b6bd2 Mon Sep 17 00:00:00 2001 From: Esther Alter Date: Tue, 14 Nov 2023 10:59:26 -0500 Subject: [PATCH 16/52] minor improvement --- audio/src/decayer.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/audio/src/decayer.rs b/audio/src/decayer.rs index ccdbff5a..ffac28df 100644 --- a/audio/src/decayer.rs +++ b/audio/src/decayer.rs @@ -28,7 +28,7 @@ impl Decayer { // Write to the decay chunks. synth.write((self.left[0..len].as_mut(), self.right[0..len].as_mut())); // If the decay chunks are totally silent then we're not decaying anymore. - self.decaying = self.left.iter().any(|s| s.abs() > SILENCE) - || self.right.iter().any(|s| s.abs() > SILENCE); + self.decaying = self.left[0..len].iter().any(|s| s.abs() > SILENCE) + || self.right[0..len].iter().any(|s| s.abs() > SILENCE); } } From 7e465e8d1d64ad89fecdf4cfada8bda5e656c283 Mon Sep 17 00:00:00 2001 From: Esther Alter Date: Tue, 14 Nov 2023 11:34:24 -0500 Subject: [PATCH 17/52] silence --- audio/src/conn.rs | 10 ++++------ audio/src/player.rs | 11 +++++++---- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/audio/src/conn.rs b/audio/src/conn.rs index 81105a6e..2aba09eb 100644 --- a/audio/src/conn.rs +++ b/audio/src/conn.rs @@ -125,7 +125,7 @@ impl Conn { vel: (note_on[2] as f32 * gain) as u8, }); } - // Play music. + // Play audio. let mut play_state = self.play_state.lock(); *play_state = PlayState::Decaying; } @@ -139,15 +139,13 @@ impl Conn { let mut synth = self.synth.lock(); synth.set_sample_rate(self.framerate); for note_off in note_offs.iter() { - if synth + let _ = synth .send_event(MidiEvent::NoteOff { channel: track.channel, key: *note_off, - }) - .is_ok() - {} + }); } - // Play music. + // Let audio decay. let mut play_state = self.play_state.lock(); *play_state = PlayState::Decaying; } diff --git a/audio/src/player.rs b/audio/src/player.rs index 70c8debf..e3aa4b50 100644 --- a/audio/src/player.rs +++ b/audio/src/player.rs @@ -97,22 +97,23 @@ impl Player { PlayState::Decaying => { let mut synth = synth.lock(); // Write the decay block. - decayer.decay(&mut synth, output.len() / channels); + let len = output.len() / channels; + decayer.decay(&mut synth, len); // Set the decay block. if decayer.decaying { for (frame, (left, right)) in output .chunks_mut(channels) - .zip(decayer.left.iter().zip(decayer.right)) + .zip(decayer.left[0..len].iter().zip(&decayer.right[0..len])) { // Add the sample. // This is almost certainly more performant than the code in the `else` block. if two_channels { frame[0] = *left; - frame[1] = right; + frame[1] = *right; } // Add for more than one channel. This is slower. else { - let channels = [*left, right]; + let channels = [*left, *right]; for (id, sample) in frame.iter_mut().enumerate() { *sample = channels[id % 2]; } @@ -121,6 +122,8 @@ impl Player { } // 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; } From c47e86b4b826dd2e59451e816c33e5f8712d56e8 Mon Sep 17 00:00:00 2001 From: Esther Alter Date: Tue, 14 Nov 2023 11:41:44 -0500 Subject: [PATCH 18/52] hmm --- audio/src/decayer.rs | 16 ++++++++++++++-- audio/src/player.rs | 3 +-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/audio/src/decayer.rs b/audio/src/decayer.rs index ffac28df..1746935f 100644 --- a/audio/src/decayer.rs +++ b/audio/src/decayer.rs @@ -1,4 +1,5 @@ use oxisynth::Synth; +use crate::SharedSynth; /// Export this many bytes per decay chunk. pub(crate) const DECAY_CHUNK_SIZE: usize = 2048; @@ -27,8 +28,19 @@ impl Decayer { pub fn decay(&mut self, synth: &mut Synth, len: usize) { // Write to the decay chunks. synth.write((self.left[0..len].as_mut(), self.right[0..len].as_mut())); - // If the decay chunks are totally silent then we're not decaying anymore. + self.set_decaying(len); + } + + pub fn decay_shared(&mut self, synth: &SharedSynth, len: usize) { + for (left, right) in self.left[0..len].iter_mut().zip(self.right[0..len].as_mut()) { + let mut synth = synth.lock(); + (*left, *right) = synth.read_next(); + } + self.set_decaying(len); + } + + fn set_decaying(&mut self, len: usize) { self.decaying = self.left[0..len].iter().any(|s| s.abs() > SILENCE) - || self.right[0..len].iter().any(|s| s.abs() > SILENCE); + || self.right[0..len].iter().any(|s| s.abs() > SILENCE); } } diff --git a/audio/src/player.rs b/audio/src/player.rs index e3aa4b50..aa9b1131 100644 --- a/audio/src/player.rs +++ b/audio/src/player.rs @@ -95,10 +95,9 @@ impl Player { PlayState::NotPlaying => (), // Add decay. PlayState::Decaying => { - let mut synth = synth.lock(); // Write the decay block. let len = output.len() / channels; - decayer.decay(&mut synth, len); + decayer.decay_shared(&synth, len); // Set the decay block. if decayer.decaying { for (frame, (left, right)) in output From b7f5152f6182865b1c58569f1ae5d99857c2780c Mon Sep 17 00:00:00 2001 From: Esther Alter Date: Tue, 14 Nov 2023 11:56:59 -0500 Subject: [PATCH 19/52] I guess we don't need this?? --- audio/src/conn.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/audio/src/conn.rs b/audio/src/conn.rs index 2aba09eb..7d817e18 100644 --- a/audio/src/conn.rs +++ b/audio/src/conn.rs @@ -145,9 +145,6 @@ impl Conn { key: *note_off, }); } - // Let audio decay. - let mut play_state = self.play_state.lock(); - *play_state = PlayState::Decaying; } } } From 784c73aee4f7e71db7ed94b0f0c6a2eb8cfd70a8 Mon Sep 17 00:00:00 2001 From: Esther Alter Date: Tue, 14 Nov 2023 12:05:48 -0500 Subject: [PATCH 20/52] fixed initial gain --- audio/src/conn.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/audio/src/conn.rs b/audio/src/conn.rs index 7d817e18..0859abbb 100644 --- a/audio/src/conn.rs +++ b/audio/src/conn.rs @@ -66,12 +66,12 @@ impl Default for Conn { fn default() -> Self { // Set the synthesizer. let mut synth = Synth::default(); - synth.set_gain(MAX_VOLUME as f32); + synth.set_gain(1.0); let synth = Arc::new(Mutex::new(synth)); // Create other shared data. let time_state = Arc::new(Mutex::new(TimeState::default())); - let midi_event_queue = Arc::new(Mutex::new(MidiEventQueue::default())); + let midi_event_queue: Arc> = 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)); From 1a70ecd4d01c650451472b8cc6d96083b55ea9da Mon Sep 17 00:00:00 2001 From: Esther Alter Date: Tue, 14 Nov 2023 12:06:10 -0500 Subject: [PATCH 21/52] fmt --- audio/src/conn.rs | 13 +++++++------ audio/src/decayer.rs | 9 ++++++--- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/audio/src/conn.rs b/audio/src/conn.rs index 0859abbb..2cb5925b 100644 --- a/audio/src/conn.rs +++ b/audio/src/conn.rs @@ -71,7 +71,9 @@ impl Default for Conn { // Create other shared data. let time_state = Arc::new(Mutex::new(TimeState::default())); - let midi_event_queue: Arc> = Arc::new(Mutex::new(MidiEventQueue::default())); + let midi_event_queue: Arc< + parking_lot::lock_api::Mutex, + > = 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)); @@ -139,11 +141,10 @@ impl Conn { let mut synth = self.synth.lock(); synth.set_sample_rate(self.framerate); for note_off in note_offs.iter() { - let _ = synth - .send_event(MidiEvent::NoteOff { - channel: track.channel, - key: *note_off, - }); + let _ = synth.send_event(MidiEvent::NoteOff { + channel: track.channel, + key: *note_off, + }); } } } diff --git a/audio/src/decayer.rs b/audio/src/decayer.rs index 1746935f..481900b6 100644 --- a/audio/src/decayer.rs +++ b/audio/src/decayer.rs @@ -1,5 +1,5 @@ -use oxisynth::Synth; use crate::SharedSynth; +use oxisynth::Synth; /// Export this many bytes per decay chunk. pub(crate) const DECAY_CHUNK_SIZE: usize = 2048; @@ -32,7 +32,10 @@ impl Decayer { } pub fn decay_shared(&mut self, synth: &SharedSynth, len: usize) { - for (left, right) in self.left[0..len].iter_mut().zip(self.right[0..len].as_mut()) { + for (left, right) in self.left[0..len] + .iter_mut() + .zip(self.right[0..len].as_mut()) + { let mut synth = synth.lock(); (*left, *right) = synth.read_next(); } @@ -41,6 +44,6 @@ impl Decayer { fn set_decaying(&mut self, len: usize) { self.decaying = self.left[0..len].iter().any(|s| s.abs() > SILENCE) - || self.right[0..len].iter().any(|s| s.abs() > SILENCE); + || self.right[0..len].iter().any(|s| s.abs() > SILENCE); } } From d6915aba8612e88b46bd02c9803fa88b3cfa3fcf Mon Sep 17 00:00:00 2001 From: Esther Alter Date: Wed, 15 Nov 2023 09:02:15 -0500 Subject: [PATCH 22/52] we can play one note very well --- Cargo.lock | 14 ++-- Cargo.toml | 6 +- audio/src/conn.rs | 20 +++-- audio/src/midi_event_queue.rs | 4 - audio/src/player.rs | 149 ++++++++++++++++++++++------------ io/src/save.rs | 7 ++ 6 files changed, 123 insertions(+), 77 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c85aecbc..1bb21350 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -96,7 +96,7 @@ checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" [[package]] name = "audio" -version = "0.1.2" +version = "0.2.0" dependencies = [ "chrono", "common", @@ -227,7 +227,7 @@ checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" [[package]] name = "cacophony" -version = "0.1.2" +version = "0.2.0" dependencies = [ "audio", "clap", @@ -385,7 +385,7 @@ dependencies = [ [[package]] name = "common" -version = "0.1.2" +version = "0.2.0" dependencies = [ "directories", "hashbrown 0.13.2", @@ -930,7 +930,7 @@ dependencies = [ [[package]] name = "input" -version = "0.1.2" +version = "0.2.0" dependencies = [ "common", "hashbrown 0.13.2", @@ -955,7 +955,7 @@ dependencies = [ [[package]] name = "io" -version = "0.1.2" +version = "0.2.0" dependencies = [ "audio", "common", @@ -1785,7 +1785,7 @@ checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" [[package]] name = "render" -version = "0.1.2" +version = "0.2.0" dependencies = [ "audio", "colorgrad", @@ -2080,7 +2080,7 @@ dependencies = [ [[package]] name = "text" -version = "0.1.2" +version = "0.2.0" dependencies = [ "common", "csv", diff --git a/Cargo.toml b/Cargo.toml index b222f034..1ddbf898 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ members = ["audio", "common", "input", "io", "render", "text"] [workspace.package] -version = "0.1.2" +version = "0.2.0" authors = ["Esther Alter "] description = "A minimalist and ergonomic MIDI sequencer" documentation = "https://github.com/subalterngames/cacophony" @@ -83,7 +83,7 @@ speech_dispatcher_0_9 = ["text/speech_dispatcher_0_9"] [package] name = "cacophony" -version = "0.1.2" +version = "0.2.0" authors = ["Esther Alter "] description = "A minimalist and ergonomic MIDI sequencer" documentation = "https://github.com/subalterngames/cacophony" @@ -119,7 +119,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.2" +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/src/conn.rs b/audio/src/conn.rs index 2cb5925b..54ecfea1 100644 --- a/audio/src/conn.rs +++ b/audio/src/conn.rs @@ -169,15 +169,17 @@ impl Conn { // Restore the other programs. let programs = self.state.programs.clone(); for program in programs.iter().filter(|p| p.0 != channel) { - let mut synth = self.synth.lock(); - synth - .program_select( - *program.0, - self.soundfonts[&program.1.path].id, - program.1.bank, - program.1.preset, - ) - .unwrap(); + 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) => { diff --git a/audio/src/midi_event_queue.rs b/audio/src/midi_event_queue.rs index fc9b4de7..d7180235 100644 --- a/audio/src/midi_event_queue.rs +++ b/audio/src/midi_event_queue.rs @@ -44,8 +44,4 @@ impl MidiEventQueue { } midi_events } - - pub(crate) fn is_empty(&self) -> bool { - self.events.is_empty() - } } diff --git a/audio/src/player.rs b/audio/src/player.rs index aa9b1131..6b3e3e68 100644 --- a/audio/src/player.rs +++ b/audio/src/player.rs @@ -4,6 +4,7 @@ use crate::types::SharedSample; use crate::{SharedMidiEventQueue, SharedPlayState, SharedSynth, SharedTimeState}; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; use cpal::*; +use oxisynth::Synth; const ERROR_MESSAGE: &str = "Failed to create an audio output stream: "; @@ -83,8 +84,7 @@ impl Player { let err_callback = |err| println!("Stream error: {}", err); let two_channels = channels == 2; - let mut left = vec![0.0; 1]; - let mut right = vec![0.0; 1]; + let mut buffer = vec![0.0; 2]; let mut decayer = Decayer::default(); // Define the data callback used by cpal. Move `stream_send` into the closure. @@ -130,65 +130,84 @@ impl Player { // Playing music. PlayState::Playing => { let len = output.len(); + // Resize the buffers. + if len > buffer.len() { + buffer.resize(len, 0.0); + } let mut time_state = time_state.lock(); - let mut midi_event_queue = midi_event_queue.lock(); - // There are no more events. Fill the buffer and advance time. - if midi_event_queue.is_empty() { - // Resize the buffers. - if len > left.len() { - left.resize(len, 0.0); - right.resize(len, 0.0); - } - - // Write the samples. - let mut synth = synth.lock(); - synth.write((left[0..len].as_mut(), right[0..len].as_mut())); + // Get the next sample. + let mut synth = synth.lock(); + match time_state.time { + // We are playing music. + Some(time) => { + 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; + for frame in output.chunks_mut(channels) { + match midi_event_queue.get_next_time() { + Some(next_time) => { + // There are events on this frame. + if time == next_time { + // Dequeue events. + let events = midi_event_queue.dequeue(time); + // Send the MIDI events to the synth. + if !events.is_empty() { + for event in events { + if synth.send_event(event).is_ok() {} + } + } + } + // Get the sample. + let (left, right) = synth.read_next(); - // Stop time. - if time_state.time.is_some() { - time_state.music = false; - time_state.time = None; - let mut play_state = play_state.lock(); - *play_state = PlayState::Decaying; - } - } - // There are MIDI events. - else { - // Iterate through the number of samples. - for frame in output.chunks_mut(channels) { - // We're playing music. Advance to the next events. - if let Some(time) = time_state.time { - // Dequeue events. - let events = midi_event_queue.dequeue(time); - // Send the MIDI events to the synth. - if !events.is_empty() { - let mut synth = synth.lock(); - 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 { + frame[0] = left; + frame[1] = right; + } + // Add for more than one channel. This is slower. + else { + let channels = [left, right]; + for (id, sample) in frame.iter_mut().enumerate() { + *sample = channels[id % 2]; + } + } + // Advance time. + time_state.time = Some(time + 1); + } + // There are no more events. + None => { + time_state.music = false; + time_state.time = None; + begin_decay = true; + break; } } - // Advance time by one sample. - time_state.time = Some(time + 1) - } - // Get the next sample. - let mut synth = synth.lock(); - // Get the sample. - let (left, right) = synth.read_next(); - - // Add the sample. - // This is almost certainly more performant than the code in the `else` block. - if two_channels { - frame[0] = left; - frame[1] = right; } - // Add for more than one channel. This is slower. - else { - let channels = [left, right]; - for (id, sample) in frame.iter_mut().enumerate() { - *sample = channels[id % 2]; - } + if begin_decay { + Self::begin_decay( + buffer[0..buffer_len].as_mut(), + output, + channels, + two_channels, + &play_state, + &mut synth, + ); } } + None => { + let buffer_len = len / channels; + Self::begin_decay( + buffer[0..buffer_len].as_mut(), + output, + channels, + two_channels, + &play_state, + &mut synth, + ); + } } } } @@ -208,4 +227,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/io/src/save.rs b/io/src/save.rs index ac96ed3a..2acad7b6 100644 --- a/io/src/save.rs +++ b/io/src/save.rs @@ -22,6 +22,7 @@ pub(crate) struct Save { /// The exporter state. exporter: Exporter, /// The version string. + #[serde(default = "default_version")] version: String, } @@ -132,3 +133,9 @@ impl Save { } } } + +/// Returns the default version string. +/// For compatibility with pre-0.2.0. +fn default_version() -> String { + common::VERSION.to_string() +} From aa1de9a3301cb65935c40208877ee644015a8d8c Mon Sep 17 00:00:00 2001 From: Esther Alter Date: Wed, 15 Nov 2023 09:17:18 -0500 Subject: [PATCH 23/52] ok --- audio/src/conn.rs | 7 +------ audio/src/decayer.rs | 34 +++++++++++++++++----------------- audio/src/player.rs | 24 +++++++++++------------- 3 files changed, 29 insertions(+), 36 deletions(-) diff --git a/audio/src/conn.rs b/audio/src/conn.rs index 54ecfea1..5c049278 100644 --- a/audio/src/conn.rs +++ b/audio/src/conn.rs @@ -471,12 +471,7 @@ impl Conn { // Append decaying silence. Self::set_export_state(&export_state, ExportState::AppendingDecay); while decayer.decaying { - decayer.decay(&mut synth, DECAY_CHUNK_SIZE); - // Add the decay. - if decayer.decaying { - left.extend_from_slice(&decayer.left); - right.extend_from_slice(&decayer.right); - } + decayer.decay_two_channels(&mut left, &mut right, &mut synth, DECAY_CHUNK_SIZE); } // Convert. Self::set_export_state(&export_state, ExportState::WritingToDisk); diff --git a/audio/src/decayer.rs b/audio/src/decayer.rs index 481900b6..dce02331 100644 --- a/audio/src/decayer.rs +++ b/audio/src/decayer.rs @@ -2,48 +2,48 @@ use crate::SharedSynth; use oxisynth::Synth; /// Export this many bytes per decay chunk. -pub(crate) const DECAY_CHUNK_SIZE: usize = 2048; +pub(crate) 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 left: [f32; DECAY_CHUNK_SIZE], - pub right: [f32; DECAY_CHUNK_SIZE], + pub buffer: [f32; DECAY_CHUNK_SIZE], pub decaying: bool, } impl Default for Decayer { fn default() -> Self { Self { - left: [0.0; DECAY_CHUNK_SIZE], - right: [0.0; DECAY_CHUNK_SIZE], + buffer: [0.0; DECAY_CHUNK_SIZE], decaying: false, } } } impl Decayer { - pub fn decay(&mut self, synth: &mut Synth, len: usize) { - // Write to the decay chunks. - synth.write((self.left[0..len].as_mut(), self.right[0..len].as_mut())); - self.set_decaying(len); - } - pub fn decay_shared(&mut self, synth: &SharedSynth, len: usize) { - for (left, right) in self.left[0..len] - .iter_mut() - .zip(self.right[0..len].as_mut()) + for sample in self.buffer[0..len].chunks_mut(2) { let mut synth = synth.lock(); - (*left, *right) = synth.read_next(); + synth.write(sample); } self.set_decaying(len); } + pub fn decay_two_channels(&mut self, left: &mut Vec, right: &mut Vec, synth: &mut Synth, len: usize) { + let i = left.len(); + // Resize the output vectors. + let new_len = i + len; + left.resize(new_len, 0.0); + right.resize(new_len, 0.0); + // Write samples. + synth.write((left[i..new_len].as_mut(), right[i..new_len].as_mut())); + self.decaying = left[i..len].iter().any(|s| s.abs() > SILENCE) || right[i..len].iter().any(|s| s.abs() > SILENCE); + } + fn set_decaying(&mut self, len: usize) { - self.decaying = self.left[0..len].iter().any(|s| s.abs() > SILENCE) - || self.right[0..len].iter().any(|s| s.abs() > SILENCE); + self.decaying = self.buffer[0..len].iter().any(|s| s.abs() > SILENCE); } } diff --git a/audio/src/player.rs b/audio/src/player.rs index 6b3e3e68..f2622080 100644 --- a/audio/src/player.rs +++ b/audio/src/player.rs @@ -85,6 +85,7 @@ impl Player { 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. @@ -100,21 +101,19 @@ impl Player { decayer.decay_shared(&synth, len); // Set the decay block. if decayer.decaying { - for (frame, (left, right)) in output + for (out_frame, in_frame) in output .chunks_mut(channels) - .zip(decayer.left[0..len].iter().zip(&decayer.right[0..len])) + .zip(decayer.buffer[0..len].chunks_mut(2)) { // Add the sample. // This is almost certainly more performant than the code in the `else` block. if two_channels { - frame[0] = *left; - frame[1] = *right; + out_frame.copy_from_slice(in_frame); } // Add for more than one channel. This is slower. else { - let channels = [*left, *right]; - for (id, sample) in frame.iter_mut().enumerate() { - *sample = channels[id % 2]; + for (id, sample) in out_frame.iter_mut().enumerate() { + *sample = in_frame[id % 2]; } } } @@ -158,20 +157,19 @@ impl Player { } } } - // Get the sample. - let (left, right) = synth.read_next(); + // Add the sample. // This is almost certainly more performant than the code in the `else` block. if two_channels { - frame[0] = left; - frame[1] = right; + // Get the sample. + synth.write(frame); } // Add for more than one channel. This is slower. else { - let channels = [left, right]; + synth.write(sample_buffer.as_mut_slice()); for (id, sample) in frame.iter_mut().enumerate() { - *sample = channels[id % 2]; + *sample = sample_buffer[id % 2]; } } // Advance time. From 38a9a636e6131e1b0524d9d1412cacdee0c423c9 Mon Sep 17 00:00:00 2001 From: Esther Alter Date: Wed, 15 Nov 2023 11:01:15 -0500 Subject: [PATCH 24/52] playback sorta works --- audio/src/player.rs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/audio/src/player.rs b/audio/src/player.rs index f2622080..04a4db22 100644 --- a/audio/src/player.rs +++ b/audio/src/player.rs @@ -133,23 +133,24 @@ impl Player { if len > buffer.len() { buffer.resize(len, 0.0); } - let mut time_state = time_state.lock(); // Get the next sample. let mut synth = synth.lock(); - match time_state.time { + let time = Self::get_time(&time_state); + match time { // We are playing music. Some(time) => { 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 time == next_time { + if t == next_time { // Dequeue events. - let events = midi_event_queue.dequeue(time); + let events = midi_event_queue.dequeue(t); // Send the MIDI events to the synth. if !events.is_empty() { for event in events { @@ -157,8 +158,6 @@ impl Player { } } } - - // Add the sample. // This is almost certainly more performant than the code in the `else` block. if two_channels { @@ -173,10 +172,12 @@ impl Player { } } // Advance time. - time_state.time = Some(time + 1); + t += 1; + *time_state.lock().time.as_mut().unwrap() += 1; } // There are no more events. None => { + let mut time_state = time_state.lock(); time_state.music = false; time_state.time = None; begin_decay = true; @@ -226,6 +227,10 @@ impl Player { } } + fn get_time(time_state: &SharedTimeState) -> Option { + time_state.lock().time.clone() + } + fn begin_decay( buffer: &mut [f32], output: &mut [f32], From 511ecfac7e7f2c71924bc8bc6e159da8caac51a2 Mon Sep 17 00:00:00 2001 From: Esther Alter Date: Wed, 15 Nov 2023 11:08:43 -0500 Subject: [PATCH 25/52] ok --- audio/src/player.rs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/audio/src/player.rs b/audio/src/player.rs index 04a4db22..feff054f 100644 --- a/audio/src/player.rs +++ b/audio/src/player.rs @@ -173,18 +173,16 @@ impl Player { } // Advance time. t += 1; - *time_state.lock().time.as_mut().unwrap() += 1; } // There are no more events. None => { - let mut time_state = time_state.lock(); - time_state.music = false; - time_state.time = None; + Self::stop_time(&time_state); begin_decay = true; break; } } } + Self::set_time(&time_state, t); if begin_decay { Self::begin_decay( buffer[0..buffer_len].as_mut(), @@ -231,6 +229,19 @@ impl Player { time_state.lock().time.clone() } + fn set_time(time_state: &SharedTimeState, time: u64) { + let mut time_state = time_state.lock(); + if let Some(t0) = time_state.time.as_mut() { + *t0 = time + } + } + + fn stop_time(time_state: &SharedTimeState) { + let mut time_state = time_state.lock(); + time_state.music = false; + time_state.time = None; + } + fn begin_decay( buffer: &mut [f32], output: &mut [f32], From 3dc70724c911a7ab75aff8a64599b412b8c2bd87 Mon Sep 17 00:00:00 2001 From: Esther Alter Date: Wed, 15 Nov 2023 11:19:43 -0500 Subject: [PATCH 26/52] render playback time updates --- audio/src/conn.rs | 2 +- audio/src/decayer.rs | 14 +++++++--- audio/src/lib.rs | 8 +++--- audio/src/player.rs | 2 +- audio/src/synth_state.rs | 5 ---- audio/src/types.rs | 2 +- render/src/piano_roll_panel.rs | 28 +++++++++++-------- render/src/piano_roll_panel/multi_track.rs | 3 ++ render/src/piano_roll_panel/viewable_notes.rs | 23 +++++++++++---- 9 files changed, 55 insertions(+), 32 deletions(-) diff --git a/audio/src/conn.rs b/audio/src/conn.rs index 5c049278..02db58d9 100644 --- a/audio/src/conn.rs +++ b/audio/src/conn.rs @@ -55,7 +55,7 @@ pub struct Conn { pub sample: SharedSample, synth: SharedSynth, midi_event_queue: SharedMidiEventQueue, - time_state: SharedTimeState, + pub time_state: SharedTimeState, soundfonts: HashMap, pub state: SynthState, pub exporter: Exporter, diff --git a/audio/src/decayer.rs b/audio/src/decayer.rs index dce02331..9c752b26 100644 --- a/audio/src/decayer.rs +++ b/audio/src/decayer.rs @@ -24,15 +24,20 @@ impl Default for Decayer { impl Decayer { pub fn decay_shared(&mut self, synth: &SharedSynth, len: usize) { - for sample in self.buffer[0..len].chunks_mut(2) - { + 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, len: usize) { + pub fn decay_two_channels( + &mut self, + left: &mut Vec, + right: &mut Vec, + synth: &mut Synth, + len: usize, + ) { let i = left.len(); // Resize the output vectors. let new_len = i + len; @@ -40,7 +45,8 @@ impl Decayer { right.resize(new_len, 0.0); // Write samples. synth.write((left[i..new_len].as_mut(), right[i..new_len].as_mut())); - self.decaying = left[i..len].iter().any(|s| s.abs() > SILENCE) || right[i..len].iter().any(|s| s.abs() > SILENCE); + self.decaying = left[i..len].iter().any(|s| s.abs() > SILENCE) + || right[i..len].iter().any(|s| s.abs() > SILENCE); } fn set_decaying(&mut self, len: usize) { diff --git a/audio/src/lib.rs b/audio/src/lib.rs index 99423ff5..652b387e 100644 --- a/audio/src/lib.rs +++ b/audio/src/lib.rs @@ -40,9 +40,9 @@ 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, SharedMidiEventQueue, SharedPlayState, SharedTimeState, +pub use crate::time_state::TimeState; +pub(crate) use crate::types::{AudioBuffer, SharedMidiEventQueue, SharedPlayState}; +pub use crate::types::{ + AudioMessage, CommandsMessage, SharedExportState, SharedSynth, SharedTimeState, }; -pub use crate::types::{AudioMessage, CommandsMessage, SharedExportState, SharedSynth}; use player::Player; diff --git a/audio/src/player.rs b/audio/src/player.rs index feff054f..8eac30da 100644 --- a/audio/src/player.rs +++ b/audio/src/player.rs @@ -226,7 +226,7 @@ impl Player { } fn get_time(time_state: &SharedTimeState) -> Option { - time_state.lock().time.clone() + time_state.lock().time } fn set_time(time_state: &SharedTimeState, time: u64) { 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/types.rs b/audio/src/types.rs index c463f0a3..1d58889a 100644 --- a/audio/src/types.rs +++ b/audio/src/types.rs @@ -16,5 +16,5 @@ pub type SharedSynth = Arc>; pub type SharedExportState = Arc>; pub(crate) type SharedMidiEventQueue = Arc>; pub(crate) type SharedPlayState = Arc>; -pub(crate) type SharedTimeState = Arc>; +pub type SharedTimeState = Arc>; pub(crate) type SharedSample = Arc>; diff --git a/render/src/piano_roll_panel.rs b/render/src/piano_roll_panel.rs index 13aa7fde..2beb799e 100644 --- a/render/src/piano_roll_panel.rs +++ b/render/src/piano_roll_panel.rs @@ -1,4 +1,5 @@ use crate::panel::*; +use audio::{SharedTimeState, TimeState}; mod piano_roll_rows; use piano_roll_rows::PianoRollRows; mod multi_track; @@ -166,8 +167,9 @@ 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 { + let time_state = conn.time_state.lock(); + if time_state.music { + match time_state.time { Some(time) => { let time_ppq = state.time.samples_to_ppq(time, conn.framerate); // The time is in range @@ -189,6 +191,10 @@ impl PianoRollPanel { dt } } + + fn get_time_state(time_state: &SharedTimeState) -> TimeState { + *time_state.lock() + } } impl Drawable for PianoRollPanel { @@ -213,6 +219,7 @@ impl Drawable for PianoRollPanel { self.top_bar.update(state, renderer, text, focus); let dt = Self::get_view_dt(state, conn).map(U64orF32::from); + let time_state = Self::get_time_state(&conn.time_state); if state.view.single_track { // Piano roll rows. @@ -225,6 +232,7 @@ impl Drawable for PianoRollPanel { conn, focus, dt, + &time_state, ); // Draw the selection background. let selected = notes @@ -373,8 +381,8 @@ 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 { + if time_state.music { + if let Some(music_time) = time_state.time { let music_time_string = ppq_to_string(state.time.samples_to_ppq(music_time, conn.framerate)); let music_time_x = @@ -412,11 +420,9 @@ impl Drawable for PianoRollPanel { }; renderer.text(&dt_label, &Renderer::get_key_color(focus)); - if state.view.single_track { - } - // Multi-track. - else { - self.multi_track.update(dt, renderer, state, conn); + if !state.view.single_track { + self.multi_track + .update(dt, renderer, state, conn, &time_state); } // Draw time lines. @@ -437,8 +443,8 @@ 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 { + if time_state.music { + if let Some(music_time) = time_state.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( diff --git a/render/src/piano_roll_panel/multi_track.rs b/render/src/piano_roll_panel/multi_track.rs index e83bdc3b..db0ff648 100644 --- a/render/src/piano_roll_panel/multi_track.rs +++ b/render/src/piano_roll_panel/multi_track.rs @@ -1,6 +1,7 @@ use super::viewable_notes::{ViewableNote, ViewableNotes}; use crate::panel::*; use crate::{get_track_heights, Page}; +use audio::TimeState; use common::config::parse; use common::{U64orF32, MAX_NOTE, MIN_NOTE}; @@ -79,6 +80,7 @@ impl MultiTrack { renderer: &Renderer, state: &State, conn: &Conn, + time_state: &TimeState, ) { let focus = state.panels[state.focus.get()] == PanelType::PianoRoll; // Get the page. @@ -123,6 +125,7 @@ impl MultiTrack { focus, dt, DN, + time_state, ); // Draw the selection background. let selected = notes diff --git a/render/src/piano_roll_panel/viewable_notes.rs b/render/src/piano_roll_panel/viewable_notes.rs index d9741656..3957cd4c 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::TimeState; use common::*; /// A viewable note. @@ -36,6 +37,7 @@ impl<'a> ViewableNotes<'a> { /// - `conn` The audio conn. /// - `focus` If true, the piano roll panel has focus. /// - `dt` The time delta. + /// - `time_state` The audio time state. pub fn new( x: f32, w: f32, @@ -43,9 +45,20 @@ impl<'a> ViewableNotes<'a> { conn: &Conn, focus: bool, dt: [U64orF32; 2], + time_state: &TimeState, ) -> Self { match state.music.get_selected_track() { - Some(track) => Self::new_from_track(x, w, track, state, conn, focus, dt, state.view.dn), + Some(track) => Self::new_from_track( + x, + w, + track, + state, + conn, + focus, + dt, + state.view.dn, + time_state, + ), None => Self { pulses_per_pixel: Self::get_pulses_per_pixel(&dt, w), notes: vec![], @@ -63,6 +76,7 @@ impl<'a> ViewableNotes<'a> { /// - `focus` If true, the piano roll panel has focus. /// - `dt` The time delta. /// - `dn` The range of viewable note pitches. + /// - `time_state` The audio time state. #[allow(clippy::too_many_arguments)] pub fn new_from_track( x: f32, @@ -73,13 +87,12 @@ impl<'a> ViewableNotes<'a> { focus: bool, dt: [U64orF32; 2], dn: [u8; 2], + time_state: &TimeState, ) -> 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 + let playtime = match time_state.music { + true => time_state .time .map(|time| state.time.samples_to_ppq(time, conn.framerate)), false => None, From 9cdcfb587c81187054b4834a7572d399fbadb32e Mon Sep 17 00:00:00 2001 From: Esther Alter Date: Wed, 15 Nov 2023 12:13:30 -0500 Subject: [PATCH 27/52] fixed start time --- audio/src/conn.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/audio/src/conn.rs b/audio/src/conn.rs index 02db58d9..577a605b 100644 --- a/audio/src/conn.rs +++ b/audio/src/conn.rs @@ -242,7 +242,7 @@ impl Conn { let mut midi_event_queue = self.midi_event_queue.lock(); for track in state.music.midi_tracks.iter() { let gain = track.gain as f32 / MAX_VOLUME as f32; - for note in track.get_playback_notes(start) { + 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), From 034c25c8e0efd079f463967f07ca4690a7dd2355 Mon Sep 17 00:00:00 2001 From: Esther Alter Date: Wed, 15 Nov 2023 16:13:01 -0500 Subject: [PATCH 28/52] cleanup and comments in audio --- audio/src/conn.rs | 19 ++++++++++++++++--- audio/src/lib.rs | 25 +++++-------------------- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/audio/src/conn.rs b/audio/src/conn.rs index 577a605b..a6921a18 100644 --- a/audio/src/conn.rs +++ b/audio/src/conn.rs @@ -52,13 +52,28 @@ pub struct Conn { /// The audio player. This is here so we don't drop it. _player: Option, /// The most recent sample. + /// `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, + /// The time state. + /// The `Conn` sets the initial time. + /// The `Player` advances time while playing. + /// Other crates can read the time state. pub time_state: SharedTimeState, + /// 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. play_state: SharedPlayState, } @@ -71,9 +86,7 @@ impl Default for Conn { // Create other shared data. let time_state = Arc::new(Mutex::new(TimeState::default())); - let midi_event_queue: Arc< - parking_lot::lock_api::Mutex, - > = Arc::new(Mutex::new(MidiEventQueue::default())); + 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)); diff --git a/audio/src/lib.rs b/audio/src/lib.rs index 652b387e..9ed95916 100644 --- a/audio/src/lib.rs +++ b/audio/src/lib.rs @@ -1,27 +1,12 @@ //! 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; From a81eceebe05c1b93dc3358ae86682ff7028f584b Mon Sep 17 00:00:00 2001 From: Esther Alter Date: Thu, 16 Nov 2023 08:55:29 -0500 Subject: [PATCH 29/52] comments --- io/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/io/src/lib.rs b/io/src/lib.rs index 07a6dc0b..631256ab 100644 --- a/io/src/lib.rs +++ b/io/src/lib.rs @@ -5,11 +5,11 @@ //! 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. From 32b6b9c42a78907c4e14b9e643d05df4f19d0e4e Mon Sep 17 00:00:00 2001 From: Esther Alter Date: Thu, 16 Nov 2023 08:57:19 -0500 Subject: [PATCH 30/52] comment --- audio/src/conn.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/audio/src/conn.rs b/audio/src/conn.rs index a6921a18..8726b61b 100644 --- a/audio/src/conn.rs +++ b/audio/src/conn.rs @@ -163,6 +163,7 @@ impl Conn { } } + /// Execute a slice of commands sent from `io`. pub fn do_commands(&mut self, commands: &[Command]) { for command in commands.iter() { match command { From 314c28a93132fab2a20910d568abe7af4ae104f8 Mon Sep 17 00:00:00 2001 From: Esther Alter Date: Thu, 16 Nov 2023 15:31:56 -0500 Subject: [PATCH 31/52] minor --- io/src/lib.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/io/src/lib.rs b/io/src/lib.rs index 631256ab..5d4d05f4 100644 --- a/io/src/lib.rs +++ b/io/src/lib.rs @@ -62,8 +62,7 @@ const MAX_UNDOS: usize = 100; /// - 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, From dd7e4df14f5e8698ab7e9b7249ecc3f64a1b6554 Mon Sep 17 00:00:00 2001 From: Esther Alter Date: Fri, 17 Nov 2023 10:49:06 -0500 Subject: [PATCH 32/52] removed crossbeam --- Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 1ddbf898..797e3757 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" From 74abd16c30edbf6ac13d81c096455e9040a8efeb Mon Sep 17 00:00:00 2001 From: Esther Alter Date: Sat, 18 Nov 2023 16:05:08 -0500 Subject: [PATCH 33/52] fixed note playing --- audio/src/player.rs | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/audio/src/player.rs b/audio/src/player.rs index 8eac30da..d7c25b97 100644 --- a/audio/src/player.rs +++ b/audio/src/player.rs @@ -96,22 +96,19 @@ impl Player { PlayState::NotPlaying => (), // Add decay. PlayState::Decaying => { + let len = output.len(); // Write the decay block. - let len = output.len() / channels; decayer.decay_shared(&synth, len); // Set the decay block. if decayer.decaying { - for (out_frame, in_frame) in output - .chunks_mut(channels) - .zip(decayer.buffer[0..len].chunks_mut(2)) - { - // Add the sample. - // This is almost certainly more performant than the code in the `else` block. - if two_channels { - out_frame.copy_from_slice(in_frame); - } - // Add for more than one channel. This is slower. - else { + // 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]; } From f05a0454edc187eca37f1c2559decc29c076c0fd Mon Sep 17 00:00:00 2001 From: Esther Alter Date: Mon, 20 Nov 2023 09:27:50 -0500 Subject: [PATCH 34/52] Removed TimeState --- audio/src/conn.rs | 33 ++--- audio/src/lib.rs | 10 +- audio/src/play_state.rs | 6 +- audio/src/player.rs | 135 +++++++----------- audio/src/time_state.rs | 10 -- audio/src/types.rs | 7 +- render/src/piano_roll_panel.rs | 119 +++++++-------- render/src/piano_roll_panel/multi_track.rs | 3 - render/src/piano_roll_panel/viewable_notes.rs | 26 +--- 9 files changed, 126 insertions(+), 223 deletions(-) delete mode 100644 audio/src/time_state.rs diff --git a/audio/src/conn.rs b/audio/src/conn.rs index 8726b61b..a239b022 100644 --- a/audio/src/conn.rs +++ b/audio/src/conn.rs @@ -9,7 +9,7 @@ use crate::types::SharedPlayState; use crate::SharedExportState; use crate::{ midi_event_queue::MidiEventQueue, types::SharedSample, Command, Player, Program, - SharedMidiEventQueue, SharedSynth, SharedTimeState, SynthState, TimeState, + SharedMidiEventQueue, SharedSynth, SynthState, }; use common::open_file::Extension; use common::{MidiTrack, Music, PathsState, State, Time, MAX_VOLUME}; @@ -62,19 +62,14 @@ pub struct Conn { /// The `Conn` can add to this. /// The `Player` can read this and remove events. midi_event_queue: SharedMidiEventQueue, - /// The time state. - /// The `Conn` sets the initial time. - /// The `Player` advances time while playing. - /// Other crates can read the time state. - pub time_state: SharedTimeState, - /// A HasHmap of loaded SoundFonts. Key = The path to a .sf2 file. + /// 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. - play_state: SharedPlayState, + pub play_state: SharedPlayState, } impl Default for Conn { @@ -85,20 +80,17 @@ impl Default for Conn { let synth = Arc::new(Mutex::new(synth)); // Create other shared data. - let time_state = Arc::new(Mutex::new(TimeState::default())); 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_time_state = Arc::clone(&time_state); 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_time_state, player_synth, player_sample, player_play_state, @@ -116,7 +108,6 @@ impl Default for Conn { sample, synth, midi_event_queue, - time_state, soundfonts: HashMap::default(), state: SynthState::default(), exporter: Exporter::default(), @@ -229,11 +220,10 @@ impl Conn { /// Start to play music if music isn't playing. Stop music if music is playing. pub fn set_music(&mut self, state: &State) { - let music = self.time_state.lock().music; - if music { - self.stop_music(&state.music) - } else { - self.start_music(state) + let play_state = *self.play_state.lock(); + match play_state { + PlayState::NotPlaying => self.start_music(state), + _ => self.stop_music(&state.music), } } @@ -279,13 +269,9 @@ impl Conn { // Sort the events by start time. midi_event_queue.sort(); - // Set time itself. - let mut time_state = self.time_state.lock(); - time_state.music = true; - time_state.time = Some(start); // Play music. let mut play_state = self.play_state.lock(); - *play_state = PlayState::Playing; + *play_state = PlayState::Playing(start); } /// Stop ongoing music. @@ -304,9 +290,6 @@ impl Conn { .is_ok() {} } - let mut time_state = self.time_state.lock(); - time_state.music = false; - time_state.time = None; // Let the audio decay. let mut play_state = self.play_state.lock(); *play_state = PlayState::Decaying; diff --git a/audio/src/lib.rs b/audio/src/lib.rs index 9ed95916..3907c8d4 100644 --- a/audio/src/lib.rs +++ b/audio/src/lib.rs @@ -14,20 +14,16 @@ mod decayer; pub mod export; pub mod exporter; pub(crate) mod midi_event_queue; -mod play_state; +pub mod play_state; mod player; mod program; mod synth_state; -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; -pub use crate::time_state::TimeState; -pub(crate) use crate::types::{AudioBuffer, SharedMidiEventQueue, SharedPlayState}; -pub use crate::types::{ - AudioMessage, CommandsMessage, SharedExportState, SharedSynth, SharedTimeState, -}; +pub(crate) use crate::types::{AudioBuffer, SharedMidiEventQueue, SharedSynth}; +pub use crate::types::{AudioMessage, CommandsMessage, SharedExportState, SharedPlayState}; use player::Player; diff --git a/audio/src/play_state.rs b/audio/src/play_state.rs index ca2895e3..9eeee08f 100644 --- a/audio/src/play_state.rs +++ b/audio/src/play_state.rs @@ -1,9 +1,9 @@ #[derive(Copy, Clone, Eq, PartialEq)] -pub(crate) enum PlayState { +pub enum PlayState { /// Not playing any audio. NotPlaying, - /// Playing music. There are queued events. - Playing, + /// 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 d7c25b97..ecc3d2fc 100644 --- a/audio/src/player.rs +++ b/audio/src/player.rs @@ -1,7 +1,7 @@ use crate::decayer::Decayer; use crate::play_state::PlayState; use crate::types::SharedSample; -use crate::{SharedMidiEventQueue, SharedPlayState, SharedSynth, SharedTimeState}; +use crate::{SharedMidiEventQueue, SharedPlayState, SharedSynth}; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; use cpal::*; use oxisynth::Synth; @@ -22,7 +22,6 @@ pub(crate) struct Player { impl Player { pub(crate) fn new( midi_event_queue: SharedMidiEventQueue, - time_state: SharedTimeState, synth: SharedSynth, sample: SharedSample, play_state: SharedPlayState, @@ -53,7 +52,6 @@ impl Player { device, stream_config, midi_event_queue, - time_state, synth, sample, play_state, @@ -69,13 +67,11 @@ impl Player { } /// Start running the stream. - #[allow(clippy::too_many_arguments)] fn run( channels: usize, device: Device, stream_config: StreamConfig, midi_event_queue: SharedMidiEventQueue, - time_state: SharedTimeState, synth: SharedSynth, sample: SharedSample, play_state: SharedPlayState, @@ -124,7 +120,7 @@ impl Player { } } // Playing music. - PlayState::Playing => { + PlayState::Playing(time) => { let len = output.len(); // Resize the buffers. if len > buffer.len() { @@ -132,76 +128,60 @@ impl Player { } // Get the next sample. let mut synth = synth.lock(); - let time = Self::get_time(&time_state); - match time { - // We are playing music. - Some(time) => { - 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() {} - } - } + 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 => { - Self::stop_time(&time_state); - begin_decay = true; - break; + } + // 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; } - Self::set_time(&time_state, t); - if begin_decay { - Self::begin_decay( - buffer[0..buffer_len].as_mut(), - output, - channels, - two_channels, - &play_state, - &mut synth, - ); + // There are no more events. + None => { + begin_decay = true; + break; } } - None => { - let buffer_len = len / channels; - Self::begin_decay( - buffer[0..buffer_len].as_mut(), - output, - channels, - two_channels, - &play_state, - &mut synth, - ); - } + } + 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); } } } @@ -222,23 +202,6 @@ impl Player { } } - fn get_time(time_state: &SharedTimeState) -> Option { - time_state.lock().time - } - - fn set_time(time_state: &SharedTimeState, time: u64) { - let mut time_state = time_state.lock(); - if let Some(t0) = time_state.time.as_mut() { - *t0 = time - } - } - - fn stop_time(time_state: &SharedTimeState) { - let mut time_state = time_state.lock(); - time_state.music = false; - time_state.time = None; - } - fn begin_decay( buffer: &mut [f32], output: &mut [f32], 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/types.rs b/audio/src/types.rs index 1d58889a..edde0091 100644 --- a/audio/src/types.rs +++ b/audio/src/types.rs @@ -1,7 +1,7 @@ use crate::export::ExportState; use crate::midi_event_queue::MidiEventQueue; use crate::play_state::PlayState; -use crate::{Command, TimeState}; +use crate::Command; use oxisynth::Synth; use parking_lot::Mutex; use std::sync::Arc; @@ -12,9 +12,8 @@ pub type AudioMessage = (f32, f32); pub type CommandsMessage = Vec; /// Type alias for an audio buffer. pub(crate) type AudioBuffer = [Vec; 2]; -pub type SharedSynth = Arc>; +pub(crate) type SharedSynth = Arc>; pub type SharedExportState = Arc>; pub(crate) type SharedMidiEventQueue = Arc>; -pub(crate) type SharedPlayState = Arc>; -pub type SharedTimeState = Arc>; +pub type SharedPlayState = Arc>; pub(crate) type SharedSample = Arc>; diff --git a/render/src/piano_roll_panel.rs b/render/src/piano_roll_panel.rs index 2beb799e..4ad3bcd3 100644 --- a/render/src/piano_roll_panel.rs +++ b/render/src/piano_roll_panel.rs @@ -1,5 +1,6 @@ use crate::panel::*; -use audio::{SharedTimeState, TimeState}; +use audio::play_state::PlayState; +use audio::SharedPlayState; mod piano_roll_rows; use piano_roll_rows::PianoRollRows; mod multi_track; @@ -167,33 +168,29 @@ 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]]; - let time_state = conn.time_state.lock(); - if time_state.music { - match time_state.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_time_state(time_state: &SharedTimeState) -> TimeState { - *time_state.lock() + fn get_play_state(play_state: &SharedPlayState) -> PlayState { + *play_state.lock() } } @@ -219,7 +216,6 @@ impl Drawable for PianoRollPanel { self.top_bar.update(state, renderer, text, focus); let dt = Self::get_view_dt(state, conn).map(U64orF32::from); - let time_state = Self::get_time_state(&conn.time_state); if state.view.single_track { // Piano roll rows. @@ -232,7 +228,6 @@ impl Drawable for PianoRollPanel { conn, focus, dt, - &time_state, ); // Draw the selection background. let selected = notes @@ -381,18 +376,17 @@ impl Drawable for PianoRollPanel { position: [selection_x, self.time_y], }; // Current playback time. - if time_state.music { - if let Some(music_time) = time_state.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, @@ -421,8 +415,7 @@ impl Drawable for PianoRollPanel { renderer.text(&dt_label, &Renderer::get_key_color(focus)); if !state.view.single_track { - self.multi_track - .update(dt, renderer, state, conn, &time_state); + self.multi_track.update(dt, renderer, state, conn); } // Draw time lines. @@ -443,32 +436,30 @@ impl Drawable for PianoRollPanel { &dt, ); // Show where we are in the music. - if time_state.music { - if let Some(music_time) = time_state.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/multi_track.rs b/render/src/piano_roll_panel/multi_track.rs index db0ff648..e83bdc3b 100644 --- a/render/src/piano_roll_panel/multi_track.rs +++ b/render/src/piano_roll_panel/multi_track.rs @@ -1,7 +1,6 @@ use super::viewable_notes::{ViewableNote, ViewableNotes}; use crate::panel::*; use crate::{get_track_heights, Page}; -use audio::TimeState; use common::config::parse; use common::{U64orF32, MAX_NOTE, MIN_NOTE}; @@ -80,7 +79,6 @@ impl MultiTrack { renderer: &Renderer, state: &State, conn: &Conn, - time_state: &TimeState, ) { let focus = state.panels[state.focus.get()] == PanelType::PianoRoll; // Get the page. @@ -125,7 +123,6 @@ impl MultiTrack { focus, dt, DN, - time_state, ); // Draw the selection background. let selected = notes diff --git a/render/src/piano_roll_panel/viewable_notes.rs b/render/src/piano_roll_panel/viewable_notes.rs index 3957cd4c..21d40bca 100644 --- a/render/src/piano_roll_panel/viewable_notes.rs +++ b/render/src/piano_roll_panel/viewable_notes.rs @@ -1,5 +1,5 @@ use crate::panel::*; -use audio::TimeState; +use audio::play_state::PlayState; use common::*; /// A viewable note. @@ -37,7 +37,6 @@ impl<'a> ViewableNotes<'a> { /// - `conn` The audio conn. /// - `focus` If true, the piano roll panel has focus. /// - `dt` The time delta. - /// - `time_state` The audio time state. pub fn new( x: f32, w: f32, @@ -45,20 +44,9 @@ impl<'a> ViewableNotes<'a> { conn: &Conn, focus: bool, dt: [U64orF32; 2], - time_state: &TimeState, ) -> Self { match state.music.get_selected_track() { - Some(track) => Self::new_from_track( - x, - w, - track, - state, - conn, - focus, - dt, - state.view.dn, - time_state, - ), + Some(track) => Self::new_from_track(x, w, track, state, conn, focus, dt, state.view.dn), None => Self { pulses_per_pixel: Self::get_pulses_per_pixel(&dt, w), notes: vec![], @@ -76,7 +64,6 @@ impl<'a> ViewableNotes<'a> { /// - `focus` If true, the piano roll panel has focus. /// - `dt` The time delta. /// - `dn` The range of viewable note pitches. - /// - `time_state` The audio time state. #[allow(clippy::too_many_arguments)] pub fn new_from_track( x: f32, @@ -87,15 +74,12 @@ impl<'a> ViewableNotes<'a> { focus: bool, dt: [U64orF32; 2], dn: [u8; 2], - time_state: &TimeState, ) -> Self { let pulses_per_pixel = Self::get_pulses_per_pixel(&dt, w); // Get any notes being played. - let playtime = match time_state.music { - true => time_state - .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. From 0270df77d89abc67cafc1d99dc43ac0f268965de Mon Sep 17 00:00:00 2001 From: Esther Alter Date: Mon, 20 Nov 2023 09:33:55 -0500 Subject: [PATCH 35/52] It plays correctly!!! --- audio/src/conn.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/audio/src/conn.rs b/audio/src/conn.rs index a239b022..858bbaae 100644 --- a/audio/src/conn.rs +++ b/audio/src/conn.rs @@ -122,7 +122,6 @@ impl Conn { if let Some(track) = state.music.get_selected_track() { if !note_ons.is_empty() { let mut synth = self.synth.lock(); - synth.set_sample_rate(self.framerate); let gain = track.gain as f32 / MAX_VOLUME as f32; for note_on in note_ons.iter() { let _ = synth.send_event(MidiEvent::NoteOn { @@ -143,7 +142,6 @@ impl Conn { if let Some(track) = state.music.get_selected_track() { if !note_offs.is_empty() { let mut synth = self.synth.lock(); - synth.set_sample_rate(self.framerate); for note_off in note_offs.iter() { let _ = synth.send_event(MidiEvent::NoteOff { channel: track.channel, @@ -380,7 +378,17 @@ impl Conn { let synth = Arc::clone(&self.synth); let exporter = self.exporter.clone(); let path = paths_state.exports.get_path(); - spawn(move || Self::export(exportables, export_state, synth, exporter, path)); + let player_framerate = self.framerate; + spawn(move || { + Self::export( + exportables, + export_state, + synth, + exporter, + path, + player_framerate, + ) + }); } fn enqueue_track_events( @@ -429,6 +437,7 @@ impl Conn { synth: SharedSynth, exporter: Exporter, path: PathBuf, + player_framerate: f32, ) { let mut decayer = Decayer::default(); let extension: Extension = exporter.export_type.get().into(); @@ -506,6 +515,7 @@ impl Conn { 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. From 4a5de511aefff375a7e2258a3d92c88126c7c725 Mon Sep 17 00:00:00 2001 From: Esther Alter Date: Mon, 20 Nov 2023 10:13:36 -0500 Subject: [PATCH 36/52] fixed note end on export --- audio/src/conn.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/audio/src/conn.rs b/audio/src/conn.rs index f1e3eb23..5b0864b3 100644 --- a/audio/src/conn.rs +++ b/audio/src/conn.rs @@ -416,7 +416,7 @@ impl Conn { vel: (note.velocity as f32 * gain) as u8, }, ); - let end = time.ppq_to_samples(note.start, self.framerate); + let end = time.ppq_to_samples(note.end, self.framerate); // This is the last known event. if *t1 < end { *t1 = end; From bb86d8a59be400132ce1f12e8ac3bf1709bbb687 Mon Sep 17 00:00:00 2001 From: Esther Alter Date: Mon, 20 Nov 2023 10:21:08 -0500 Subject: [PATCH 37/52] fixed note end --- audio/src/conn.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/audio/src/conn.rs b/audio/src/conn.rs index 5b0864b3..0c2e8b6c 100644 --- a/audio/src/conn.rs +++ b/audio/src/conn.rs @@ -348,7 +348,6 @@ impl Conn { let mut events = MidiEventQueue::default(); let mut t1 = 0; 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 { @@ -364,7 +363,6 @@ impl Conn { let mut events = MidiEventQueue::default(); let mut t1 = 0; self.enqueue_track_events(track, &state.time, &mut events, &mut t1, gain); - events.sort(); // Add an exportable. exportables.push(Exportable { events, @@ -416,7 +414,7 @@ impl Conn { vel: (note.velocity as f32 * gain) as u8, }, ); - let end = time.ppq_to_samples(note.end, self.framerate); + let end = time.ppq_to_samples(note.start, self.framerate); // This is the last known event. if *t1 < end { *t1 = end; @@ -429,6 +427,7 @@ impl Conn { }, ); } + events.sort(); } fn export( From e85c8a9c006d92528c60e885ce086e210cfb7967 Mon Sep 17 00:00:00 2001 From: Esther Alter Date: Mon, 20 Nov 2023 10:21:20 -0500 Subject: [PATCH 38/52] oops --- audio/src/conn.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/audio/src/conn.rs b/audio/src/conn.rs index 0c2e8b6c..71dee7f4 100644 --- a/audio/src/conn.rs +++ b/audio/src/conn.rs @@ -414,7 +414,7 @@ impl Conn { vel: (note.velocity as f32 * gain) as u8, }, ); - let end = time.ppq_to_samples(note.start, self.framerate); + let end = time.ppq_to_samples(note.end, self.framerate); // This is the last known event. if *t1 < end { *t1 = end; From 8aaa5e02f8b9d8bed060ad893e3d6a24522d9546 Mon Sep 17 00:00:00 2001 From: Esther Alter Date: Mon, 20 Nov 2023 10:28:31 -0500 Subject: [PATCH 39/52] fixed framerate and a crash --- audio/src/conn.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/audio/src/conn.rs b/audio/src/conn.rs index 71dee7f4..07c33db1 100644 --- a/audio/src/conn.rs +++ b/audio/src/conn.rs @@ -404,17 +404,18 @@ impl Conn { channel: track.channel, }, ); + let framerate = self.exporter.framerate.get_f(); for note in track.notes.iter() { // Note-on. events.enqueue( - time.ppq_to_samples(note.start, self.framerate), + 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, self.framerate); + let end = time.ppq_to_samples(note.end, framerate); // This is the last known event. if *t1 < end { *t1 = end; @@ -480,9 +481,9 @@ impl Conn { } // Convert. Self::set_export_state(&export_state, ExportState::WritingToDisk); - let suffix = exportable.suffix.clone().unwrap(); - let path = if exporter.multi_file { - path.parent() + let path = match &exportable.suffix { + Some(suffix) => { + path.parent() .unwrap() .join(format!( "{}_{}{}", @@ -491,8 +492,8 @@ impl Conn { extension.to_str(true) )) .to_path_buf() - } else { - path.clone() + } + None => path.clone() }; let audio = [left, right]; match &exporter.export_type.get() { From a814451bfc379f3767f21db55aabe30858b89cb4 Mon Sep 17 00:00:00 2001 From: Esther Alter Date: Mon, 20 Nov 2023 10:33:30 -0500 Subject: [PATCH 40/52] Fixed music export --- audio/src/conn.rs | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/audio/src/conn.rs b/audio/src/conn.rs index 07c33db1..548b7a4b 100644 --- a/audio/src/conn.rs +++ b/audio/src/conn.rs @@ -359,17 +359,17 @@ impl Conn { } // Export all tracks combined. else { + let mut t1 = 0; + let mut events = MidiEventQueue::default(); for track in tracks { - let mut events = MidiEventQueue::default(); - let mut t1 = 0; self.enqueue_track_events(track, &state.time, &mut events, &mut t1, gain); - // Add an exportable. - exportables.push(Exportable { - events, - total_samples: t1, - suffix: None, - }); } + // Add an exportable. + exportables.push(Exportable { + events, + total_samples: t1, + suffix: None, + }); } let export_state = Arc::clone(&self.export_state); @@ -481,19 +481,19 @@ impl Conn { } // 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() + Some(suffix) => path + .parent() .unwrap() - .join(format!( - "{}_{}{}", - path.file_stem().unwrap().to_str().unwrap(), - suffix, - extension.to_str(true) - )) - .to_path_buf() - } - None => path.clone() + .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() { From efc229fc14930e452f0d9d78357c9dcca1497042 Mon Sep 17 00:00:00 2001 From: Esther Alter Date: Mon, 20 Nov 2023 15:14:42 -0500 Subject: [PATCH 41/52] minor fixes. export still doesn't work... --- audio/src/conn.rs | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/audio/src/conn.rs b/audio/src/conn.rs index 548b7a4b..2eba7f6d 100644 --- a/audio/src/conn.rs +++ b/audio/src/conn.rs @@ -337,7 +337,6 @@ impl Conn { } pub fn start_export(&mut self, state: &State, paths_state: &PathsState) { - let gain = self.state.gain as f32 / MAX_VOLUME as f32; let mut exportables = vec![]; let tracks = state.music.get_playable_tracks(); self.set_export_framerate(); @@ -347,7 +346,9 @@ impl Conn { for track in tracks { let mut events = MidiEventQueue::default(); let mut t1 = 0; + let gain = track.gain as f32 / MAX_VOLUME as f32; 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 { @@ -362,8 +363,10 @@ impl Conn { let mut t1 = 0; let mut events = MidiEventQueue::default(); for track in tracks { + let gain = track.gain as f32 / MAX_VOLUME as f32; self.enqueue_track_events(track, &state.time, &mut events, &mut t1, gain); } + events.sort(); // Add an exportable. exportables.push(Exportable { events, @@ -428,7 +431,6 @@ impl Conn { }, ); } - events.sort(); } fn export( @@ -456,17 +458,9 @@ impl Conn { for event in exportable.events.dequeue(t).iter() { if synth.send_event(*event).is_ok() {} } - // Write a sample. - if t0 == t { - let sample = synth.read_next(); - let t = t as usize; - left[t] = sample.0; - right[t] = sample.1; - } - // Write a block of samples. - else { - let t0u = t0 as usize; - let t1u = t as usize; + let t0u = t0 as usize; + let t1u = t as usize; + if t0 < t { synth.write((left[t0u..t1u].as_mut(), right[t0u..t1u].as_mut())); } // Set the export state. From 1b933bd844faf5ffa9c15431619240a816d4120a Mon Sep 17 00:00:00 2001 From: Esther Alter Date: Mon, 20 Nov 2023 15:40:39 -0500 Subject: [PATCH 42/52] fixed some bugs --- audio/src/conn.rs | 24 +++++------------------- audio/src/midi_event_queue.rs | 11 +++-------- 2 files changed, 8 insertions(+), 27 deletions(-) diff --git a/audio/src/conn.rs b/audio/src/conn.rs index 2eba7f6d..efb36625 100644 --- a/audio/src/conn.rs +++ b/audio/src/conn.rs @@ -242,7 +242,7 @@ impl Conn { // Enqueue note events. let mut midi_event_queue = self.midi_event_queue.lock(); - for track in state.music.midi_tracks.iter() { + for track in state.music.get_playable_tracks().iter() { let gain = track.gain as f32 / MAX_VOLUME as f32; for note in track.get_playback_notes(state.time.playback) { // Note-on event. @@ -400,13 +400,6 @@ impl Conn { t1: &mut u64, gain: f32, ) { - // Turn off the sound. - events.enqueue( - 0, - MidiEvent::AllSoundOff { - channel: track.channel, - }, - ); let framerate = self.exporter.framerate.get_f(); for note in track.notes.iter() { // Note-on. @@ -451,22 +444,15 @@ impl Conn { // Set the initial wav export state. Self::set_export_state_wav(exportable, &export_state, 0); let mut synth = synth.lock(); - let mut t0 = 0; - // Process each event. - while let Some(t) = exportable.events.get_next_time() { + for t in 0..total_samples { // Get and send each event at this time. for event in exportable.events.dequeue(t).iter() { - if synth.send_event(*event).is_ok() {} - } - let t0u = t0 as usize; - let t1u = t as usize; - if t0 < t { - synth.write((left[t0u..t1u].as_mut(), right[t0u..t1u].as_mut())); + let _ = synth.send_event(*event); } // Set the export state. Self::set_export_state_wav(exportable, &export_state, t); - // Set the start time. - t0 = t; + let t = t as usize; + (left[t], right[t]) = synth.read_next(); } // Append decaying silence. Self::set_export_state(&export_state, ExportState::AppendingDecay); diff --git a/audio/src/midi_event_queue.rs b/audio/src/midi_event_queue.rs index d7180235..5e2b2220 100644 --- a/audio/src/midi_event_queue.rs +++ b/audio/src/midi_event_queue.rs @@ -33,14 +33,9 @@ impl MidiEventQueue { /// Dequeue any events that start at `time`. pub(crate) fn dequeue(&mut self, time: u64) -> Vec { - let midi_events = self - .events - .iter() - .filter(|e| e.time == time) - .map(|e| e.event) - .collect::>(); - if !midi_events.is_empty() { - self.events.retain(|e| e.time != time); + 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 } From 0e3e57300075a6a75a641e3a056a28135f84f8f1 Mon Sep 17 00:00:00 2001 From: Esther Alter Date: Mon, 20 Nov 2023 16:29:59 -0500 Subject: [PATCH 43/52] Fixed decayer --- audio/src/conn.rs | 10 +++++----- audio/src/decayer.rs | 18 ++++++++---------- audio/src/play_state.rs | 2 +- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/audio/src/conn.rs b/audio/src/conn.rs index efb36625..84006466 100644 --- a/audio/src/conn.rs +++ b/audio/src/conn.rs @@ -1,7 +1,4 @@ -use std::sync::Arc; -use std::thread::spawn; - -use crate::decayer::{Decayer, DECAY_CHUNK_SIZE}; +use crate::decayer::Decayer; use crate::export::{ExportState, ExportType, Exportable, MultiFileSuffix}; use crate::exporter::Exporter; use crate::play_state::PlayState; @@ -18,6 +15,8 @@ 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 { @@ -456,8 +455,9 @@ impl Conn { } // 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, DECAY_CHUNK_SIZE); + decayer.decay_two_channels(&mut left, &mut right, &mut synth); } // Convert. Self::set_export_state(&export_state, ExportState::WritingToDisk); diff --git a/audio/src/decayer.rs b/audio/src/decayer.rs index 9c752b26..515f3420 100644 --- a/audio/src/decayer.rs +++ b/audio/src/decayer.rs @@ -2,7 +2,7 @@ use crate::SharedSynth; use oxisynth::Synth; /// Export this many bytes per decay chunk. -pub(crate) const DECAY_CHUNK_SIZE: usize = 4096; +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; @@ -10,6 +10,7 @@ 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, } @@ -17,6 +18,7 @@ impl Default for Decayer { fn default() -> Self { Self { buffer: [0.0; DECAY_CHUNK_SIZE], + buffer_1: [0.0; DECAY_CHUNK_SIZE], decaying: false, } } @@ -36,17 +38,13 @@ impl Decayer { left: &mut Vec, right: &mut Vec, synth: &mut Synth, - len: usize, ) { - let i = left.len(); - // Resize the output vectors. - let new_len = i + len; - left.resize(new_len, 0.0); - right.resize(new_len, 0.0); // Write samples. - synth.write((left[i..new_len].as_mut(), right[i..new_len].as_mut())); - self.decaying = left[i..len].iter().any(|s| s.abs() > SILENCE) - || right[i..len].iter().any(|s| s.abs() > SILENCE); + 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) { diff --git a/audio/src/play_state.rs b/audio/src/play_state.rs index 9eeee08f..55f2c144 100644 --- a/audio/src/play_state.rs +++ b/audio/src/play_state.rs @@ -1,4 +1,4 @@ -#[derive(Copy, Clone, Eq, PartialEq)] +#[derive(Copy, Clone, Eq, PartialEq, Debug)] pub enum PlayState { /// Not playing any audio. NotPlaying, From e054d7fff1ffd806f224f7df5bb53796a3019ad6 Mon Sep 17 00:00:00 2001 From: Esther Alter Date: Tue, 21 Nov 2023 11:43:21 -0500 Subject: [PATCH 44/52] fixed export order --- audio/src/midi_event_queue.rs | 2 +- audio/src/timed_midi_event.rs | 58 +++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/audio/src/midi_event_queue.rs b/audio/src/midi_event_queue.rs index 5e2b2220..17fd99cd 100644 --- a/audio/src/midi_event_queue.rs +++ b/audio/src/midi_event_queue.rs @@ -28,7 +28,7 @@ impl MidiEventQueue { /// Sort the list of events by start time. pub(crate) fn sort(&mut self) { - self.events.sort_by(|a, b| a.time.cmp(&b.time)) + self.events.sort() } /// Dequeue any events that start at `time`. diff --git a/audio/src/timed_midi_event.rs b/audio/src/timed_midi_event.rs index 988c97e9..1a57990c 100644 --- a/audio/src/timed_midi_event.rs +++ b/audio/src/timed_midi_event.rs @@ -1,4 +1,5 @@ use oxisynth::MidiEvent; +use std::cmp::Ordering; /// A MIDI event with a start time. #[derive(Copy, Clone, Eq, PartialEq)] @@ -8,3 +9,60 @@ pub(crate) struct TimedMidiEvent { /// 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)) + } +} From 68307a90d492ce6dac94f88bc0c47d21a51c973a Mon Sep 17 00:00:00 2001 From: Esther Alter Date: Tue, 21 Nov 2023 12:45:07 -0500 Subject: [PATCH 45/52] everything looks good --- audio/src/conn.rs | 7 +++---- common/src/midi_track.rs | 9 +++++++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/audio/src/conn.rs b/audio/src/conn.rs index 84006466..65f5db2e 100644 --- a/audio/src/conn.rs +++ b/audio/src/conn.rs @@ -242,7 +242,6 @@ impl Conn { // Enqueue note events. let mut midi_event_queue = self.midi_event_queue.lock(); for track in state.music.get_playable_tracks().iter() { - let gain = track.gain as f32 / MAX_VOLUME as f32; for note in track.get_playback_notes(state.time.playback) { // Note-on event. midi_event_queue.enqueue( @@ -250,7 +249,7 @@ impl Conn { MidiEvent::NoteOn { channel: track.channel, key: note.note, - vel: (note.velocity as f32 * gain) as u8, + vel: note.velocity, }, ); // Note-off event. @@ -345,7 +344,7 @@ impl Conn { for track in tracks { let mut events = MidiEventQueue::default(); let mut t1 = 0; - let gain = track.gain as f32 / MAX_VOLUME as f32; + 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)); @@ -362,7 +361,7 @@ impl Conn { let mut t1 = 0; let mut events = MidiEventQueue::default(); for track in tracks { - let gain = track.gain as f32 / MAX_VOLUME as f32; + let gain = track.get_gain_f(); self.enqueue_track_events(track, &state.time, &mut events, &mut t1, gain); } events.sort(); diff --git a/common/src/midi_track.rs b/common/src/midi_track.rs index 026eb11e..0d9dd107 100644 --- a/common/src/midi_track.rs +++ b/common/src/midi_track.rs @@ -32,13 +32,18 @@ impl MidiTrack { 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.gain as f64 / MAX_VOLUME as f64; + 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 f64 * gain) as u8; + n1.velocity = (n1.velocity as f32 * gain) as u8; notes.push(n1); } notes.sort(); From c38b894aac0af46c57710fa9e9343ba111111195 Mon Sep 17 00:00:00 2001 From: Esther Alter Date: Tue, 21 Nov 2023 12:59:59 -0500 Subject: [PATCH 46/52] changelog --- changelog.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/changelog.md b/changelog.md index c5346a14..7e0146d1 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,11 @@ +# 0.2.x + +## 0.2.0 + +Reduced idle CPU usage by around 50% and CPU usage while playing music by around 16%. I think I sped up export time too but I haven't properly benchmarked it. + +*(Backend notes)* The problem was that the `Synthesizer` struct (which no longer exists) ran a very fast infinite loop to send samples to `Player`. 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 but the most noticeable one is that `Exporter` is no longer shared (there is no `Arc>` anywhere in the code) and can instead by called like this: `conn.exporter`. A lot of code through Cacophony referenced an `Arc>`, so there's a bit of refactoring pretty much everywhere in the codebase. + # 0.1.x ## 0.1.4 From 9575e4c4bb30828658a8f9f827ce25e1d96bc0ef Mon Sep 17 00:00:00 2001 From: Esther Alter Date: Tue, 21 Nov 2023 16:47:35 -0500 Subject: [PATCH 47/52] revised changelog --- changelog.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/changelog.md b/changelog.md index 7e0146d1..9f53c932 100644 --- a/changelog.md +++ b/changelog.md @@ -2,9 +2,11 @@ ## 0.2.0 -Reduced idle CPU usage by around 50% and CPU usage while playing music by around 16%. I think I sped up export time too but I haven't properly benchmarked it. +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. -*(Backend notes)* The problem was that the `Synthesizer` struct (which no longer exists) ran a very fast infinite loop to send samples to `Player`. 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 but the most noticeable one is that `Exporter` is no longer shared (there is no `Arc>` anywhere in the code) and can instead by called like this: `conn.exporter`. A lot of code through Cacophony referenced an `Arc>`, so there's a bit of refactoring pretty much everywhere in the codebase. +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 From f57d46e862a0a08bcd3fc1a689c23f3cef8486b7 Mon Sep 17 00:00:00 2001 From: Esther Alter Date: Wed, 22 Nov 2023 09:30:46 -0500 Subject: [PATCH 48/52] Update audio/src/conn.rs Co-authored-by: Thomas Versteeg --- audio/src/conn.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/audio/src/conn.rs b/audio/src/conn.rs index 65f5db2e..ba092f38 100644 --- a/audio/src/conn.rs +++ b/audio/src/conn.rs @@ -28,7 +28,7 @@ struct SoundFontBanks { impl SoundFontBanks { pub fn new(font: SoundFont, synth: &mut SharedSynth) -> Self { let mut banks: HashMap> = HashMap::new(); - (0u32..129u32).for_each(|b| { + (0u32..=128u32).for_each(|b| { let presets: Vec = (0u8..128) .filter(|p| font.preset(b, *p).is_some()) .collect(); From 4e4070f8e18a97a9d19ab57ae4180f3a80699377 Mon Sep 17 00:00:00 2001 From: Esther Alter Date: Wed, 22 Nov 2023 09:34:09 -0500 Subject: [PATCH 49/52] Update audio/src/conn.rs Co-authored-by: Thomas Versteeg --- audio/src/conn.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/audio/src/conn.rs b/audio/src/conn.rs index ba092f38..6946bd65 100644 --- a/audio/src/conn.rs +++ b/audio/src/conn.rs @@ -238,6 +238,7 @@ impl Conn { // 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(); From d3a32f69c32497b12a4e0bf426f077ad60a95364 Mon Sep 17 00:00:00 2001 From: Esther Alter Date: Wed, 22 Nov 2023 09:34:19 -0500 Subject: [PATCH 50/52] Update audio/src/conn.rs Co-authored-by: Thomas Versteeg --- audio/src/conn.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/audio/src/conn.rs b/audio/src/conn.rs index 6946bd65..81a9ad69 100644 --- a/audio/src/conn.rs +++ b/audio/src/conn.rs @@ -287,6 +287,7 @@ impl Conn { .is_ok() {} } + drop(synth); // Let the audio decay. let mut play_state = self.play_state.lock(); *play_state = PlayState::Decaying; From 50fa091faaa2013965745a5ebf4dbb4b75346901 Mon Sep 17 00:00:00 2001 From: Esther Alter Date: Wed, 22 Nov 2023 09:35:06 -0500 Subject: [PATCH 51/52] drop(midi_event_queue) --- audio/src/conn.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/audio/src/conn.rs b/audio/src/conn.rs index 81a9ad69..e4921c27 100644 --- a/audio/src/conn.rs +++ b/audio/src/conn.rs @@ -265,6 +265,7 @@ impl Conn { } // Sort the events by start time. midi_event_queue.sort(); + drop(midi_event_queue); // Play music. let mut play_state = self.play_state.lock(); From 89fe0ac45f7c68b5d270103395527fdf2ed4c392 Mon Sep 17 00:00:00 2001 From: Seth Alter Date: Thu, 23 Nov 2023 13:43:55 -0500 Subject: [PATCH 52/52] tiny optimization --- render/src/main_menu.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/render/src/main_menu.rs b/render/src/main_menu.rs index acd3d5d9..70aff6f6 100644 --- a/render/src/main_menu.rs +++ b/render/src/main_menu.rs @@ -280,8 +280,8 @@ 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. - let sample = *conn.sample.lock(); 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); }