diff --git a/core/src/avm1.rs b/core/src/avm1.rs index 837f45d04..0ab66dc0f 100644 --- a/core/src/avm1.rs +++ b/core/src/avm1.rs @@ -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; diff --git a/core/src/avm1/globals.rs b/core/src/avm1/globals.rs index 484af1626..28505183d 100644 --- a/core/src/avm1/globals.rs +++ b/core/src/avm1/globals.rs @@ -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; diff --git a/core/src/avm1/globals/sound.rs b/core/src/avm1/globals/sound.rs index fe604f870..468f184d8 100644 --- a/core/src/avm1/globals/sound.rs +++ b/core/src/avm1/globals/sound.rs @@ -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, 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>], diff --git a/core/src/backend/audio.rs b/core/src/backend/audio.rs index d137ee808..ddb658396 100644 --- a/core/src/backend/audio.rs +++ b/core/src/backend/audio.rs @@ -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; + /// Registers MP3 audio from an external source. + fn register_mp3(&mut self, data: &[u8]) -> Result; + /// Plays a sound. fn start_sound( &mut self, @@ -174,6 +179,19 @@ impl AudioBackend for NullAudioBackend { })) } + fn register_mp3(&mut self, _data: &[u8]) -> Result { + 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, diff --git a/core/src/backend/audio/mixer.rs b/core/src/backend/audio/mixer.rs index 692dd1e92..1564a9ed3 100644 --- a/core/src/backend/audio/mixer.rs +++ b/core/src/backend/audio/mixer.rs @@ -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 { // 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 { + 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 { + self.$mixer.register_mp3(data) + } + #[inline] fn start_stream( &mut self, diff --git a/core/src/loader.rs b/core/src/loader.rs index 6968c8444..85b953b61 100644 --- a/core/src/loader.rs +++ b/core/src/loader.rs @@ -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>, + 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, + + /// 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>, + 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);