Skip to content

Commit

Permalink
Merge pull request #76 from Leif-Rydenfalk/main
Browse files Browse the repository at this point in the history
Synth example fix + key released events
  • Loading branch information
wtholliday authored Jan 30, 2025
2 parents a4fea74 + acde643 commit 5401b21
Show file tree
Hide file tree
Showing 11 changed files with 525 additions and 8,750 deletions.
8 changes: 5 additions & 3 deletions examples/synth/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ publish = false

[dependencies]
rui = { workspace = true }
palette = "0.7.6"
enterpolation = "0.2"
rodio = { version = "0.20.1", default-features = false, features = ["symphonia-all"] }
# palette = "0.7.6"
# enterpolation = "0.2"
rodio = { version = "0.20.1", default-features = false, features = [
"symphonia-all",
] }
4 changes: 3 additions & 1 deletion examples/synth/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,13 @@ fn main() {

let mut synth = synth.lock().unwrap();

// let notes = note + 1 + 3 + 5;

// Get the frequency of the note.
let frequency: MidiFrequency = note.frequency();

// Create an audio source for the note.
let audio_source = Oscillator::sawtooth_wave(frequency).amplify(1.0);
let audio_source = Oscillator::sawtooth(frequency).amplify(1.0);

// Get the note id (u8) if you need it. 0 is the lowest note. 127 is the highest note.
let source_id: MidiNoteId = note.id();
Expand Down
381 changes: 180 additions & 201 deletions examples/synth/src/midi_keyboard.rs

Large diffs are not rendered by default.

82 changes: 52 additions & 30 deletions examples/synth/src/synth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,14 @@ use rodio::Sink;
mod oscillator;
pub use oscillator::Oscillator;

mod osc;
use osc::AnalogOsc;

// The envelope state struct
struct EnvelopeState {
envelope: Envelope,
start_time: Instant,
is_releasing: bool,
release_start_time: Option<Instant>,
release_start_volume: Option<f32>, // Track starting volume for release
}

// The envelope struct
struct Envelope {
attack: f32,
decay: f32,
Expand All @@ -45,9 +41,6 @@ pub struct Synth {

impl Synth {
pub fn new(stream_handle: rodio::OutputStreamHandle) -> Synth {
// let (_stream, stream_handle) = rodio::OutputStream::try_default().unwrap();
// For some reason the above code would fail if it was in the new function

Synth {
audio_sinks: HashMap::new(),
envelope_states: HashMap::new(),
Expand All @@ -59,12 +52,13 @@ impl Synth {
let sink = Sink::try_new(&self.stream_handle).expect("Failed to create sink");
sink.append(audio_source);

let envelope = Envelope::new(0.1, 0.2, 0.7, 1.3); // example envelope
let envelope = Envelope::new(0.8, 0.2, 0.7, 1.3);
let envelope_state = EnvelopeState {
envelope,
start_time: Instant::now(),
is_releasing: false,
release_start_time: None,
release_start_volume: None,
};

self.audio_sinks.insert(source_id, sink);
Expand All @@ -73,48 +67,76 @@ impl Synth {

pub fn release_source(&mut self, source_id: u8) {
if let Some(envelope_state) = self.envelope_states.get_mut(&source_id) {
let now = Instant::now();
let elapsed = now.duration_since(envelope_state.start_time).as_secs_f32();
let envelope = &envelope_state.envelope;

// Calculate current volume at release time
let current_volume = if elapsed < envelope.attack {
// Attack phase
elapsed / envelope.attack
} else if elapsed < envelope.attack + envelope.decay {
// Decay phase
1.0 - (elapsed - envelope.attack) / envelope.decay * (1.0 - envelope.sustain)
} else {
// Sustain phase
envelope.sustain
};

envelope_state.is_releasing = true;
envelope_state.release_start_time = Some(Instant::now());
envelope_state.release_start_time = Some(now);
envelope_state.release_start_volume = Some(current_volume);
}
}

pub fn update(&mut self) {
let now = Instant::now();

let mut to_remove = Vec::new();

for (source_id, envelope_state) in self.envelope_states.iter_mut() {
let elapsed = now.duration_since(envelope_state.start_time).as_secs_f32();

let envelope = &envelope_state.envelope;
let sink = self.audio_sinks.get_mut(source_id).unwrap();
let envelope = &envelope_state.envelope;

let volume = if elapsed < envelope.attack {
// Attack
elapsed / envelope.attack
} else if elapsed < envelope.attack + envelope.decay {
// Decay
1.0 - (elapsed - envelope.attack) / envelope.decay * (1.0 - envelope.sustain)
} else if envelope_state.is_releasing {
// Release
let elapsed_since_released = now
let volume = if envelope_state.is_releasing {
// Release phase - use captured start volume and release time
let elapsed_release = now
.duration_since(envelope_state.release_start_time.unwrap())
.as_secs_f32();
envelope.sustain - elapsed_since_released / envelope.release * envelope.sustain

let start_volume = envelope_state.release_start_volume.unwrap();
let t = (elapsed_release / envelope.release).min(1.0);
start_volume * (1.0 - t)
} else {
// Sustain
envelope.sustain
// Calculate based on ADSR phases
let elapsed = now.duration_since(envelope_state.start_time).as_secs_f32();

if elapsed < envelope.attack {
// Attack phase
elapsed / envelope.attack
} else if elapsed < envelope.attack + envelope.decay {
// Decay phase
1.0 - (elapsed - envelope.attack) / envelope.decay * (1.0 - envelope.sustain)
} else {
// Sustain phase
envelope.sustain
}
};

sink.set_volume(volume);

if envelope_state.is_releasing && elapsed > envelope.release {
// This is done as a separate step to avoid a second mutable borrow of self.envelope_states
// First borrow is when .iter_mut() is called, second is when .remove() is called
to_remove.push(*source_id);
// Check if release is complete
if envelope_state.is_releasing {
let elapsed_release = now
.duration_since(envelope_state.release_start_time.unwrap())
.as_secs_f32();

if elapsed_release >= envelope.release {
to_remove.push(*source_id);
}
}
}

// Cleanup completed sounds
for source_id in to_remove {
self.envelope_states.remove(&source_id);
self.audio_sinks.remove(&source_id);
Expand Down
Loading

0 comments on commit 5401b21

Please sign in to comment.