From af9f5cf32db7804f640d9cc57f9f3643416ee847 Mon Sep 17 00:00:00 2001 From: Aaron Hill Date: Tue, 7 Feb 2023 11:59:45 -0600 Subject: [PATCH] core: Queue up Sound and SoundChannel methods during loading Flash supports calling `Sound.play`, `SoundChannel.stop`, and `SoundChannel.soundTransform` while a sound load is in progress (e.g. immediately after calling `Sound.load`). To support this, we queue up information inside `SoundObject` and `SoundChannelObject` when a load is in progress. When a load completes, we trigger any queued `Sound.play` and `SoundChannel.stop` calls, and apply the most recent `SoundChannel.soundTransform` --- core/src/avm2/globals/flash/media/sound.rs | 74 +++++---- .../avm2/globals/flash/media/soundchannel.rs | 22 +-- core/src/avm2/object.rs | 15 +- core/src/avm2/object/sound_object.rs | 150 ++++++++++++++---- core/src/avm2/object/soundchannel_object.rs | 128 +++++++++++++-- core/src/loader.rs | 8 +- 6 files changed, 285 insertions(+), 112 deletions(-) diff --git a/core/src/avm2/globals/flash/media/sound.rs b/core/src/avm2/globals/flash/media/sound.rs index c46b956d4b87..47e5025d6df9 100644 --- a/core/src/avm2/globals/flash/media/sound.rs +++ b/core/src/avm2/globals/flash/media/sound.rs @@ -3,7 +3,7 @@ use crate::avm2::activation::Activation; use crate::avm2::class::{Class, ClassAttributes}; use crate::avm2::method::{Method, NativeMethodImpl}; -use crate::avm2::object::{sound_allocator, Object, SoundChannelObject, TObject}; +use crate::avm2::object::{sound_allocator, Object, QueuedPlay, SoundChannelObject, TObject}; use crate::avm2::value::Value; use crate::avm2::Error; use crate::avm2::Multiname; @@ -12,7 +12,7 @@ use crate::avm2::QName; use crate::backend::navigator::Request; use crate::character::Character; use crate::display_object::SoundTransform; -use crate::{avm2_stub_getter, avm2_stub_method}; +use crate::{avm2_stub_constructor, avm2_stub_getter, avm2_stub_method}; use gc_arena::{GcCell, MutationContext}; use swf::{SoundEvent, SoundInfo}; @@ -20,12 +20,16 @@ use swf::{SoundEvent, SoundInfo}; pub fn instance_init<'gc>( activation: &mut Activation<'_, 'gc>, this: Option>, - _args: &[Value<'gc>], + args: &[Value<'gc>], ) -> Result, Error<'gc>> { if let Some(this) = this { activation.super_init(this, &[])?; - if this.as_sound().is_none() { + if !args.is_empty() { + avm2_stub_constructor!(activation, "flash.media.Sound", "with arguments"); + } + + if let Some(sound_object) = this.as_sound_object() { let class_object = this .instance_of() .ok_or("Attempted to construct Sound on a bare object.")?; @@ -42,7 +46,8 @@ pub fn instance_init<'gc>( .library_for_movie_mut(movie) .character_by_id(symbol) { - this.set_sound(activation.context.gc_context, *sound); + let sound = *sound; + sound_object.set_sound(&mut activation.context, sound)?; } else { tracing::warn!("Attempted to construct subclass of Sound, {}, which is associated with non-Sound character {}", class_object.inner_class_definition().read().name().local_name(), symbol); } @@ -68,10 +73,13 @@ pub fn bytes_total<'gc>( this: Option>, _args: &[Value<'gc>], ) -> Result, Error<'gc>> { - if let Some(sound) = this.and_then(|this| this.as_sound()) { - if let Some(length) = activation.context.audio.get_sound_size(sound) { - return Ok((length).into()); + if let Some(sound) = this.and_then(|this| this.as_sound_object()) { + if let Some(sound_handle) = sound.sound_handle() { + if let Some(length) = activation.context.audio.get_sound_size(sound_handle) { + return Ok((length).into()); + } } + return Ok(0.into()); } Ok(Value::Undefined) @@ -105,10 +113,13 @@ pub fn length<'gc>( this: Option>, _args: &[Value<'gc>], ) -> Result, Error<'gc>> { - if let Some(sound) = this.and_then(|this| this.as_sound()) { - if let Some(duration) = activation.context.audio.get_sound_duration(sound) { - return Ok((duration).into()); + if let Some(sound) = this.and_then(|this| this.as_sound_object()) { + if let Some(sound_handle) = sound.sound_handle() { + if let Some(duration) = activation.context.audio.get_sound_duration(sound_handle) { + return Ok((duration).into()); + } } + return Ok(0.into()); } Ok(Value::Undefined) @@ -120,7 +131,7 @@ pub fn play<'gc>( this: Option>, args: &[Value<'gc>], ) -> Result, Error<'gc>> { - if let Some(sound) = this.and_then(|this| this.as_sound()) { + if let Some(sound_object) = this.and_then(|this| this.as_sound_object()) { let position = args .get(0) .cloned() @@ -133,12 +144,6 @@ pub fn play<'gc>( .coerce_to_i32(activation)?; let sound_transform = args.get(2).cloned().unwrap_or(Value::Null).as_object(); - if let Some(duration) = activation.context.audio.get_sound_duration(sound) { - if position > duration { - return Ok(Value::Null); - } - } - let in_sample = if position > 0.0 { Some((position / 1000.0 * 44100.0) as u32) } else { @@ -153,23 +158,29 @@ pub fn play<'gc>( envelope: None, }; - if let Some(instance) = activation - .context - .start_sound(sound, &sound_info, None, None) - { - if let Some(sound_transform) = sound_transform { - let st = SoundTransform::from_avm2_object(activation, sound_transform)?; - activation.context.set_local_sound_transform(instance, st); - } - - let sound_channel = SoundChannelObject::from_sound_instance(activation, instance)?; + let sound_transform = if let Some(sound_transform) = sound_transform { + Some(SoundTransform::from_avm2_object( + activation, + sound_transform, + )?) + } else { + None + }; - activation - .context - .attach_avm2_sound_channel(instance, sound_channel); + let sound_channel = SoundChannelObject::empty(activation)?; + let queued_play = QueuedPlay { + position, + sound_info, + sound_transform, + sound_channel, + }; + if sound_object.play(queued_play, activation)? { return Ok(sound_channel.into()); } + // If we start playing a loaded sound with an invalid position, + // this method returns `null` + return Ok(Value::Null); } Ok(Value::Null) @@ -202,6 +213,7 @@ pub fn load<'gc>( args: &[Value<'gc>], ) -> Result, Error<'gc>> { if let Some(this) = this { + // FIXME - don't allow replacing an existing sound let url_request = match args.get(0) { Some(Value::Object(request)) => request, // This should never actually happen diff --git a/core/src/avm2/globals/flash/media/soundchannel.rs b/core/src/avm2/globals/flash/media/soundchannel.rs index 1d0a256aae6c..ff20e54832f7 100644 --- a/core/src/avm2/globals/flash/media/soundchannel.rs +++ b/core/src/avm2/globals/flash/media/soundchannel.rs @@ -89,11 +89,7 @@ pub fn sound_transform<'gc>( _args: &[Value<'gc>], ) -> Result, Error<'gc>> { if let Some(channel) = this.and_then(|this| this.as_sound_channel()) { - let dobj_st = channel - .instance() - .and_then(|instance| activation.context.local_sound_transform(instance)) - .cloned() - .unwrap_or_default(); + let dobj_st = channel.sound_transform(activation).unwrap_or_default(); return Ok(dobj_st.into_avm2_object(activation)?.into()); } @@ -107,10 +103,7 @@ pub fn set_sound_transform<'gc>( this: Option>, args: &[Value<'gc>], ) -> Result, Error<'gc>> { - if let Some(instance) = this - .and_then(|this| this.as_sound_channel()) - .and_then(|channel| channel.instance()) - { + if let Some(sound_channel) = this.and_then(|this| this.as_sound_channel()) { let as3_st = args .get(0) .cloned() @@ -118,9 +111,7 @@ pub fn set_sound_transform<'gc>( .coerce_to_object(activation)?; let dobj_st = SoundTransform::from_avm2_object(activation, as3_st)?; - activation - .context - .set_local_sound_transform(instance, dobj_st); + sound_channel.set_sound_transform(activation, dobj_st); } Ok(Value::Undefined) @@ -132,11 +123,8 @@ pub fn stop<'gc>( this: Option>, _args: &[Value<'gc>], ) -> Result, Error<'gc>> { - if let Some(instance) = this - .and_then(|this| this.as_sound_channel()) - .and_then(|channel| channel.instance()) - { - activation.context.stop_sound(instance); + if let Some(sound_channel) = this.and_then(|this| this.as_sound_channel()) { + sound_channel.stop(activation); } Ok(Value::Undefined) diff --git a/core/src/avm2/object.rs b/core/src/avm2/object.rs index c1b14e0773c4..682399d2385b 100644 --- a/core/src/avm2/object.rs +++ b/core/src/avm2/object.rs @@ -17,7 +17,6 @@ use crate::avm2::Error; use crate::avm2::Multiname; use crate::avm2::Namespace; use crate::avm2::QName; -use crate::backend::audio::{SoundHandle, SoundInstanceHandle}; use crate::bitmap::bitmap_data::{BitmapData, BitmapDataWrapper}; use crate::display_object::DisplayObject; use crate::html::TextFormat; @@ -81,7 +80,7 @@ pub use crate::avm2::object::proxy_object::{proxy_allocator, ProxyObject}; pub use crate::avm2::object::qname_object::{qname_allocator, QNameObject}; pub use crate::avm2::object::regexp_object::{regexp_allocator, RegExpObject}; pub use crate::avm2::object::script_object::{ScriptObject, ScriptObjectData}; -pub use crate::avm2::object::sound_object::{sound_allocator, SoundObject}; +pub use crate::avm2::object::sound_object::{sound_allocator, QueuedPlay, SoundData, SoundObject}; pub use crate::avm2::object::soundchannel_object::{soundchannel_allocator, SoundChannelObject}; pub use crate::avm2::object::stage3d_object::{stage_3d_allocator, Stage3DObject}; pub use crate::avm2::object::stage_object::{stage_allocator, StageObject}; @@ -1087,25 +1086,15 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into> + Clone + Copy } /// Unwrap this object's sound handle. - fn as_sound(self) -> Option { + fn as_sound_object(self) -> Option> { None } - /// Associate the object with a particular sound handle. - /// - /// This does nothing if the object is not a sound. - fn set_sound(self, _mc: MutationContext<'gc, '_>, _sound: SoundHandle) {} - /// Unwrap this object's sound instance handle. fn as_sound_channel(self) -> Option> { None } - /// Associate the object with a particular sound instance handle. - /// - /// This does nothing if the object is not a sound channel. - fn set_sound_instance(self, _mc: MutationContext<'gc, '_>, _sound: SoundInstanceHandle) {} - /// Unwrap this object's bitmap data fn as_bitmap_data(&self) -> Option>> { None diff --git a/core/src/avm2/object/sound_object.rs b/core/src/avm2/object/sound_object.rs index 41e2bce45a9d..c739a54ce2e2 100644 --- a/core/src/avm2/object/sound_object.rs +++ b/core/src/avm2/object/sound_object.rs @@ -6,9 +6,14 @@ use crate::avm2::object::{ClassObject, Object, ObjectPtr, TObject}; use crate::avm2::value::Value; use crate::avm2::Error; use crate::backend::audio::SoundHandle; +use crate::context::UpdateContext; +use crate::display_object::SoundTransform; use core::fmt; use gc_arena::{Collect, GcCell, MutationContext}; use std::cell::{Ref, RefMut}; +use swf::SoundInfo; + +use super::SoundChannelObject; /// A class instance allocator that allocates Sound objects. pub fn sound_allocator<'gc>( @@ -19,7 +24,12 @@ pub fn sound_allocator<'gc>( Ok(SoundObject(GcCell::allocate( activation.context.gc_context, - SoundObjectData { base, sound: None }, + SoundObjectData { + base, + sound_data: SoundData::NotLoaded { + queued_plays: Vec::new(), + }, + }, )) .into()) } @@ -36,44 +46,125 @@ impl fmt::Debug for SoundObject<'_> { } } -#[derive(Clone, Collect)] +#[derive(Collect)] #[collect(no_drop)] pub struct SoundObjectData<'gc> { /// Base script object base: ScriptObjectData<'gc>, /// The sound this object holds. + sound_data: SoundData<'gc>, +} + +#[derive(Collect)] +#[collect(no_drop)] +pub enum SoundData<'gc> { + NotLoaded { + queued_plays: Vec>, + }, + Loaded { + #[collect(require_static)] + sound: SoundHandle, + }, +} + +#[derive(Clone, Collect)] +#[collect(no_drop)] +pub struct QueuedPlay<'gc> { + #[collect(require_static)] + pub sound_info: SoundInfo, #[collect(require_static)] - sound: Option, + pub sound_transform: Option, + pub sound_channel: SoundChannelObject<'gc>, + pub position: f64, } impl<'gc> SoundObject<'gc> { - /// Convert a bare sound into it's object representation. - /// - /// In AS3, library sounds are accessed through subclasses of `Sound`. As a - /// result, this needs to take the subclass so that the returned object is - /// an instance of the correct class. - pub fn from_sound( + pub fn sound_handle(self) -> Option { + let this = self.0.read(); + match this.sound_data { + SoundData::NotLoaded { .. } => None, + SoundData::Loaded { sound } => Some(sound), + } + } + + /// Returns `true` if a `SoundChannel` should be returned back to the AVM2 caller. + pub fn play( + self, + queued: QueuedPlay<'gc>, activation: &mut Activation<'_, 'gc>, - class: ClassObject<'gc>, + ) -> Result> { + let mut this = self.0.write(activation.context.gc_context); + match &mut this.sound_data { + SoundData::NotLoaded { queued_plays } => { + queued_plays.push(queued); + // We don't know the length yet, so return the `SoundChannel` + Ok(true) + } + SoundData::Loaded { sound } => play_queued(queued, *sound, activation), + } + } + + pub fn set_sound( + self, + context: &mut UpdateContext<'_, 'gc>, sound: SoundHandle, - ) -> Result, Error<'gc>> { - let base = ScriptObjectData::new(class); - - let mut sound_object: Object<'gc> = SoundObject(GcCell::allocate( - activation.context.gc_context, - SoundObjectData { - base, - sound: Some(sound), - }, - )) - .into(); - sound_object.install_instance_slots(activation); + ) -> Result<(), Error<'gc>> { + let mut this = self.0.write(context.gc_context); + let mut activation = Activation::from_nothing(context.reborrow()); + match &mut this.sound_data { + SoundData::NotLoaded { queued_plays } => { + for queued in std::mem::take(queued_plays) { + play_queued(queued, sound, &mut activation)?; + } + this.sound_data = SoundData::Loaded { sound }; + } + SoundData::Loaded { sound: old_sound } => { + panic!("Tried to replace sound {old_sound:?} with {sound:?}") + } + } + Ok(()) + } +} - class.call_native_init(Some(sound_object), &[], activation)?; +/// Returns `true` if the sound had a valid position, and `false` otherwise +fn play_queued<'gc>( + queued: QueuedPlay<'gc>, + sound: SoundHandle, + activation: &mut Activation<'_, 'gc>, +) -> Result> { + if let Some(duration) = activation.context.audio.get_sound_duration(sound) { + if queued.position > duration { + tracing::error!( + "Sound.play: position={} is greater than duration={}", + queued.position, + duration + ); + return Ok(false); + } + } - Ok(sound_object) + if let Some(instance) = activation + .context + .start_sound(sound, &queued.sound_info, None, None) + { + if let Some(sound_transform) = queued.sound_transform { + activation + .context + .set_local_sound_transform(instance, sound_transform); + } + + queued + .sound_channel + .as_sound_channel() + .unwrap() + .set_sound_instance(activation, instance); + + activation + .context + .attach_avm2_sound_channel(instance, queued.sound_channel); } + Ok(true) } impl<'gc> TObject<'gc> for SoundObject<'gc> { @@ -93,14 +184,7 @@ impl<'gc> TObject<'gc> for SoundObject<'gc> { Ok(Object::from(*self).into()) } - fn as_sound(self) -> Option { - self.0.read().sound - } - - /// Associate the object with a particular sound handle. - /// - /// This does nothing if the object is not a sound. - fn set_sound(self, mc: MutationContext<'gc, '_>, sound: SoundHandle) { - self.0.write(mc).sound = Some(sound); + fn as_sound_object(self) -> Option> { + Some(self) } } diff --git a/core/src/avm2/object/soundchannel_object.rs b/core/src/avm2/object/soundchannel_object.rs index 6f2f227cd9db..a901e6685c24 100644 --- a/core/src/avm2/object/soundchannel_object.rs +++ b/core/src/avm2/object/soundchannel_object.rs @@ -6,6 +6,7 @@ use crate::avm2::object::{ClassObject, Object, ObjectPtr, TObject}; use crate::avm2::value::Value; use crate::avm2::Error; use crate::backend::audio::SoundInstanceHandle; +use crate::display_object::SoundTransform; use core::fmt; use gc_arena::{Collect, GcCell, MutationContext}; use std::cell::{Ref, RefMut}; @@ -21,7 +22,10 @@ pub fn soundchannel_allocator<'gc>( activation.context.gc_context, SoundChannelObjectData { base, - sound: None, + sound_channel_data: SoundChannelData::NotLoaded { + sound_transform: None, + should_stop: false, + }, position: 0.0, }, )) @@ -40,7 +44,7 @@ impl fmt::Debug for SoundChannelObject<'_> { } } -#[derive(Clone, Collect)] +#[derive(Collect)] #[collect(no_drop)] pub struct SoundChannelObjectData<'gc> { /// Base script object @@ -48,18 +52,27 @@ pub struct SoundChannelObjectData<'gc> { /// The sound this object holds. #[collect(require_static)] - sound: Option, + sound_channel_data: SoundChannelData, /// Position of the last playing sound in milliseconds. position: f64, } +#[derive(Collect)] +#[collect(require_static)] +pub enum SoundChannelData { + NotLoaded { + sound_transform: Option, + should_stop: bool, + }, + Loaded { + sound_instance: SoundInstanceHandle, + }, +} + impl<'gc> SoundChannelObject<'gc> { /// Convert a bare sound instance into it's object representation. - pub fn from_sound_instance( - activation: &mut Activation<'_, 'gc>, - sound: SoundInstanceHandle, - ) -> Result> { + pub fn empty(activation: &mut Activation<'_, 'gc>) -> Result> { let class = activation.avm2().classes().soundchannel; let base = ScriptObjectData::new(class); @@ -67,7 +80,10 @@ impl<'gc> SoundChannelObject<'gc> { activation.context.gc_context, SoundChannelObjectData { base, - sound: Some(sound), + sound_channel_data: SoundChannelData::NotLoaded { + sound_transform: None, + should_stop: false, + }, position: 0.0, }, )); @@ -78,11 +94,6 @@ impl<'gc> SoundChannelObject<'gc> { Ok(sound_object) } - /// Return the backend handle to the currently playing sound instance. - pub fn instance(self) -> Option { - self.0.read().sound - } - /// Return the position of the playing sound in seconds. pub fn position(self) -> f64 { self.0.read().position @@ -92,6 +103,93 @@ impl<'gc> SoundChannelObject<'gc> { pub fn set_position(self, mc: MutationContext<'gc, '_>, value: f64) { self.0.write(mc).position = value; } + + pub fn instance(self) -> Option { + match &self.0.read().sound_channel_data { + SoundChannelData::NotLoaded { .. } => None, + SoundChannelData::Loaded { sound_instance } => Some(*sound_instance), + } + } + + pub fn set_sound_instance( + self, + activation: &mut Activation<'_, 'gc>, + instance: SoundInstanceHandle, + ) { + let mut this = self.0.write(activation.context.gc_context); + match &mut this.sound_channel_data { + SoundChannelData::NotLoaded { + sound_transform, + should_stop, + } => { + if let Some(sound_transform) = sound_transform { + activation + .context + .set_local_sound_transform(instance, sound_transform.clone()); + } + + if *should_stop { + activation.context.stop_sound(instance); + } + this.sound_channel_data = SoundChannelData::Loaded { + sound_instance: instance, + } + } + SoundChannelData::Loaded { sound_instance } => { + panic!( + "Tried to replace loaded sound instance {sound_instance:?} with {instance:?}" + ) + } + } + } + + pub fn sound_transform(self, activation: &mut Activation<'_, 'gc>) -> Option { + let this = self.0.read(); + match &this.sound_channel_data { + SoundChannelData::NotLoaded { + sound_transform, .. + } => sound_transform.clone(), + SoundChannelData::Loaded { sound_instance } => activation + .context + .local_sound_transform(*sound_instance) + .cloned(), + } + } + + pub fn set_sound_transform( + self, + activation: &mut Activation<'_, 'gc>, + new_sound_transform: SoundTransform, + ) { + let mut this = self.0.write(activation.context.gc_context); + match &mut this.sound_channel_data { + SoundChannelData::NotLoaded { + sound_transform, .. + } => { + *sound_transform = Some(new_sound_transform); + } + SoundChannelData::Loaded { sound_instance } => { + activation + .context + .set_local_sound_transform(*sound_instance, new_sound_transform); + } + } + } + + pub fn stop(self, activation: &mut Activation<'_, 'gc>) { + let mut this = self.0.write(activation.context.gc_context); + match &mut this.sound_channel_data { + SoundChannelData::NotLoaded { + sound_transform: _, + should_stop, + } => { + *should_stop = true; + } + SoundChannelData::Loaded { sound_instance } => { + activation.context.stop_sound(*sound_instance); + } + } + } } impl<'gc> TObject<'gc> for SoundChannelObject<'gc> { @@ -114,8 +212,4 @@ impl<'gc> TObject<'gc> for SoundChannelObject<'gc> { fn as_sound_channel(self) -> Option> { Some(self) } - - fn set_sound_instance(self, mc: MutationContext<'gc, '_>, sound: SoundInstanceHandle) { - self.0.write(mc).sound = Some(sound); - } } diff --git a/core/src/loader.rs b/core/src/loader.rs index eb5eef6b587d..ab9ff7ec87aa 100644 --- a/core/src/loader.rs +++ b/core/src/loader.rs @@ -1207,7 +1207,13 @@ impl<'gc> Loader<'gc> { match response { Ok(response) => { let handle = uc.audio.register_mp3(&response.body)?; - sound_object.set_sound(uc.gc_context, handle); + if let Err(e) = sound_object + .as_sound_object() + .expect("Not a sound object") + .set_sound(uc, handle) + { + tracing::error!("Encountered AVM2 error when setting sound: {}", e); + } // FIXME - the "open" event should be fired earlier, and not fired in case of ioerror. let mut activation = Avm2Activation::from_nothing(uc.reborrow());