Skip to content

Commit c11cea4

Browse files
committed
[+] feat: add audio mixer
1 parent 20790ba commit c11cea4

File tree

13 files changed

+925
-1
lines changed

13 files changed

+925
-1
lines changed

Cargo.lock

Lines changed: 66 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,15 @@ hound = "3.5"
8888
which = "8.0"
8989
ctrlc = "3.5"
9090
rayon = "1.11"
91+
rubato = "0.16"
9192
memmap2 = "0.9"
9293
pipewire = "0.9"
9394
thiserror = "2.0"
9495
crossbeam = "0.8"
9596
spin_sleep = "1.3"
9697
nnnoiseless = "0.5"
9798
ffmpeg-sidecar = "2.2"
99+
derive_builder = "0.20"
98100
wayland-client = "0.31"
99101
fast_image_resize = "5.3"
100102
nix = { version = "0.30", features = ["fs"] }

lib/mp4m/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/target
2+
/data/tmp

lib/mp4m/Cargo.toml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
[package]
2+
name = "mp4m"
3+
readme.workspace = true
4+
license.workspace = true
5+
edition.workspace = true
6+
version.workspace = true
7+
authors.workspace = true
8+
keywords.workspace = true
9+
homepage.workspace = true
10+
repository.workspace = true
11+
description.workspace = true
12+
13+
14+
[dependencies]
15+
log.workspace = true
16+
hound.workspace = true
17+
rubato.workspace = true
18+
thiserror.workspace = true
19+
crossbeam.workspace = true
20+
derive_builder.workspace = true
21+
22+
[dev-dependencies]
23+
rand.workspace = true
24+
env_logger.workspace = true

lib/mp4m/data/input.wav

3.54 MB
Binary file not shown.

lib/mp4m/data/speaker-mono.wav

1.11 MB
Binary file not shown.

lib/mp4m/data/speaker.wav

4.44 MB
Binary file not shown.
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
use hound::WavReader;
2+
use mp4m::{AudioProcessor, AudioProcessorConfigBuilder, OutputDestination, sample_rate};
3+
use rand::Rng;
4+
5+
fn main() -> Result<(), Box<dyn std::error::Error>> {
6+
env_logger::init();
7+
8+
let input_file1 = "data/speaker-mono.wav";
9+
let input_file2 = "data/input.wav";
10+
let output_file = "data/tmp/diff-audio-mixed.wav";
11+
12+
log::debug!("Reading WAV files: {}, {}", input_file1, input_file2);
13+
14+
let mut reader1 = WavReader::open(input_file1)?;
15+
let mut reader2 = WavReader::open(input_file2)?;
16+
17+
let spec1 = reader1.spec();
18+
let spec2 = reader2.spec();
19+
20+
log::debug!("Audio specs - File 1: {:?}", spec1);
21+
log::debug!("Audio specs - File 2: {:?}", spec2);
22+
23+
let all_samples1: Vec<f32> = match spec1.sample_format {
24+
hound::SampleFormat::Float => reader1.samples::<f32>().collect::<Result<Vec<f32>, _>>()?,
25+
hound::SampleFormat::Int => reader1
26+
.samples::<i16>()
27+
.map(|s| s.map(|v| v as f32))
28+
.collect::<Result<Vec<f32>, _>>()?,
29+
};
30+
let all_samples2: Vec<f32> = match spec2.sample_format {
31+
hound::SampleFormat::Float => reader2.samples::<f32>().collect::<Result<Vec<f32>, _>>()?,
32+
hound::SampleFormat::Int => reader2
33+
.samples::<i16>()
34+
.map(|s| s.map(|v| v as f32))
35+
.collect::<Result<Vec<f32>, _>>()?,
36+
};
37+
38+
log::debug!("Total samples - File 1: {}", all_samples1.len());
39+
log::debug!("Total samples - File 2: {}", all_samples2.len());
40+
41+
let max_samples = all_samples1.len().max(all_samples2.len());
42+
log::debug!("Max samples across files: {}", max_samples);
43+
44+
let samples_per_ms1 = (spec1.sample_rate as f32 / 1000.0) as usize * spec1.channels as usize;
45+
let samples_per_ms2 = (spec2.sample_rate as f32 / 1000.0) as usize * spec2.channels as usize;
46+
47+
let min_chunk_samples1 = (500.0 * samples_per_ms1 as f32) as usize;
48+
let max_chunk_samples1 = (1000.0 * samples_per_ms1 as f32) as usize;
49+
let min_chunk_samples2 = (500.0 * samples_per_ms2 as f32) as usize;
50+
let max_chunk_samples2 = (1000.0 * samples_per_ms2 as f32) as usize;
51+
52+
log::debug!(
53+
"Track 1 - Samples per ms: {} ({} channels)",
54+
samples_per_ms1,
55+
spec1.channels
56+
);
57+
log::debug!(
58+
"Track 1 - Min chunk samples (500ms): {}",
59+
min_chunk_samples1
60+
);
61+
log::debug!(
62+
"Track 1 - Max chunk samples (1000ms): {}",
63+
max_chunk_samples1
64+
);
65+
log::debug!(
66+
"Track 2 - Samples per ms: {} ({} channels)",
67+
samples_per_ms2,
68+
spec2.channels
69+
);
70+
log::debug!(
71+
"Track 2 - Min chunk samples (500ms): {}",
72+
min_chunk_samples2
73+
);
74+
log::debug!(
75+
"Track 2 - Max chunk samples (1000ms): {}",
76+
max_chunk_samples2
77+
);
78+
79+
let config = AudioProcessorConfigBuilder::default()
80+
.target_sample_rate(sample_rate::CD)
81+
.convert_to_mono(true)
82+
.output_destination(Some(OutputDestination::File(output_file.into())))
83+
.build()?;
84+
85+
let mut processor = AudioProcessor::new(config);
86+
let sender1 = processor.add_track(spec1);
87+
let sender2 = processor.add_track(spec2);
88+
89+
let mut rng = rand::rng();
90+
let mut processed_samples1 = 0;
91+
let mut processed_samples2 = 0;
92+
93+
while processed_samples1 < all_samples1.len() || processed_samples2 < all_samples2.len() {
94+
let remaining_samples1 = all_samples1.len() - processed_samples1;
95+
let remaining_samples2 = all_samples2.len() - processed_samples2;
96+
97+
let chunk_size1 = if remaining_samples1 == 0 {
98+
0
99+
} else if remaining_samples1 < min_chunk_samples1 {
100+
remaining_samples1
101+
} else {
102+
rng.random_range(min_chunk_samples1..=max_chunk_samples1.min(remaining_samples1))
103+
};
104+
105+
let chunk_size2 = if remaining_samples2 == 0 {
106+
0
107+
} else if remaining_samples2 < min_chunk_samples2 {
108+
remaining_samples2
109+
} else {
110+
rng.random_range(min_chunk_samples2..=max_chunk_samples2.min(remaining_samples2))
111+
};
112+
113+
let chunk1 = if chunk_size1 > 0 {
114+
let end = processed_samples1 + chunk_size1;
115+
&all_samples1[processed_samples1..end]
116+
} else {
117+
&[]
118+
};
119+
120+
let chunk2 = if chunk_size2 > 0 {
121+
let end = processed_samples2 + chunk_size2;
122+
&all_samples2[processed_samples2..end]
123+
} else {
124+
&[]
125+
};
126+
127+
log::debug!(
128+
"Processing chunks - Track1: {} samples, Track2: {} samples",
129+
chunk1.len(),
130+
chunk2.len()
131+
);
132+
133+
if !chunk1.is_empty() {
134+
sender1.send(chunk1.to_vec())?;
135+
processed_samples1 += chunk_size1;
136+
}
137+
if !chunk2.is_empty() {
138+
sender2.send(chunk2.to_vec())?;
139+
processed_samples2 += chunk_size2;
140+
}
141+
142+
processor.process_samples()?;
143+
144+
std::thread::sleep(std::time::Duration::from_millis(10));
145+
}
146+
147+
processor.flush()?;
148+
149+
log::debug!("Two-track audio mixing completed!");
150+
log::debug!("Mixed output saved to: {}", output_file);
151+
152+
Ok(())
153+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
use hound::WavReader;
2+
use mp4m::audio_processor::AudioProcessorConfigBuilder;
3+
use mp4m::{AudioProcessor, OutputDestination, sample_rate};
4+
use rand::Rng;
5+
6+
fn main() -> Result<(), Box<dyn std::error::Error>> {
7+
env_logger::init();
8+
9+
let input_file = "data/speaker.wav";
10+
let output_file = "data/tmp/one-audio-mixed.wav";
11+
12+
log::debug!("Reading WAV file: {}", input_file);
13+
14+
let mut reader = WavReader::open(input_file)?;
15+
let spec = reader.spec();
16+
17+
log::debug!("Audio specs: {:?}", spec);
18+
log::debug!("Sample rate: {} Hz", spec.sample_rate);
19+
log::debug!("Channels: {}", spec.channels);
20+
log::debug!("Bits per sample: {}", spec.bits_per_sample);
21+
22+
let all_samples: Vec<f32> = reader.samples::<f32>().collect::<Result<Vec<f32>, _>>()?;
23+
log::debug!("Total samples: {}", all_samples.len());
24+
25+
let samples_per_ms = (spec.sample_rate as f32 / 1000.0) as usize;
26+
let min_chunk_samples = (500.0 * samples_per_ms as f32) as usize;
27+
let max_chunk_samples = (1000.0 * samples_per_ms as f32) as usize;
28+
29+
log::debug!("Samples per ms: {}", samples_per_ms);
30+
log::debug!("Min chunk samples (500ms): {}", min_chunk_samples);
31+
log::debug!("Max chunk samples (1000ms): {}", max_chunk_samples);
32+
33+
let config = AudioProcessorConfigBuilder::default()
34+
.target_sample_rate(sample_rate::CD)
35+
.convert_to_mono(true)
36+
.output_destination(Some(OutputDestination::File(output_file.into())))
37+
.build()?;
38+
39+
let mut processor = AudioProcessor::new(config);
40+
let sender = processor.add_track(spec);
41+
42+
let mut rng = rand::rng();
43+
let mut processed_samples = 0;
44+
45+
while processed_samples < all_samples.len() {
46+
let remaining_samples = all_samples.len() - processed_samples;
47+
48+
let chunk_size = if remaining_samples < min_chunk_samples {
49+
remaining_samples
50+
} else {
51+
rng.random_range(min_chunk_samples..=max_chunk_samples.min(remaining_samples))
52+
};
53+
54+
let chunk = &all_samples[processed_samples..processed_samples + chunk_size];
55+
56+
log::debug!(
57+
"Processing chunk: {} samples ({} ms)",
58+
chunk_size,
59+
chunk_size as f32 / samples_per_ms as f32
60+
);
61+
62+
sender.send(chunk.to_vec())?;
63+
processor.process_samples()?;
64+
65+
processed_samples += chunk_size;
66+
67+
std::thread::sleep(std::time::Duration::from_millis(10));
68+
}
69+
70+
processor.flush()?;
71+
72+
log::debug!("Audio processing completed!");
73+
log::debug!("Output saved to: {}", output_file);
74+
75+
Ok(())
76+
}

0 commit comments

Comments
 (0)