Skip to content

Commit cc87d60

Browse files
committed
Optimize release profile, add graceful MIDI fallback with SC-55 soundfont, and implement audio device selector with clean filtering
1 parent 7703405 commit cc87d60

11 files changed

Lines changed: 573 additions & 102 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "rusttracker"
3-
version = "0.9.7"
3+
version = "0.9.8"
44
edition = "2024"
55
default-run = "rusttracker"
66
description = "High-Performance Vulkan Tracker Module Visualizer"
@@ -66,6 +66,6 @@ objc2-core-audio = "0.3.2"
6666

6767
[profile.release]
6868
opt-level = 3
69-
lto = false
70-
codegen-units = 16
71-
strip = false
69+
lto = true
70+
codegen-units = 1
71+
strip = "symbols"

assets/soundfont.sf2

18.9 MB
Binary file not shown.

src/audio.rs

Lines changed: 186 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,67 @@ pub struct DspMessage {
4545
pub channel_audio_data: Vec<Vec<f32>>,
4646
}
4747

48+
fn is_relevant_audio_device(name: &str) -> bool {
49+
let name_lower = name.to_lowercase();
50+
// Filter out dummy/silent devices
51+
if name_lower == "null" {
52+
return false;
53+
}
54+
// Filter out virtual multi-channel surround setups (not suitable for normal stereo playback/recording)
55+
if name_lower.contains("surround") {
56+
return false;
57+
}
58+
// Filter out internal mixer and snooping plugins
59+
if name_lower.starts_with("dmix") || name_lower.starts_with("dsnoop") {
60+
return false;
61+
}
62+
// Filter out legacy or internal virtual interfaces/plugins
63+
if name_lower.starts_with("adsp") {
64+
return false;
65+
}
66+
if name_lower.starts_with("speex") || name_lower.contains("speexrate") {
67+
return false;
68+
}
69+
if name_lower.contains("upmix") || name_lower.contains("vdownmix") {
70+
return false;
71+
}
72+
if name_lower.contains("samplerate") || name_lower.contains("lavrate") {
73+
return false;
74+
}
75+
true
76+
}
77+
78+
pub fn get_available_audio_devices(mic: bool) -> Vec<String> {
79+
let host = cpal::default_host();
80+
let devices = if mic {
81+
host.input_devices()
82+
} else {
83+
host.output_devices()
84+
};
85+
86+
let mut names = Vec::new();
87+
if let Ok(devices) = devices {
88+
for d in devices {
89+
if let Ok(name) = d.name() {
90+
if is_relevant_audio_device(&name) {
91+
names.push(name);
92+
}
93+
}
94+
}
95+
}
96+
names
97+
}
98+
99+
pub fn get_default_audio_device_name(mic: bool) -> Option<String> {
100+
let host = cpal::default_host();
101+
let default_device = if mic {
102+
host.default_input_device()
103+
} else {
104+
host.default_output_device()
105+
};
106+
default_device.and_then(|d| d.name().ok())
107+
}
108+
48109
pub fn spawn_dsp_thread(
49110
rx: Receiver<DspMessage>,
50111
shared_state: Arc<Mutex<AppState>>,
@@ -191,6 +252,7 @@ pub fn spawn_dsp_thread(
191252
}
192253
let wave_clone = state.raw_waveform.clone();
193254
state.waveform_history.push_back(wave_clone);
255+
state.waveform_history_push_count += 1;
194256
last_waveform_push = now;
195257
}
196258

@@ -380,7 +442,8 @@ struct MidiEvent {
380442
}
381443

382444
struct MidiSource {
383-
sequencer: MidiFileSequencer,
445+
sequencer: Option<MidiFileSequencer>,
446+
pos_silent: f64,
384447
events: Vec<MidiEvent>,
385448
event_idx: usize,
386449
channel_vus: Vec<f32>,
@@ -398,17 +461,34 @@ unsafe impl Send for MidiSource {}
398461

399462
impl MidiSource {
400463
pub fn new(file_path: &str, soundfont_path: &str, sample_rate: i32) -> anyhow::Result<Self> {
401-
let mut sf2_file = File::open(soundfont_path)?;
402-
let sf2 = Arc::new(SoundFont::new(&mut sf2_file).map_err(|e| anyhow::anyhow!("SoundFont error: {:?}", e))?);
403-
let settings = SynthesizerSettings::new(sample_rate);
404-
let synth = Synthesizer::new(&sf2, &settings).map_err(|e| anyhow::anyhow!("Synth error: {:?}", e))?;
405-
let mut sequencer = MidiFileSequencer::new(synth);
464+
let mut artist_suffix = "";
465+
let sf2_file_opt = File::open(soundfont_path).ok();
466+
let mut sequencer = if let Some(mut sf2_file) = sf2_file_opt {
467+
if let Ok(sf2) = SoundFont::new(&mut sf2_file) {
468+
let sf2 = Arc::new(sf2);
469+
let settings = SynthesizerSettings::new(sample_rate);
470+
if let Ok(synth) = Synthesizer::new(&sf2, &settings) {
471+
Some(MidiFileSequencer::new(synth))
472+
} else {
473+
artist_suffix = " (No SoundFont)";
474+
None
475+
}
476+
} else {
477+
artist_suffix = " (No SoundFont)";
478+
None
479+
}
480+
} else {
481+
artist_suffix = " (No SoundFont)";
482+
None
483+
};
406484

407485
let mut midi_file = File::open(file_path)?;
408486
let midi = Arc::new(MidiFile::new(&mut midi_file).map_err(|e| anyhow::anyhow!("Midi parse error: {:?}", e))?);
409487

410488
let duration = midi.get_length();
411-
sequencer.play(&midi, false);
489+
if let Some(seq) = &mut sequencer {
490+
seq.play(&midi, false);
491+
}
412492

413493
let data = std::fs::read(file_path)?;
414494
let smf = Smf::parse(&data).map_err(|e| anyhow::anyhow!("Midly parse error: {:?}", e))?;
@@ -508,9 +588,13 @@ impl MidiSource {
508588
if artist.is_empty() {
509589
artist = "Unknown MIDI".to_string();
510590
}
591+
if !artist_suffix.is_empty() {
592+
artist.push_str(artist_suffix);
593+
}
511594

512595
Ok(Self {
513596
sequencer,
597+
pos_silent: 0.0,
514598
events: parsed_events,
515599
event_idx: 0,
516600
channel_vus: vec![0.0; 16],
@@ -530,18 +614,34 @@ impl AudioSource for MidiSource {
530614
fn read_frames(&mut self, hardware_channels: usize, _sample_rate: u32, output: &mut [f32]) -> usize {
531615
let frames_to_render = output.len() / hardware_channels;
532616

533-
if self.left_buf.len() < frames_to_render {
534-
self.left_buf.resize(frames_to_render, 0.0);
535-
self.right_buf.resize(frames_to_render, 0.0);
536-
}
537-
538-
self.sequencer.render(&mut self.left_buf[..frames_to_render], &mut self.right_buf[..frames_to_render]);
617+
let pos = if let Some(ref mut seq) = self.sequencer {
618+
if self.left_buf.len() < frames_to_render {
619+
self.left_buf.resize(frames_to_render, 0.0);
620+
self.right_buf.resize(frames_to_render, 0.0);
621+
}
622+
seq.render(&mut self.left_buf[..frames_to_render], &mut self.right_buf[..frames_to_render]);
623+
for i in 0..frames_to_render {
624+
let l = self.left_buf[i];
625+
let r = self.right_buf[i];
626+
output[i * hardware_channels] = l;
627+
if hardware_channels > 1 {
628+
output[i * hardware_channels + 1] = r;
629+
for j in 2..hardware_channels {
630+
output[i * hardware_channels + j] = 0.0;
631+
}
632+
}
633+
}
634+
seq.get_position()
635+
} else {
636+
output.fill(0.0);
637+
self.pos_silent = (self.pos_silent + frames_to_render as f64 / _sample_rate as f64).min(self.duration);
638+
self.pos_silent
639+
};
539640

540641
for c in 0..16 {
541642
self.channel_vus[c] = (self.channel_vus[c] - 0.02).max(0.0);
542643
}
543644

544-
let pos = self.sequencer.get_position();
545645
while self.event_idx < self.events.len() && self.events[self.event_idx].time_sec <= pos {
546646
let ev = &self.events[self.event_idx];
547647
if ev.is_note_on {
@@ -553,38 +653,41 @@ impl AudioSource for MidiSource {
553653
self.event_idx += 1;
554654
}
555655

556-
for i in 0..frames_to_render {
557-
let l = self.left_buf[i];
558-
let r = self.right_buf[i];
559-
560-
output[i * hardware_channels] = l;
561-
if hardware_channels > 1 {
562-
output[i * hardware_channels + 1] = r;
563-
for j in 2..hardware_channels {
564-
output[i * hardware_channels + j] = 0.0;
565-
}
566-
}
567-
}
656+
let is_end = if let Some(ref seq) = self.sequencer {
657+
seq.end_of_sequence()
658+
} else {
659+
pos >= self.duration
660+
};
568661

569-
if self.sequencer.end_of_sequence() {
662+
if is_end {
570663
0
571664
} else {
572665
frames_to_render
573666
}
574667
}
575668

576669
fn get_duration_seconds(&mut self) -> f64 { self.duration }
577-
fn get_position_seconds(&mut self) -> f64 { self.sequencer.get_position() }
670+
fn get_position_seconds(&mut self) -> f64 {
671+
if let Some(ref seq) = self.sequencer {
672+
seq.get_position()
673+
} else {
674+
self.pos_silent
675+
}
676+
}
578677

579678
fn set_position_seconds(&mut self, pos: f64) {
580-
if pos < self.get_position_seconds() {
581-
self.sequencer.play(&self.midi_file, false);
582-
}
583-
584-
let mut trash_left = vec![0.0; 8192];
585-
let mut trash_right = vec![0.0; 8192];
586-
while self.sequencer.get_position() < pos && !self.sequencer.end_of_sequence() {
587-
self.sequencer.render(&mut trash_left, &mut trash_right);
679+
if let Some(ref mut seq) = self.sequencer {
680+
if pos < seq.get_position() {
681+
seq.play(&self.midi_file, false);
682+
}
683+
684+
let mut trash_left = vec![0.0; 8192];
685+
let mut trash_right = vec![0.0; 8192];
686+
while seq.get_position() < pos && !seq.end_of_sequence() {
687+
seq.render(&mut trash_left, &mut trash_right);
688+
}
689+
} else {
690+
self.pos_silent = pos.clamp(0.0, self.duration);
588691
}
589692

590693
self.event_idx = 0;
@@ -621,7 +724,7 @@ impl AudioSource for MidiSource {
621724

622725
fn get_current_order(&mut self) -> i32 { 0 }
623726
fn get_current_row(&mut self) -> i32 {
624-
(self.sequencer.get_position() * (self.tempo as f64 / 60.0) * 4.0) as i32
727+
(self.get_position_seconds() * (self.tempo as f64 / 60.0) * 4.0) as i32
625728
}
626729

627730
fn get_tracker_channels(&mut self) -> Option<i32> { Some(16) }
@@ -1443,7 +1546,7 @@ pub fn start_audio_thread(file_path: &str, mic: bool, shared_state: Arc<Mutex<Ap
14431546
_ => &codec_name,
14441547
}.to_string();
14451548

1446-
let video_suffix = if has_video { " (Video stream available)" } else { "" };
1549+
let video_suffix = if has_video { " (Video available: 'v' to view)" } else { "" };
14471550

14481551
{
14491552
let mut state = shared_state.lock().unwrap();
@@ -1476,8 +1579,52 @@ pub fn start_audio_thread(file_path: &str, mic: bool, shared_state: Arc<Mutex<Ap
14761579
target_channels = 2;
14771580
}
14781581

1582+
let selected_device_name = {
1583+
let state = shared_state.lock().unwrap();
1584+
state.selected_audio_device.clone()
1585+
};
1586+
1587+
let device = if mic {
1588+
let mut dev = None;
1589+
if let Some(ref name) = selected_device_name {
1590+
if let Ok(devices) = host.input_devices() {
1591+
for d in devices {
1592+
if let Ok(d_name) = d.name() {
1593+
if &d_name == name {
1594+
dev = Some(d);
1595+
break;
1596+
}
1597+
}
1598+
}
1599+
}
1600+
}
1601+
if let Some(d) = dev {
1602+
d
1603+
} else {
1604+
host.default_input_device().context("No input device available")?
1605+
}
1606+
} else {
1607+
let mut dev = None;
1608+
if let Some(ref name) = selected_device_name {
1609+
if let Ok(devices) = host.output_devices() {
1610+
for d in devices {
1611+
if let Ok(d_name) = d.name() {
1612+
if &d_name == name {
1613+
dev = Some(d);
1614+
break;
1615+
}
1616+
}
1617+
}
1618+
}
1619+
}
1620+
if let Some(d) = dev {
1621+
d
1622+
} else {
1623+
host.default_output_device().context("No output device available")?
1624+
}
1625+
};
1626+
14791627
let supported_config = if mic {
1480-
let device = host.default_input_device().context("No input device available")?;
14811628
let supported_configs_range = device.supported_input_configs().context("error while querying input configs")?;
14821629
supported_configs_range
14831630
.into_iter()
@@ -1492,9 +1639,7 @@ pub fn start_audio_thread(file_path: &str, mic: bool, shared_state: Arc<Mutex<Ap
14921639
.or_else(|| device.default_input_config().ok())
14931640
.context("No supported config?!")?
14941641
} else {
1495-
let device = host.default_output_device().context("No output device available")?;
14961642
let supported_configs_range = device.supported_output_configs().context("error while querying output configs")?;
1497-
14981643
let mut configs: Vec<_> = supported_configs_range
14991644
.filter(|c| {
15001645
c.sample_format() == cpal::SampleFormat::F32 || c.sample_format() == cpal::SampleFormat::I16
@@ -1529,12 +1674,6 @@ pub fn start_audio_thread(file_path: &str, mic: bool, shared_state: Arc<Mutex<Ap
15291674
.context("No supported config?!")?
15301675
};
15311676

1532-
let device = if mic {
1533-
host.default_input_device().context("No input device available")?
1534-
} else {
1535-
host.default_output_device().context("No output device available")?
1536-
};
1537-
15381677
let config: cpal::StreamConfig = supported_config.clone().into();
15391678
let (tx, rx) = bounded::<DspMessage>(32);
15401679

0 commit comments

Comments
 (0)