audio: Support loading external MP3s

This commit is contained in:
Mike Welsh 2022-04-12 16:09:21 -07:00
parent 93607aa86e
commit 3137306975
6 changed files with 160 additions and 8 deletions

View File

@ -29,6 +29,7 @@ pub use error::Error;
pub use function::ExecutionReason;
pub use globals::context_menu::make_context_menu_state;
pub use globals::shared_object::flush;
pub use globals::sound::start as start_sound;
pub use globals::system::SystemProperties;
pub use object::array_object::ArrayObject;
pub use object::script_object::ScriptObject;

View File

@ -48,7 +48,7 @@ mod point;
mod rectangle;
mod selection;
pub(crate) mod shared_object;
mod sound;
pub(crate) mod sound;
mod stage;
pub(crate) mod string;
pub(crate) mod system;

View File

@ -6,6 +6,7 @@ use crate::avm1::error::Error;
use crate::avm1::property_decl::{define_properties_on, Declaration};
use crate::avm1::{Object, ScriptObject, SoundObject, TObject, Value};
use crate::avm_warn;
use crate::backend::navigator::Request;
use crate::character::Character;
use crate::display_object::{SoundTransform, TDisplayObject};
use gc_arena::MutationContext;
@ -240,11 +241,24 @@ fn id3<'gc>(
fn load_sound<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
_this: Object<'gc>,
_args: &[Value<'gc>],
this: Object<'gc>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
if activation.swf_version() >= 6 {
avm_warn!(activation, "Sound.loadSound: Unimplemented");
if let Some(sound) = this.as_sound_object() {
if let Some(url) = args.get(0) {
let url = url.coerce_to_string(activation)?;
let is_streaming = args
.get(1)
.unwrap_or(&Value::Undefined)
.as_bool(activation.swf_version());
let future = activation.context.load_manager.load_sound(
activation.context.player.clone(),
sound,
Request::get(url.to_utf8_lossy().into_owned()),
is_streaming,
);
activation.context.navigator.spawn_future(future);
}
}
Ok(Value::Undefined)
}
@ -356,7 +370,7 @@ fn set_volume<'gc>(
Ok(Value::Undefined)
}
fn start<'gc>(
pub fn start<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
this: Object<'gc>,
args: &[Value<'gc>],

View File

@ -43,8 +43,13 @@ pub enum RegisterError {
pub trait AudioBackend: Downcast {
fn play(&mut self);
fn pause(&mut self);
/// Registers an sound embedded in an SWF.
fn register_sound(&mut self, swf_sound: &swf::Sound) -> Result<SoundHandle, RegisterError>;
/// Registers MP3 audio from an external source.
fn register_mp3(&mut self, data: &[u8]) -> Result<SoundHandle, DecodeError>;
/// Plays a sound.
fn start_sound(
&mut self,
@ -174,6 +179,19 @@ impl AudioBackend for NullAudioBackend {
}))
}
fn register_mp3(&mut self, _data: &[u8]) -> Result<SoundHandle, DecodeError> {
Ok(self.sounds.insert(NullSound {
size: 0,
duration: 0.0,
format: swf::SoundFormat {
compression: swf::AudioCompression::Mp3,
sample_rate: 44100,
is_stereo: true,
is_16_bit: true,
},
}))
}
fn start_sound(
&mut self,
_sound: SoundHandle,

View File

@ -363,7 +363,7 @@ impl AudioMixer {
sound_instances.retain(|_, sound| sound.active);
}
/// Registers a sound with the audio mixer.
/// Registers an embedded SWF sound with the audio mixer.
pub fn register_sound(&mut self, swf_sound: &swf::Sound) -> Result<SoundHandle, RegisterError> {
// Slice off latency seek for MP3 data.
let (skip_sample_frames, data) = if swf_sound.format.compression == AudioCompression::Mp3 {
@ -385,6 +385,26 @@ impl AudioMixer {
Ok(self.sounds.insert(sound))
}
/// Registers an external MP3 with the audio mixer.
pub fn register_mp3(&mut self, data: &[u8]) -> Result<SoundHandle, DecodeError> {
let mut sound = Sound {
format: swf::SoundFormat {
compression: AudioCompression::Mp3,
// This value is ignored; the sample rate from the MP3 data itself will be used.
sample_rate: 44100,
is_stereo: true,
is_16_bit: true,
},
data: Arc::from(data),
num_sample_frames: 0, // TODO
skip_sample_frames: 0,
};
let data = Cursor::new(ArcAsRef(Arc::clone(&sound.data)));
let decoder = Self::make_seekable_decoder(&sound.format, data)?;
sound.num_sample_frames = decoder.count() as u32;
Ok(self.sounds.insert(sound))
}
/// Starts a timeline audio stream.
pub fn start_stream(
&mut self,
@ -871,6 +891,11 @@ macro_rules! impl_audio_mixer_backend {
self.$mixer.register_sound(swf_sound)
}
#[inline]
fn register_mp3(&mut self, data: &[u8]) -> Result<SoundHandle, DecodeError> {
self.$mixer.register_mp3(data)
}
#[inline]
fn start_stream(
&mut self,

View File

@ -3,7 +3,7 @@
use crate::avm1::Avm1;
use crate::avm1::ExecutionReason;
use crate::avm1::{Activation, ActivationIdentifier};
use crate::avm1::{Object, TObject, Value};
use crate::avm1::{Object, SoundObject, TObject, Value};
use crate::avm2::bytearray::ByteArrayStorage;
use crate::avm2::object::ByteArrayObject;
use crate::avm2::object::EventObject as Avm2EventObject;
@ -129,6 +129,9 @@ pub enum Error {
#[error("Invalid bitmap")]
InvalidBitmap(#[from] ruffle_render::error::Error),
#[error("Invalid sound: {0}")]
InvalidSound(#[from] crate::backend::audio::DecodeError),
#[error("Unexpected content of type {1}, expected {0}")]
UnexpectedData(ContentType, ContentType),
@ -179,6 +182,7 @@ impl<'gc> LoadManager<'gc> {
| Loader::Form { self_handle, .. }
| Loader::LoadVars { self_handle, .. }
| Loader::LoadURLLoader { self_handle, .. } => *self_handle = Some(handle),
Loader::Sound { self_handle, .. } => *self_handle = Some(handle),
}
handle
}
@ -306,6 +310,25 @@ impl<'gc> LoadManager<'gc> {
let loader = self.get_loader_mut(handle).unwrap();
loader.load_url_loader(player, request, data_format)
}
/// Kick off an audio load.
///
/// Returns the loader's async process, which you will need to spawn.
pub fn load_sound(
&mut self,
player: Weak<Mutex<Player>>,
target_object: SoundObject<'gc>,
request: Request,
is_streaming: bool,
) -> OwnedFuture<(), Error> {
let loader = Loader::Sound {
self_handle: None,
target_object,
};
let handle = self.add_loader(loader);
let loader = self.get_loader_mut(handle).unwrap();
loader.sound_loader(player, request, is_streaming)
}
}
impl<'gc> Default for LoadManager<'gc> {
@ -398,6 +421,16 @@ pub enum Loader<'gc> {
/// The target `URLLoader` to load data into.
target_object: Avm2Object<'gc>,
},
/// Loader that is loading an MP3 into an AVM1 Sound object.
Sound {
/// The handle to refer to this loader instance.
#[collect(require_static)]
self_handle: Option<Handle>,
/// The target AVM1 object to load the audio into.
target_object: SoundObject<'gc>,
},
}
impl<'gc> Loader<'gc> {
@ -914,6 +947,67 @@ impl<'gc> Loader<'gc> {
})
}
/// Creates a future for a Sound load call.
fn sound_loader(
&mut self,
player: Weak<Mutex<Player>>,
request: Request,
is_streaming: bool,
) -> OwnedFuture<(), Error> {
let handle = match self {
Loader::Sound { self_handle, .. } => self_handle.expect("Loader not self-introduced"),
_ => return Box::pin(async { Err(Error::NotLoadVarsLoader) }),
};
let player = player
.upgrade()
.expect("Could not upgrade weak reference to player");
Box::pin(async move {
let fetch = player.lock().unwrap().navigator().fetch(request);
let data = fetch.await;
// Fire the load handler.
player.lock().unwrap().update(|uc| {
let loader = uc.load_manager.get_loader(handle);
let sound_object = match loader {
Some(&Loader::Sound { target_object, .. }) => target_object,
None => return Err(Error::Cancelled),
_ => return Err(Error::NotLoadVarsLoader),
};
let success = data
.and_then(|data| {
let handle = uc.audio.register_mp3(&data.body)?;
sound_object.set_sound(uc.gc_context, Some(handle));
let duration = uc
.audio
.get_sound_duration(handle)
.map(|d| d.round() as u32);
sound_object.set_duration(uc.gc_context, duration);
Ok(())
})
.is_ok();
let mut activation =
Activation::from_stub(uc.reborrow(), ActivationIdentifier::root("[Loader]"));
let _ = sound_object.call_method(
"onLoad".into(),
&[success.into()],
&mut activation,
ExecutionReason::Special,
);
// Streaming sounds should auto-play.
if is_streaming {
crate::avm1::start_sound(&mut activation, sound_object.into(), &[])?;
}
Ok(())
})
})
}
/// Report a movie loader start event to script code.
fn movie_loader_start(handle: Index, uc: &mut UpdateContext<'_, 'gc, '_>) -> Result<(), Error> {
let me = uc.load_manager.get_loader_mut(handle);