Skip to content

Commit bb58902

Browse files
authored
Merge pull request #531 from b-ma/fix/absn-stitching
Fix `AudioBufferSourceNode` stitching
2 parents f4478ef + f719fd7 commit bb58902

File tree

2 files changed

+123
-40
lines changed

2 files changed

+123
-40
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ include = [
1818
rust-version = "1.71"
1919

2020
[dependencies]
21+
almost = "0.2.0"
2122
arc-swap = "1.6"
2223
arrayvec = "0.7"
2324
cpal = { version = "0.15", optional = true }

src/node/audio_buffer_source.rs

Lines changed: 122 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -633,6 +633,12 @@ impl AudioProcessor for AudioBufferSourceRenderer {
633633
for (i, playback_info) in playback_infos.iter_mut().enumerate() {
634634
let current_time = block_time + i as f64 * dt;
635635

636+
// Sticky behavior to handle floating point errors due to start time computation
637+
// cf. test_subsample_buffer_stitching
638+
if !self.render_state.started && almost::equal(current_time, self.start_time) {
639+
self.start_time = current_time;
640+
}
641+
636642
// Handle following cases:
637643
// - we are before start time
638644
// - we are after stop time
@@ -736,8 +742,9 @@ impl AudioProcessor for AudioBufferSourceRenderer {
736742
let next_sample = match buffer_channel.get(prev_frame_index + 1)
737743
{
738744
Some(val) => *val as f64,
745+
// End of buffer
739746
None => {
740-
let sample = if is_looping {
747+
if is_looping {
741748
if playback_rate >= 0. {
742749
let start_playhead =
743750
actual_loop_start * sample_rate;
@@ -749,18 +756,31 @@ impl AudioProcessor for AudioBufferSourceRenderer {
749756
start_playhead as usize + 1
750757
};
751758

752-
buffer_channel[start_index]
759+
buffer_channel[start_index] as f64
753760
} else {
754761
let end_playhead =
755762
actual_loop_end * sample_rate;
756763
let end_index = end_playhead as usize;
757-
buffer_channel[end_index]
764+
buffer_channel[end_index] as f64
758765
}
759766
} else {
760-
0.
761-
};
762-
763-
sample as f64
767+
// Handle 2 edge cases:
768+
// 1. We are in a case where buffer time is below buffer
769+
// duration due to floating point errors, but where
770+
// prev_frame_index is last index and k is near 1. We can't
771+
// filter this case before, because it might break
772+
// loops logic.
773+
// 2. Buffer contains only one sample
774+
if almost::equal(*k, 1.) || *prev_frame_index == 0 {
775+
0.
776+
} else {
777+
// Extrapolate next sample using the last two known samples
778+
// cf. https://github.com/WebAudio/web-audio-api/issues/2032
779+
let prev_prev_sample =
780+
buffer_channel[*prev_frame_index - 1];
781+
2. * prev_sample - prev_prev_sample as f64
782+
}
783+
}
764784
}
765785
};
766786

@@ -835,6 +855,7 @@ mod tests {
835855
use std::sync::{Arc, Mutex};
836856

837857
use crate::context::{BaseAudioContext, OfflineAudioContext};
858+
use crate::AudioBufferOptions;
838859
use crate::RENDER_QUANTUM_SIZE;
839860

840861
use super::*;
@@ -1120,44 +1141,47 @@ mod tests {
11201141

11211142
#[test]
11221143
fn test_audio_buffer_resampling() {
1123-
[22_500, 38_000, 48_000, 96_000].iter().for_each(|sr| {
1124-
let base_sr = 44_100;
1125-
let mut context = OfflineAudioContext::new(1, base_sr, base_sr as f32);
1126-
1127-
// 1Hz sine at different sample rates
1128-
let buf_sr = *sr;
1129-
// safe cast for sample rate, see discussion at #113
1130-
let sample_rate = buf_sr as f32;
1131-
let mut buffer = context.create_buffer(1, buf_sr, sample_rate);
1132-
let mut sine = vec![];
1133-
1134-
for i in 0..buf_sr {
1135-
let phase = i as f32 / buf_sr as f32 * 2. * PI;
1136-
let sample = phase.sin();
1137-
sine.push(sample);
1138-
}
1144+
[22_500, 38_000, 43_800, 48_000, 96_000]
1145+
.iter()
1146+
.for_each(|sr| {
1147+
let freq = 1.;
1148+
let base_sr = 44_100;
1149+
let mut context = OfflineAudioContext::new(1, base_sr, base_sr as f32);
1150+
1151+
// 1Hz sine at different sample rates
1152+
let buf_sr = *sr;
1153+
// safe cast for sample rate, see discussion at #113
1154+
let sample_rate = buf_sr as f32;
1155+
let mut buffer = context.create_buffer(1, buf_sr, sample_rate);
1156+
let mut sine = vec![];
1157+
1158+
for i in 0..buf_sr {
1159+
let phase = freq * i as f32 / buf_sr as f32 * 2. * PI;
1160+
let sample = phase.sin();
1161+
sine.push(sample);
1162+
}
11391163

1140-
buffer.copy_to_channel(&sine[..], 0);
1164+
buffer.copy_to_channel(&sine[..], 0);
11411165

1142-
let mut src = context.create_buffer_source();
1143-
src.connect(&context.destination());
1144-
src.set_buffer(buffer);
1145-
src.start_at(0. / sample_rate as f64);
1166+
let mut src = context.create_buffer_source();
1167+
src.connect(&context.destination());
1168+
src.set_buffer(buffer);
1169+
src.start_at(0. / sample_rate as f64);
11461170

1147-
let result = context.start_rendering_sync();
1148-
let channel = result.get_channel_data(0);
1171+
let result = context.start_rendering_sync();
1172+
let channel = result.get_channel_data(0);
11491173

1150-
// 1Hz sine at audio context sample rate
1151-
let mut expected = vec![];
1174+
// 1Hz sine at audio context sample rate
1175+
let mut expected = vec![];
11521176

1153-
for i in 0..base_sr {
1154-
let phase = i as f32 / base_sr as f32 * 2. * PI;
1155-
let sample = phase.sin();
1156-
expected.push(sample);
1157-
}
1177+
for i in 0..base_sr {
1178+
let phase = freq * i as f32 / base_sr as f32 * 2. * PI;
1179+
let sample = phase.sin();
1180+
expected.push(sample);
1181+
}
11581182

1159-
assert_float_eq!(channel[..], expected[..], abs_all <= 1e-6);
1160-
});
1183+
assert_float_eq!(channel[..], expected[..], abs_all <= 1e-6);
1184+
});
11611185
}
11621186

11631187
#[test]
@@ -1263,7 +1287,7 @@ mod tests {
12631287
}
12641288

12651289
#[test]
1266-
fn test_end_of_file_slow_track() {
1290+
fn test_end_of_file_slow_track_1() {
12671291
let sample_rate = 48_000.;
12681292
let mut context = OfflineAudioContext::new(1, RENDER_QUANTUM_SIZE * 2, sample_rate);
12691293

@@ -1817,6 +1841,64 @@ mod tests {
18171841
assert_float_eq!(channel[..], expected[..], abs_all <= 0.);
18181842
}
18191843

1844+
#[test]
1845+
// Ported from wpt: the-audiobuffersourcenode-interface/sub-sample-buffer-stitching.html
1846+
// Note that in wpt, results are tested against an oscillator node, which fails
1847+
// in the (44_100., 43_800., 3.8986e-3) condition for some (yet) unknown reason
1848+
fn test_subsample_buffer_stitching() {
1849+
[(44_100., 44_100., 9.0957e-5), (44_100., 43_800., 3.8986e-3)]
1850+
.iter()
1851+
.for_each(|(sample_rate, buffer_rate, error_threshold)| {
1852+
let sample_rate = *sample_rate;
1853+
let buffer_rate = *buffer_rate;
1854+
let buffer_length = 30;
1855+
let frequency = 440.;
1856+
1857+
// let length = sample_rate as usize;
1858+
let length = buffer_length * 15;
1859+
let mut context = OfflineAudioContext::new(2, length, sample_rate);
1860+
1861+
let mut wave_signal = vec![0.; context.length()];
1862+
let omega = 2. * PI / buffer_rate * frequency;
1863+
wave_signal.iter_mut().enumerate().for_each(|(i, s)| {
1864+
*s = (omega * i as f32).sin();
1865+
});
1866+
1867+
// Slice the sine wave into many little buffers to be assigned to ABSNs
1868+
// that are started at the appropriate times to produce a final sine
1869+
// wave.
1870+
for k in (0..context.length()).step_by(buffer_length) {
1871+
let mut buffer = AudioBuffer::new(AudioBufferOptions {
1872+
number_of_channels: 1,
1873+
length: buffer_length,
1874+
sample_rate: buffer_rate,
1875+
});
1876+
buffer.copy_to_channel(&wave_signal[k..k + buffer_length], 0);
1877+
1878+
let mut src = AudioBufferSourceNode::new(
1879+
&context,
1880+
AudioBufferSourceOptions {
1881+
buffer: Some(buffer),
1882+
..Default::default()
1883+
},
1884+
);
1885+
src.connect(&context.destination());
1886+
src.start_at(k as f64 / buffer_rate as f64);
1887+
}
1888+
1889+
let mut expected = vec![0.; context.length()];
1890+
let omega = 2. * PI / sample_rate * frequency;
1891+
expected.iter_mut().enumerate().for_each(|(i, s)| {
1892+
*s = (omega * i as f32).sin();
1893+
});
1894+
1895+
let result = context.start_rendering_sync();
1896+
let actual = result.get_channel_data(0);
1897+
1898+
assert_float_eq!(actual[..], expected[..], abs_all <= error_threshold);
1899+
});
1900+
}
1901+
18201902
#[test]
18211903
fn test_onended_before_drop() {
18221904
let sample_rate = 48_000.;

0 commit comments

Comments
 (0)