@@ -4,7 +4,7 @@ use cpal::{SampleFormat, SampleRate, Stream, StreamConfig};
44use decibel:: { AmplitudeRatio , DecibelRatio } ;
55use ringbuf:: traits:: { Observer , Consumer , Producer , Split } ;
66use ringbuf:: { HeapCons , HeapRb } ;
7- use std:: collections:: { BTreeSet , HashMap } ;
7+ use std:: collections:: { HashMap } ; // BTreeSet is no longer needed here
88use std:: fs:: File ;
99use std:: io:: BufReader ;
1010use std:: sync:: { mpsc, Arc } ;
@@ -452,7 +452,11 @@ fn spawn_audio_processing_thread<P>(
452452 spawn_reaper_thread ( reaper_rx) ;
453453
454454 thread:: spawn ( move || {
455- let mut active_stops: BTreeSet < usize > = BTreeSet :: new ( ) ;
455+ // Create a map from stop_name -> stop_index for fast lookup
456+ let stop_name_to_index_map: HashMap < String , usize > = organ. stops . iter ( ) . enumerate ( )
457+ . map ( |( i, stop) | ( stop. name . clone ( ) , i) )
458+ . collect ( ) ;
459+
456460 let mut active_notes: HashMap < u8 , Vec < ActiveNote > > = HashMap :: new ( ) ;
457461 let mut voices: HashMap < u64 , Voice > = HashMap :: with_capacity ( 128 ) ;
458462 let mut voice_counter: u64 = 0 ;
@@ -477,136 +481,105 @@ fn spawn_audio_processing_thread<P>(
477481 // --- Handle incoming messages ---
478482 while let Ok ( msg) = rx. try_recv ( ) {
479483 match msg {
480- AppMessage :: NoteOn ( note, _vel) => {
481- // Check if note is already active
482- if let Some ( notes) = active_notes. get_mut ( & note) {
483- if !notes. is_empty ( ) {
484- log:: warn!( "[AudioThread] NoteOn received for already active note {}. Ignoring." , note) ;
485- continue ; // Ignore this NoteOn
486- }
487- }
488- if _vel > 0 {
484+ AppMessage :: NoteOn ( note, _vel, stop_name) => {
485+ let note_on_time = Instant :: now ( ) ;
486+ // Find the stop_index from the stop_name
487+ if let Some ( stop_index) = stop_name_to_index_map. get ( & stop_name) {
488+ let stop = & organ. stops [ * stop_index] ;
489489 let mut new_notes = Vec :: new ( ) ;
490- let note_on_time = Instant :: now ( ) ;
491- for stop_index in & active_stops {
492- let stop = & organ. stops [ * stop_index] ;
493- for rank_id in & stop. rank_ids {
494- if let Some ( rank) = organ. ranks . get ( rank_id) {
495- if let Some ( pipe) = rank. pipes . get ( & note) {
496- let total_gain = rank. gain_db + pipe. gain_db ;
497- // Play attack sample
498- match Voice :: new (
499- & pipe. attack_sample_path ,
500- Arc :: clone ( & organ) ,
501- sample_rate,
502- total_gain,
503- false ,
504- true ,
505- note_on_time,
506- ) {
507- Ok ( voice) => {
508- let voice_id = voice_counter;
509- voice_counter += 1 ;
510- voices. insert ( voice_id, voice) ;
511-
512- new_notes. push ( ActiveNote {
513- note,
514- start_time : Instant :: now ( ) ,
515- stop_index : * stop_index,
516- rank_id : rank_id. clone ( ) ,
517- voice_id,
518- } ) ;
519- }
520- Err ( e) => {
521- log:: error!( "[AudioThread] Error creating attack voice: {}" , e)
522- }
490+
491+ for rank_id in & stop. rank_ids {
492+ if let Some ( rank) = organ. ranks . get ( rank_id) {
493+ if let Some ( pipe) = rank. pipes . get ( & note) {
494+ let total_gain = rank. gain_db + pipe. gain_db ;
495+ // Play attack sample
496+ match Voice :: new (
497+ & pipe. attack_sample_path ,
498+ Arc :: clone ( & organ) ,
499+ sample_rate,
500+ total_gain,
501+ false ,
502+ true ,
503+ note_on_time,
504+ ) {
505+ Ok ( voice) => {
506+ let voice_id = voice_counter;
507+ voice_counter += 1 ;
508+ voices. insert ( voice_id, voice) ;
509+
510+ new_notes. push ( ActiveNote {
511+ note,
512+ start_time : note_on_time, // Use the same start time
513+ stop_index : * stop_index,
514+ rank_id : rank_id. clone ( ) ,
515+ voice_id,
516+ } ) ;
517+ }
518+ Err ( e) => {
519+ log:: error!( "[AudioThread] Error creating attack voice: {}" , e)
523520 }
524521 }
525522 }
526523 }
527524 }
525+
528526 if !new_notes. is_empty ( ) {
529- // insert() returns the old Vec if one existed
530- let _old_notes = active_notes. insert ( note, new_notes) ;
531-
532- // // If there were old notes, we MUST kill them
533- // if let Some(notes_to_stop) = old_notes {
534- // log::warn!("[AudioThread] NoteOn re-trigger on note {}. Fading old voices.", note);
535- // for stopped_note in notes_to_stop {
536- // // This is the same logic from handle_note_off
537- // if let Some(voice) = voices.get_mut(&stopped_note.voice_id) {
538- // voice.is_cancelled.store(true, Ordering::SeqCst);
539- // voice.is_fading_out = true;
540- // // We do NOT add a release sample here, as this is a
541- // // re-trigger, not a release. The new voice takes over.
542- // }
543- // }
544- // }
527+ // Add all new notes to the map entry for that note number
528+ active_notes. entry ( note) . or_default ( ) . extend ( new_notes) ;
545529 }
530+
546531 } else {
547- handle_note_off (
548- note, & organ, & mut voices, & mut active_notes,
549- sample_rate, & mut voice_counter,
550- ) ;
532+ log:: warn!( "[AudioThread] NoteOn for unknown stop: {}" , stop_name) ;
551533 }
552534 }
553- AppMessage :: NoteOff ( note) => {
554- handle_note_off (
555- note, & organ, & mut voices, & mut active_notes,
556- sample_rate, & mut voice_counter,
557- ) ;
558- }
559- AppMessage :: AllNotesOff => {
560- let notes: Vec < u8 > = active_notes. keys ( ) . cloned ( ) . collect ( ) ;
561- for note in notes {
562- handle_note_off (
563- note, & organ, & mut voices, & mut active_notes,
564- sample_rate, & mut voice_counter,
565- ) ;
566- }
567- }
568- AppMessage :: StopToggle ( stop_index, is_active) => {
569- if is_active {
570- active_stops. insert ( stop_index) ;
571- } else {
572- // Remove the desired stop from set to prevent future notes being played
573- active_stops. remove ( & stop_index) ;
574-
575- // Find all currently playing notes on this stop
576- let mut notes_to_stop: Vec < ActiveNote > = Vec :: new ( ) ;
577-
578- // Iterate over all active notes (e.g., C4, G#5, etc.)
579- active_notes. values_mut ( ) . for_each ( |note_list| {
580- // Use retain to keep notes that *don't* match the stop_index
581- note_list. retain ( |an| {
582- if an. stop_index == stop_index {
583- // If it matches, add it to our stop list...
584- notes_to_stop. push ( an. clone ( ) ) ; // We need to own it
585- // ...and return false to remove it from note_list
586- false
587- } else {
588- // Keep it
589- true
590- }
591- } ) ;
592- } ) ;
593-
594- // Clean up any note keys that now have empty lists
595- active_notes. retain ( |_note, note_list| !note_list. is_empty ( ) ) ;
535+ AppMessage :: NoteOff ( note, stop_name) => {
536+ // Find the stop_index from the stop_name
537+ if let Some ( stop_index) = stop_name_to_index_map. get ( & stop_name) {
538+ let mut stopped_note_opt: Option < ActiveNote > = None ;
539+
540+ // Check if the note is active at all
541+ if let Some ( note_list) = active_notes. get_mut ( & note) {
542+ // Find the index of the specific note to remove
543+ if let Some ( pos) = note_list. iter ( ) . position ( |an| an. stop_index == * stop_index) {
544+ // Remove it from the list and take ownership
545+ stopped_note_opt = Some ( note_list. remove ( pos) ) ;
546+ }
547+
548+ // If list is now empty, remove the note key from the main map
549+ if note_list. is_empty ( ) {
550+ active_notes. remove ( & note) ;
551+ }
552+ }
596553
597- // Process each note that needs to be stopped
598- for current_note in notes_to_stop {
599- trigger_note_release (
600- current_note ,
554+ // If we successfully removed a note, trigger its release
555+ if let Some ( stopped_note ) = stopped_note_opt {
556+ trigger_note_release (
557+ stopped_note ,
601558 & organ,
602559 & mut voices,
603560 sample_rate,
604561 & mut voice_counter
605562 ) ;
563+ } else {
564+ // This is common if NoteOff is sent twice, etc.
565+ log:: trace!( "[AudioThread] NoteOff for stop {} on note {}, but not found." , stop_name, note) ;
606566 }
607567
568+ } else {
569+ log:: warn!( "[AudioThread] NoteOff for unknown stop: {}" , stop_name) ;
570+ }
571+ }
572+ AppMessage :: AllNotesOff => {
573+ // This is a panic, stop all notes
574+ let notes: Vec < u8 > = active_notes. keys ( ) . cloned ( ) . collect ( ) ;
575+ for note in notes {
576+ handle_note_off (
577+ note, & organ, & mut voices, & mut active_notes,
578+ sample_rate, & mut voice_counter,
579+ ) ;
608580 }
609581 }
582+ // AppMessage::StopToggle removed
610583 AppMessage :: Quit => {
611584 drop ( reaper_tx) ;
612585 return ; // Exit thread
@@ -808,6 +781,8 @@ fn handle_note_off(
808781 sample_rate : u32 ,
809782 voice_counter : & mut u64 ,
810783) {
784+ // This removes *all* active notes for this note number,
785+ // which is used for the panic function
811786 if let Some ( notes_to_stop) = active_notes. remove ( & note) {
812787 for stopped_note in notes_to_stop {
813788 // This `stopped_note` is an `ActiveNote`
0 commit comments