web: Implement WebAudioBackend::get_sound_position
This commit is contained in:
parent
105c889f5f
commit
04d84a3386
|
@ -26,3 +26,15 @@ export function copyToAudioBuffer(
|
||||||
dstBuffer.set(rightData);
|
dstBuffer.set(rightData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns `AudioContext.getOutputTimestamp`, defaulting to `context.currentTime` if
|
||||||
|
* `getOutputTimestamp` is unavailable. This is necessary because `web-sys` does not yet export
|
||||||
|
* `AudioBuffer.copyToChannel`.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export function getAudioOutputTimestamp(context: AudioContext): number {
|
||||||
|
const timestamp = context.getOutputTimestamp?.();
|
||||||
|
return timestamp?.contextTime ?? context.currentTime - context.baseLatency;
|
||||||
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ pub struct WebAudioBackend {
|
||||||
sounds: Arena<Sound>,
|
sounds: Arena<Sound>,
|
||||||
left_samples: Vec<f32>,
|
left_samples: Vec<f32>,
|
||||||
right_samples: Vec<f32>,
|
right_samples: Vec<f32>,
|
||||||
|
output_time: f64,
|
||||||
frame_rate: f64,
|
frame_rate: f64,
|
||||||
min_sample_rate: u16,
|
min_sample_rate: u16,
|
||||||
preload_stream_data: FnvHashMap<PreloadStreamHandle, StreamData>,
|
preload_stream_data: FnvHashMap<PreloadStreamHandle, StreamData>,
|
||||||
|
@ -93,6 +94,22 @@ struct SoundInstance {
|
||||||
/// either decoded on the fly with Decoder, or pre-decoded
|
/// either decoded on the fly with Decoder, or pre-decoded
|
||||||
/// and played with and AudioBufferSourceNode.
|
/// and played with and AudioBufferSourceNode.
|
||||||
instance_type: SoundInstanceType,
|
instance_type: SoundInstanceType,
|
||||||
|
|
||||||
|
/// The time in seconds that this buffer started playing.
|
||||||
|
/// This time uses the same origin as `AudioContext.currentTime`.
|
||||||
|
start_time: f64,
|
||||||
|
|
||||||
|
/// The starting point of the sound data in seconds.
|
||||||
|
/// `0.0` means the beginning of the sound.
|
||||||
|
loop_start: f64,
|
||||||
|
|
||||||
|
/// The ending point of the sound data in seconds.
|
||||||
|
/// `f64::MAX` if no end point is specified.
|
||||||
|
loop_end: f64,
|
||||||
|
|
||||||
|
/// The number of times the sound data will loop.
|
||||||
|
/// `1` means the sound plays once.
|
||||||
|
num_loops: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The Drop impl ensures that the sound is stopped and remove from the audio context,
|
/// The Drop impl ensures that the sound is stopped and remove from the audio context,
|
||||||
|
@ -324,6 +341,7 @@ impl WebAudioBackend {
|
||||||
next_stream_id: 0,
|
next_stream_id: 0,
|
||||||
left_samples: vec![],
|
left_samples: vec![],
|
||||||
right_samples: vec![],
|
right_samples: vec![],
|
||||||
|
output_time: 0.0,
|
||||||
frame_rate: 1.0,
|
frame_rate: 1.0,
|
||||||
min_sample_rate,
|
min_sample_rate,
|
||||||
})
|
})
|
||||||
|
@ -350,6 +368,9 @@ impl WebAudioBackend {
|
||||||
|
|
||||||
let sound_sample_rate: f64 = sound.format.sample_rate.into();
|
let sound_sample_rate: f64 = sound.format.sample_rate.into();
|
||||||
let mut is_stereo = sound.format.is_stereo;
|
let mut is_stereo = sound.format.is_stereo;
|
||||||
|
let mut loop_start = f64::from(sound.skip_sample_frames) / 44100.0;
|
||||||
|
let mut loop_end = std::f64::MAX;
|
||||||
|
let mut num_loops = 1;
|
||||||
let node: web_sys::AudioNode = match settings {
|
let node: web_sys::AudioNode = match settings {
|
||||||
Some(settings)
|
Some(settings)
|
||||||
if sound.skip_sample_frames > 0
|
if sound.skip_sample_frames > 0
|
||||||
|
@ -360,12 +381,12 @@ impl WebAudioBackend {
|
||||||
{
|
{
|
||||||
// Event sound with non-default parameters.
|
// Event sound with non-default parameters.
|
||||||
// Note that start/end values are in 44.1kHZ samples regardless of the sound's sample rate.
|
// Note that start/end values are in 44.1kHZ samples regardless of the sound's sample rate.
|
||||||
let start_sample_frame = f64::from(settings.in_sample.unwrap_or(0))
|
loop_start = f64::from(settings.in_sample.unwrap_or(0)) / 44100.0
|
||||||
/ 44100.0
|
|
||||||
+ f64::from(sound.skip_sample_frames) / sound_sample_rate;
|
+ f64::from(sound.skip_sample_frames) / sound_sample_rate;
|
||||||
node.set_loop(settings.num_loops > 1);
|
num_loops = settings.num_loops;
|
||||||
node.set_loop_start(start_sample_frame);
|
node.set_loop(num_loops > 1);
|
||||||
node.start_with_when_and_grain_offset(0.0, start_sample_frame)
|
node.set_loop_start(loop_start);
|
||||||
|
node.start_with_when_and_grain_offset(0.0, loop_start)
|
||||||
.warn_on_error();
|
.warn_on_error();
|
||||||
|
|
||||||
let current_time = self.context.current_time();
|
let current_time = self.context.current_time();
|
||||||
|
@ -373,18 +394,18 @@ impl WebAudioBackend {
|
||||||
// The length of the sound in the swf, or by the script playing it, doesn't
|
// The length of the sound in the swf, or by the script playing it, doesn't
|
||||||
// always line up with the actual length of the sound.
|
// always line up with the actual length of the sound.
|
||||||
// Always set a custom end point to make sure we're correct.
|
// Always set a custom end point to make sure we're correct.
|
||||||
let end_sample_frame = if let Some(out_sample) = settings.out_sample {
|
loop_end = if let Some(out_sample) = settings.out_sample {
|
||||||
f64::from(out_sample) / 44100.0
|
f64::from(out_sample) / 44100.0
|
||||||
} else {
|
} else {
|
||||||
f64::from(sound.num_sample_frames + u32::from(sound.skip_sample_frames))
|
f64::from(sound.num_sample_frames + u32::from(sound.skip_sample_frames))
|
||||||
/ sound_sample_rate
|
/ sound_sample_rate
|
||||||
};
|
};
|
||||||
|
|
||||||
// `AudioSourceBufferNode.loop` is a bool, so we have to stop the loop at the proper time.
|
// `AudioSourceBufferNode.loop` is a bool, so we have to stop the loop at the proper time.
|
||||||
// `start_with_when_and_grain_offset_and_grain_duration` unfortunately doesn't work
|
// `start_with_when_and_grain_offset_and_grain_duration` unfortunately doesn't work
|
||||||
// as you might expect with loops, so we use `stop_with_when` to stop the loop.
|
// as you might expect with loops, so we use `stop_with_when` to stop the loop.
|
||||||
let total_len =
|
let total_len = (loop_end - loop_start) * f64::from(settings.num_loops);
|
||||||
(end_sample_frame - start_sample_frame) * f64::from(settings.num_loops);
|
node.set_loop_end(loop_end);
|
||||||
node.set_loop_end(end_sample_frame);
|
|
||||||
node.stop_with_when(current_time + total_len)
|
node.stop_with_when(current_time + total_len)
|
||||||
.warn_on_error();
|
.warn_on_error();
|
||||||
|
|
||||||
|
@ -416,6 +437,10 @@ impl WebAudioBackend {
|
||||||
let instance = SoundInstance {
|
let instance = SoundInstance {
|
||||||
handle: Some(handle),
|
handle: Some(handle),
|
||||||
format: sound.format.clone(),
|
format: sound.format.clone(),
|
||||||
|
start_time: self.context.current_time(),
|
||||||
|
loop_start,
|
||||||
|
loop_end,
|
||||||
|
num_loops,
|
||||||
instance_type: SoundInstanceType::AudioBuffer(AudioBufferInstance {
|
instance_type: SoundInstanceType::AudioBuffer(AudioBufferInstance {
|
||||||
envelope_node: node.clone(),
|
envelope_node: node.clone(),
|
||||||
envelope_is_stereo: is_stereo,
|
envelope_is_stereo: is_stereo,
|
||||||
|
@ -473,6 +498,10 @@ impl WebAudioBackend {
|
||||||
let instance = SoundInstance {
|
let instance = SoundInstance {
|
||||||
handle: Some(handle),
|
handle: Some(handle),
|
||||||
format: sound.format.clone(),
|
format: sound.format.clone(),
|
||||||
|
start_time: self.context.current_time(),
|
||||||
|
loop_start: 0.0,
|
||||||
|
loop_end: std::f64::MAX,
|
||||||
|
num_loops: 1,
|
||||||
instance_type: SoundInstanceType::Decoder(decoder),
|
instance_type: SoundInstanceType::Decoder(decoder),
|
||||||
};
|
};
|
||||||
SOUND_INSTANCES.with(|instances| {
|
SOUND_INSTANCES.with(|instances| {
|
||||||
|
@ -1022,8 +1051,21 @@ impl AudioBackend for WebAudioBackend {
|
||||||
fn get_sound_position(&self, instance: SoundInstanceHandle) -> Option<f64> {
|
fn get_sound_position(&self, instance: SoundInstanceHandle) -> Option<f64> {
|
||||||
SOUND_INSTANCES.with(|instances| {
|
SOUND_INSTANCES.with(|instances| {
|
||||||
let instances = instances.borrow();
|
let instances = instances.borrow();
|
||||||
// TODO: Return actual position
|
instances.get(instance).map(|instance| {
|
||||||
instances.get(instance).map(|_| 0.0)
|
// Estimate the position of the sound based on the current AudioContext time.
|
||||||
|
let mut dt = self.output_time - instance.start_time;
|
||||||
|
dt = dt.max(0.0);
|
||||||
|
let loop_time = instance.loop_end - instance.loop_start;
|
||||||
|
let loop_index = (dt / loop_time) as u16;
|
||||||
|
// If the sound is looping, the position cycles between the start and end times,
|
||||||
|
// except on the final loop, where we clamp to the final position.
|
||||||
|
if loop_index < instance.num_loops {
|
||||||
|
dt = dt.rem_euclid(loop_time);
|
||||||
|
}
|
||||||
|
dt += instance.loop_start;
|
||||||
|
dt = dt.min(instance.loop_end);
|
||||||
|
dt * 1000.0
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1057,6 +1099,12 @@ impl AudioBackend for WebAudioBackend {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn tick(&mut self) {
|
||||||
|
// Update the output timestamp.
|
||||||
|
// We do this once per frame to avoid spamming it in `get_sound_position`.
|
||||||
|
self.output_time = get_audio_output_timestamp(&self.context);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen(raw_module = "./ruffle-imports.js")]
|
#[wasm_bindgen(raw_module = "./ruffle-imports.js")]
|
||||||
|
@ -1070,6 +1118,11 @@ extern "C" {
|
||||||
left_data: Option<&[f32]>,
|
left_data: Option<&[f32]>,
|
||||||
right_data: Option<&[f32]>,
|
right_data: Option<&[f32]>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// Imported JS method to call `AudioContext.getOutputTimestamp` because
|
||||||
|
/// it is not yet available in `web_sys`.
|
||||||
|
#[wasm_bindgen(js_name = "getAudioOutputTimestamp")]
|
||||||
|
fn get_audio_output_timestamp(context: &web_sys::AudioContext) -> f64;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Janky resmapling code.
|
// Janky resmapling code.
|
||||||
|
|
Loading…
Reference in New Issue