Skip to content

Commit f618bbf

Browse files
Implement midi channel mapping
1 parent d622a07 commit f618bbf

5 files changed

Lines changed: 332 additions & 240 deletions

File tree

src/app.rs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,12 @@ use std::time::Instant;
33
/// Messages sent from the TUI and MIDI threads to the Audio thread.
44
#[derive(Debug)]
55
pub enum AppMessage {
6-
/// MIDI Note On event. (key, velocity)
7-
NoteOn(u8, u8),
8-
/// MIDI Note Off event. (key)
9-
NoteOff(u8),
6+
/// MIDI Note On event. (key, velocity, stop name)
7+
NoteOn(u8, u8, String),
8+
/// MIDI Note Off event. (key, stop name)
9+
NoteOff(u8, String),
1010
/// A command to stop all currently playing notes.
1111
AllNotesOff,
12-
/// TUI stop toggle event. (stop_index, is_active)
13-
StopToggle(usize, bool),
1412
/// TUI quit event.
1513
Quit,
1614
}
@@ -26,6 +24,14 @@ pub enum TuiMessage {
2624
TuiNoteOn(u8, Instant),
2725
TuiNoteOff(u8, Instant),
2826
TuiAllNotesOff,
27+
28+
/// --- Midi events to TUI---
29+
/// (note, velocity, channel)
30+
MidiNoteOn(u8, u8, u8),
31+
/// (note, channel)
32+
MidiNoteOff(u8, u8),
33+
/// (channel)
34+
MidiChannelNotesOff(u8),
2935
}
3036

3137
/// Holds information about a currently playing note.

src/audio.rs

Lines changed: 86 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use cpal::{SampleFormat, SampleRate, Stream, StreamConfig};
44
use decibel::{AmplitudeRatio, DecibelRatio};
55
use ringbuf::traits::{Observer, Consumer, Producer, Split};
66
use ringbuf::{HeapCons, HeapRb};
7-
use std::collections::{BTreeSet, HashMap};
7+
use std::collections::{HashMap}; // BTreeSet is no longer needed here
88
use std::fs::File;
99
use std::io::BufReader;
1010
use 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`

src/main.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,14 +112,13 @@ fn main() -> Result<()> {
112112
println!("Starting MIDI file playback: {}", path.display());
113113
_midi_file_thread = Some(midi::play_midi_file(
114114
path,
115-
audio_tx.clone(),
116115
tui_tx.clone()
117116
)?);
118117
_midi_connection = None;
119118
} else {
120119
// --- Use live MIDI input ---
121120
println!("Initializing MIDI...");
122-
_midi_connection = Some(midi::setup_midi_input(audio_tx.clone(), tui_tx.clone())?);
121+
_midi_connection = Some(midi::setup_midi_input(tui_tx.clone())?);
123122
_midi_file_thread = None;
124123
println!("MIDI input enabled.");
125124
}

0 commit comments

Comments
 (0)