From 6daf70df87a42b7aa2c0b67576d124d1b94fbddf Mon Sep 17 00:00:00 2001 From: ChrisCPI <92183829+ChrisCPI@users.noreply.github.com> Date: Sat, 13 Jun 2026 16:15:52 -0400 Subject: [PATCH] avm1: Improve behavior of `Sound` when `loadSound()` has been called Corrects several behaviors of the Sound object when loading external sound. Some of the important changes: - On desktop, when loading local sounds, execution is paused until the load is completed, matching the behavior of the FP desktop projector. - The playback queue has been removed. This was a misinterpretation of how loaded sounds behave on the desktop projector. - Added a separate `ExternalSound` struct, which will be available (and reset) whenever `loadSound` has been called. Stores several things, such as the individual sound transform for the Sound, whether it is streaming, and whether it will autoplay. - When calling `start()`, per the docs, the second argument (loops) is now ignored when the sound is streaming. --- core/src/avm1/globals/sound.rs | 473 +++++++++++------- core/src/backend/audio.rs | 26 +- core/src/context.rs | 10 + core/src/loader.rs | 108 ++-- .../sound_load_multiple_instances/output.txt | 19 + .../sound_load_multiple_instances/sound.mp3 | Bin 0 -> 8586 bytes .../sound_load_multiple_instances/test.swf | Bin 0 -> 516 bytes .../sound_load_multiple_instances/test.toml | 23 + .../avm1/sound_load_multiple_remote/README.md | 12 + .../localhost/noise.mp3 | Bin 0 -> 4180 bytes .../sound_load_multiple_remote/output.txt | 6 + .../avm1/sound_load_multiple_remote/test.as | 15 + .../avm1/sound_load_multiple_remote/test.swf | Bin 0 -> 390 bytes .../avm1/sound_load_multiple_remote/test.toml | 11 + .../swfs/avm1/sound_load_props/output.txt | 112 +++++ .../tests/swfs/avm1/sound_load_props/test.as | 86 ++++ .../tests/swfs/avm1/sound_load_props/test.swf | Bin 0 -> 1455 bytes .../swfs/avm1/sound_load_props/test.toml | 7 + .../swfs/avm1/sound_load_start/output.txt | 3 + .../tests/swfs/avm1/sound_load_start/test.as | 6 + .../tests/swfs/avm1/sound_load_start/test.swf | Bin 213 -> 247 bytes .../avm1/sound_load_start_remote/README.md | 12 + .../localhost/noise.mp3 | Bin 0 -> 4180 bytes .../avm1/sound_load_start_remote/output.txt | 1 + .../swfs/avm1/sound_load_start_remote/test.as | 18 + .../avm1/sound_load_start_remote/test.swf | Bin 0 -> 380 bytes .../avm1/sound_load_start_remote/test.toml | 11 + .../output.txt | 0 .../sound.mp3 | Bin 0 -> 8586 bytes .../test.swf | Bin 0 -> 106 bytes .../test.toml | 13 + .../README.md | 12 + .../localhost/noise.mp3 | Bin 0 -> 4180 bytes .../output.txt | 3 + .../sound_load_streaming_stop_remote/test.as | 12 + .../sound_load_streaming_stop_remote/test.swf | Bin 0 -> 293 bytes .../test.toml | 11 + .../avm1/sound_mixed_attach_load/noise.mp3 | Bin 0 -> 4180 bytes .../avm1/sound_mixed_attach_load/output.txt | 13 + .../avm1/sound_mixed_attach_load/sound.mp3 | Bin 0 -> 8586 bytes .../avm1/sound_mixed_attach_load/test.swf | Bin 0 -> 8370 bytes .../avm1/sound_mixed_attach_load/test.toml | 19 + .../swfs/avm1/sound_start_stop/test.toml | 2 +- .../Sound-v6/output.ruffle.txt | 27 +- .../actionscript.all/Sound-v6/test.toml | 3 + .../Sound-v7/output.ruffle.txt | 27 +- .../actionscript.all/Sound-v7/test.toml | 3 + .../Sound-v8/output.ruffle.txt | 27 +- .../actionscript.all/Sound-v8/test.toml | 3 + 49 files changed, 890 insertions(+), 244 deletions(-) create mode 100644 tests/tests/swfs/avm1/sound_load_multiple_instances/output.txt create mode 100644 tests/tests/swfs/avm1/sound_load_multiple_instances/sound.mp3 create mode 100644 tests/tests/swfs/avm1/sound_load_multiple_instances/test.swf create mode 100644 tests/tests/swfs/avm1/sound_load_multiple_instances/test.toml create mode 100644 tests/tests/swfs/avm1/sound_load_multiple_remote/README.md create mode 100644 tests/tests/swfs/avm1/sound_load_multiple_remote/localhost/noise.mp3 create mode 100644 tests/tests/swfs/avm1/sound_load_multiple_remote/output.txt create mode 100644 tests/tests/swfs/avm1/sound_load_multiple_remote/test.as create mode 100644 tests/tests/swfs/avm1/sound_load_multiple_remote/test.swf create mode 100644 tests/tests/swfs/avm1/sound_load_multiple_remote/test.toml create mode 100644 tests/tests/swfs/avm1/sound_load_props/output.txt create mode 100644 tests/tests/swfs/avm1/sound_load_props/test.as create mode 100644 tests/tests/swfs/avm1/sound_load_props/test.swf create mode 100644 tests/tests/swfs/avm1/sound_load_props/test.toml create mode 100644 tests/tests/swfs/avm1/sound_load_start_remote/README.md create mode 100644 tests/tests/swfs/avm1/sound_load_start_remote/localhost/noise.mp3 create mode 100644 tests/tests/swfs/avm1/sound_load_start_remote/output.txt create mode 100644 tests/tests/swfs/avm1/sound_load_start_remote/test.as create mode 100644 tests/tests/swfs/avm1/sound_load_start_remote/test.swf create mode 100644 tests/tests/swfs/avm1/sound_load_start_remote/test.toml create mode 100644 tests/tests/swfs/avm1/sound_load_stops_when_dereferenced/output.txt create mode 100644 tests/tests/swfs/avm1/sound_load_stops_when_dereferenced/sound.mp3 create mode 100644 tests/tests/swfs/avm1/sound_load_stops_when_dereferenced/test.swf create mode 100644 tests/tests/swfs/avm1/sound_load_stops_when_dereferenced/test.toml create mode 100644 tests/tests/swfs/avm1/sound_load_streaming_stop_remote/README.md create mode 100644 tests/tests/swfs/avm1/sound_load_streaming_stop_remote/localhost/noise.mp3 create mode 100644 tests/tests/swfs/avm1/sound_load_streaming_stop_remote/output.txt create mode 100644 tests/tests/swfs/avm1/sound_load_streaming_stop_remote/test.as create mode 100644 tests/tests/swfs/avm1/sound_load_streaming_stop_remote/test.swf create mode 100644 tests/tests/swfs/avm1/sound_load_streaming_stop_remote/test.toml create mode 100644 tests/tests/swfs/avm1/sound_mixed_attach_load/noise.mp3 create mode 100644 tests/tests/swfs/avm1/sound_mixed_attach_load/output.txt create mode 100644 tests/tests/swfs/avm1/sound_mixed_attach_load/sound.mp3 create mode 100644 tests/tests/swfs/avm1/sound_mixed_attach_load/test.swf create mode 100644 tests/tests/swfs/avm1/sound_mixed_attach_load/test.toml diff --git a/core/src/avm1/globals/sound.rs b/core/src/avm1/globals/sound.rs index 1a7897808cc82..1ea352717fec5 100644 --- a/core/src/avm1/globals/sound.rs +++ b/core/src/avm1/globals/sound.rs @@ -5,7 +5,7 @@ use std::fmt; use std::io::Cursor; use gc_arena::barrier::unlock; -use gc_arena::{Collect, Gc, Mutation, RefLock}; +use gc_arena::{Collect, Gc, Lock, Mutation}; use id3::Tag; use ruffle_macros::istr; @@ -23,32 +23,90 @@ use crate::display_object::{DisplayObject, SoundTransform, TDisplayObject}; use crate::string::AvmString; use crate::{avm_warn, avm1_stub}; -#[derive(Debug, Collect)] +/// Represents information about the Sound when `loadSound()` has been called. +#[derive(Clone, Copy, Collect)] #[collect(no_drop)] -struct QueuedPlay<'gc> { - sound_object: Object<'gc>, - start_offset: f64, - loops: u16, -} +pub struct ExternalSound<'gc>(Gc<'gc, ExternalSoundData>); -#[derive(Debug, Collect)] +#[derive(Collect)] #[collect(no_drop)] -enum SoundState<'gc> { - /// Empty sound object, no sound playback allowed. - Empty, - - /// Sound is loading, plays can be queued. - Loading { queued_plays: Vec> }, - - /// Sound is loaded, plays can be started immediately. - Loaded { - /// The sound that is attached to this object. - #[collect(require_static)] - sound: SoundHandle, - }, +struct ExternalSoundData { + /// Whether this sound is set to be streaming. + /// This will be true if `Sound.loadSound` was called with `isStreaming` of `true`. + /// A streaming sound can only have a single active instance. + is_streaming: Cell, + + /// Whether this sound will autoplay + will_autoplay: Cell, + + /// A dedicated sound transform for this sound. + transform: Cell, + + /// Whether we are currently loading a sound. + is_loading: Cell, + + /// The load generation associated with this external sound. + /// Used to prevent multiple simultaneous loads on one Sound. + load_id: Cell, +} + +impl<'gc> ExternalSound<'gc> { + pub fn empty(mc: &Mutation<'gc>, is_streaming: bool, load_id: u32) -> ExternalSound<'gc> { + ExternalSound(Gc::new( + mc, + ExternalSoundData { + is_streaming: Cell::new(is_streaming), + will_autoplay: Cell::new(is_streaming), + transform: Cell::new(SoundTransform::default()), + is_loading: Cell::new(true), + load_id: Cell::new(load_id), + }, + )) + } + + pub fn is_streaming(self) -> bool { + self.0.is_streaming.get() + } + + pub fn will_autoplay(self) -> bool { + self.0.will_autoplay.get() + } + + pub fn set_will_autoplay(self, will_autoplay: bool) { + self.0.will_autoplay.set(will_autoplay); + } + + pub fn transform(self) -> SoundTransform { + self.0.transform.get() + } + + pub fn set_transform( + self, + context: &mut UpdateContext<'gc>, + sound: Option, + sound_transform: SoundTransform, + ) { + self.0.transform.set(sound_transform); + + if let Some(sound) = sound { + context.set_sound_transform_with_handle(sound, sound_transform); + } + } + + pub fn is_loading(self) -> bool { + self.0.is_loading.get() + } + + pub fn set_is_loading(self, is_loading: bool) { + self.0.is_loading.set(is_loading) + } + + pub fn load_id(self) -> u32 { + self.0.load_id.get() + } } -/// A `Sound` object that is tied to a sound from the `AudioBackend``. +/// A `Sound` object that is tied to a sound from the `AudioBackend`. #[derive(Clone, Copy, Collect)] #[collect(no_drop)] pub struct Sound<'gc>(Gc<'gc, SoundData<'gc>>); @@ -56,7 +114,8 @@ pub struct Sound<'gc>(Gc<'gc, SoundData<'gc>>); #[derive(Collect)] #[collect(no_drop)] struct SoundData<'gc> { - state: RefLock>, + /// The sound that is attached to this object. + sound: Cell>, /// The instance of the last played sound on this object. sound_instance: Cell>, @@ -72,17 +131,19 @@ struct SoundData<'gc> { /// Duration of the currently attached sound in milliseconds. duration: Cell>, - /// Whether this sound is an external streaming MP3. - /// This will be true if `Sound.loadSound` was called with `isStreaming` of `true`. - /// A streaming sound can only have a single active instance. - is_streaming: Cell, + /// Data for playing external sounds on this object. + /// This is `Some` when this sound has had `loadSound()` called on it, + /// and therefore enters a state of being "separated" from its + /// original owner (even if it was controlling global sound initially), + /// and can then only ever control externally loaded sounds. + external: Lock>>, } impl fmt::Debug for Sound<'_> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.debug_struct("Sound") .field("ptr", &Gc::as_ptr(self.0)) - .field("state", &self.0.state.borrow()) + .field("sound", &self.0.sound.get()) .field("sound_instance", &self.0.sound_instance.get()) .field("target", &self.0.target) .finish() @@ -94,12 +155,12 @@ impl<'gc> Sound<'gc> { Sound(Gc::new( mc, SoundData { - state: RefLock::new(SoundState::Empty), + sound: Cell::new(None), sound_instance: Cell::new(None), target, position: Cell::new(0), duration: Cell::new(None), - is_streaming: Cell::new(false), + external: Lock::new(None), }, )) } @@ -113,10 +174,41 @@ impl<'gc> Sound<'gc> { } pub fn sound(self) -> Option { - if let SoundState::Loaded { sound } = *self.0.state.borrow() { - Some(sound) - } else { - None + self.0.sound.get() + } + + pub fn set_sound( + self, + activation: &mut Activation<'_, 'gc>, + sound_object: Object<'gc>, + sound: Option, + ) { + self.0.sound.set(sound); + + // `position` and `duration` are only defined when a sound is loaded. + if !sound_object.has_property(activation, istr!("position")) + || !sound_object.has_property(activation, istr!("duration")) + { + let fn_proto = activation.prototypes().function; + let getter_position = + FunctionObject::native(position).build(activation.strings(), fn_proto, None); + let getter_duration = + FunctionObject::native(duration).build(activation.strings(), fn_proto, None); + + sound_object.add_property( + activation.gc(), + istr!("position"), + getter_position, + None, + Attribute::DONT_ENUM | Attribute::DONT_DELETE, + ); + sound_object.add_property( + activation.gc(), + istr!("duration"), + getter_duration, + None, + Attribute::DONT_ENUM | Attribute::DONT_DELETE, + ); } } @@ -128,8 +220,14 @@ impl<'gc> Sound<'gc> { self.0.sound_instance.set(sound_instance); } + /// Tries to resolve the display object at the `target` path and returns it. + /// + /// Note that this will also return `None` if this object is set to + /// play external audio, even if the target display object exists. pub fn owner(self, activation: &mut Activation<'_, 'gc>) -> Option> { - if let Some(target) = self.0.target { + if let Some(target) = self.0.target + && !self.is_external() + { let start_clip = activation.target_clip_or_root(); activation .resolve_target_display_object(start_clip, Value::String(target), false) @@ -143,20 +241,24 @@ impl<'gc> Sound<'gc> { self.0.target.is_none() } - /// Returns `true` if this sound is attached to the global sound - /// (initial target was `Null` or `Undefined`), or if the target - /// *currently* resolves to a valid display object. + /// Returns `true` if any one of the following is true: + /// - We are controlling global sound. + /// - We are playing an external MP3 sound. + /// - The `target` *currently* resolves to a valid display object. pub fn has_valid_owner(self, activation: &mut Activation<'_, 'gc>) -> bool { - self.use_global_sound() || self.owner(activation).is_some() + self.use_global_sound() || self.is_external() || self.owner(activation).is_some() } - /// Gets the sound transform for this sound. - /// If this sound is set to use global sound, then the - /// global sound transform will be returned. Otherwise, - /// it will try to resolve the owner and get its sound - /// transform. If it can't, then `None` is returned. + /// Gets the sound transform for this sound. It goes through the following in order: + /// 1. If we have our own sound transform (from `loadSound()` being called), return that. + /// 2. If not, then if we are controlling global sound, return the global sound transform. + /// 3. If not, then return the sound transform of our `owner`, if we have one. + /// 4. If not, then return `None`. pub fn sound_transform(self, activation: &mut Activation<'_, 'gc>) -> Option { - if self.use_global_sound() { + // `external` must be checked first since it takes precedence over every other case + if let Some(external) = self.external() { + Some(external.transform()) + } else if self.use_global_sound() { Some(*activation.context.global_sound_transform()) } else if let Some(owner) = self.owner(activation) { Some(owner.base().sound_transform()) @@ -173,106 +275,19 @@ impl<'gc> Sound<'gc> { self.0.position.set(position); } - pub fn is_streaming(self) -> bool { - self.0.is_streaming.get() + pub fn external(self) -> Option> { + self.0.external.get() } - pub fn set_is_streaming(self, is_streaming: bool) { - self.0.is_streaming.set(is_streaming); + pub fn set_external(self, mc: &Mutation<'gc>, external_sound: ExternalSound<'gc>) { + let write = Gc::write(mc, self.0); + unlock!(write, SoundData, external).set(Some(external_sound)); } - fn play(self, play: QueuedPlay<'gc>, activation: &mut Activation<'_, 'gc>) { - let write = Gc::write(activation.context.gc(), self.0); - let sound_handle = match &mut *unlock!(write, SoundData, state).borrow_mut() { - SoundState::Empty => { - tracing::warn!("Ignoring Sound playback, because it's not loaded."); - return; - } - SoundState::Loading { queued_plays } => { - queued_plays.push(play); - return; - } - SoundState::Loaded { sound } => *sound, - }; - - if self.is_streaming() { - // Streaming MP3s can only have a single active instance. - if let Some(sound_instance) = self.sound_instance() { - activation.context.stop_sound(sound_instance); - } - } - - let owner = self.owner(activation); - let sound_instance = activation.context.start_sound( - sound_handle, - &swf::SoundInfo { - event: swf::SoundEvent::Start, - in_sample: if play.start_offset > 0.0 { - Some((play.start_offset * 44100.0) as u32) - } else { - None - }, - out_sample: None, - num_loops: play.loops, - envelope: None, - }, - None, - owner, - Some(play.sound_object), - ); - if sound_instance.is_some() { - self.set_sound_instance(sound_instance); - } - } - - fn set_is_loading(self, context: &mut UpdateContext<'gc>) { - // All queued plays are discarded at this point. - let new_data = SoundState::Loading { - queued_plays: Vec::new(), - }; - unlock!(Gc::write(context.gc(), self.0), SoundData, state).replace(new_data); - } - - pub fn load_sound( - self, - activation: &mut Activation<'_, 'gc>, - sound_object: Object<'gc>, - sound: SoundHandle, - ) { - let new_data = SoundState::Loaded { sound }; - let write = Gc::write(activation.gc(), self.0); - let old_data = unlock!(write, SoundData, state).replace(new_data); - - if let SoundState::Loading { queued_plays } = old_data { - for play in queued_plays { - self.play(play, activation); - } - } - - if !sound_object.has_property(activation, istr!("position")) - || !sound_object.has_property(activation, istr!("duration")) - { - let fn_proto = activation.prototypes().function; - let getter_position = - FunctionObject::native(position).build(activation.strings(), fn_proto, None); - let getter_duration = - FunctionObject::native(duration).build(activation.strings(), fn_proto, None); - - sound_object.add_property( - activation.gc(), - istr!("position"), - getter_position, - None, - Attribute::DONT_ENUM | Attribute::DONT_DELETE, - ); - sound_object.add_property( - activation.gc(), - istr!("duration"), - getter_duration, - None, - Attribute::DONT_ENUM | Attribute::DONT_DELETE, - ); - } + /// Whether this Sound is set to play external audio + /// (`loadSound()` has been called on it). + pub fn is_external(self) -> bool { + self.external().is_some() } pub fn load_id3( @@ -361,8 +376,8 @@ impl<'gc> Sound<'gc> { const PROTO_DECLS: StaticDeclarations = declare_static_properties! { // Note: id3 is not a built-in property. See [`Sound::load_id3`]. - // Note: duration is defined later. See [`Sound::load_sound`]. - // Note: position is defined later. See [`Sound::load_sound`]. + // Note: duration is defined later. See [`Sound::set_sound`]. + // Note: position is defined later. See [`Sound::set_sound`]. "getPan" => method(get_pan; DONT_ENUM | DONT_DELETE | READ_ONLY); "getTransform" => method(get_transform; DONT_ENUM | DONT_DELETE | READ_ONLY); "getVolume" => method(get_volume; DONT_ENUM | DONT_DELETE | READ_ONLY); @@ -431,7 +446,11 @@ fn attach_sound<'gc>( .unwrap_or(&Value::Undefined) .coerce_to_string(activation)?; - let movie = if sound.use_global_sound() { + // A Sound that has had `loadSound()` called on it + // can never again have sound characters attached to it. + // This check works because `owner` always returns `None` + // when `is_external()` is true. + let movie = if sound.use_global_sound() && !sound.is_external() { activation.base_clip().avm1_root().movie() } else if let Some(owner) = sound.owner(activation) { owner.movie() @@ -445,8 +464,7 @@ fn attach_sound<'gc>( .library_for_movie_mut(movie) .character_by_export_name(name) { - sound.load_sound(activation, this, sound_handle); - sound.set_is_streaming(false); + sound.set_sound(activation, this, Some(sound_handle)); sound.set_duration( activation .context @@ -572,20 +590,67 @@ fn load_sound<'gc>( .get(1) .unwrap_or(&Value::Undefined) .as_bool(activation.swf_version()); - if is_streaming { - // Streaming MP3s can only have a single active instance. + + let mut load_id = 0; + + if let Some(external) = sound.external() { + // Any external sound instances that are playing + // are stopped when `loadSound` is called again, streaming or not. // (Previous `attachSound` instances will continue to play.) - if let Some(sound_instance) = sound.sound_instance() { - activation.context.stop_sound(sound_instance); + if let Some(sound) = sound.sound() { + activation.context.stop_sounds_with_handle(sound); + } + + // If a load on this Sound is already in progress, + // onLoad gets called with success as `false`. + if external.is_loading() { + let _ = this.call_method( + istr!("onLoad"), + &[false.into()], + activation, + ExecutionReason::Special, + ); + } + + // Add 1 to the load counter for this Sound + load_id = external.load_id() + 1; + } + + sound.set_external( + activation.gc(), + ExternalSound::empty(activation.gc(), is_streaming, load_id), + ); + + let request_url = url.to_utf8_lossy().into_owned(); + + // Local files are loaded synchronously on the FP desktop projector; + // that is, execution is paused until the load has completed. + // TODO: This check should probably be implemented into `NavigatorBackend`. + #[cfg(not(target_family = "wasm"))] + { + let is_blocking = activation + .context + .navigator + .resolve_url(&request_url) + .ok() + .is_some_and(|resolved| resolved.scheme() == "file"); + + if is_blocking { + let _ = crate::loader::load_sound_avm1_blocking( + activation.context, + this, + Request::get(request_url), + load_id, + ); + return Ok(Value::Undefined); } } - sound.set_is_streaming(is_streaming); - sound.set_is_loading(activation.context); + let future = crate::loader::load_sound_avm1( activation.context, this, - Request::get(url.to_utf8_lossy().into_owned()), - is_streaming, + Request::get(request_url), + load_id, ); activation.context.navigator.spawn_future(future); } @@ -620,7 +685,13 @@ fn set_pan<'gc>( .unwrap_or(&0.into()) .coerce_to_f64(activation)? .clamp_to_i32(); - if let Some(owner) = sound.owner(activation) { + if let Some(external) = sound.external() { + external.set_transform( + activation.context, + sound.sound(), + external.transform().with_pan(pan), + ); + } else if let Some(owner) = sound.owner(activation) { let transform = owner.base().sound_transform(); owner.set_sound_transform(activation.context, transform.with_pan(pan)); } else if sound.use_global_sound() { @@ -647,14 +718,19 @@ fn set_transform<'gc>( let owner = sound.owner(activation); - if owner.is_none() && !sound.use_global_sound() { + if owner.is_none() && !sound.use_global_sound() && !sound.is_external() { return Ok(Value::Undefined); } - let mut transform = owner.map_or_else( - || *activation.context.global_sound_transform(), - |owner| owner.base().sound_transform(), - ); + // Check first if we have our own sound transform. + // If we don't, then see if we currently have a valid owner. + // If not, then use the global sound transform. + let mut transform = sound.external().map(|e| e.transform()).unwrap_or_else(|| { + owner.map_or_else( + || *activation.context.global_sound_transform(), + |owner| owner.base().sound_transform(), + ) + }); if obj.has_own_property(activation, istr!("ll")) { transform.left_to_left = obj @@ -678,7 +754,9 @@ fn set_transform<'gc>( .coerce_to_i32(activation)?; } - if let Some(owner) = owner { + if let Some(external) = sound.external() { + external.set_transform(activation.context, sound.sound(), transform); + } else if let Some(owner) = owner { owner.set_sound_transform(activation.context, transform); } else { activation.context.set_global_sound_transform(transform); @@ -698,7 +776,13 @@ fn set_volume<'gc>( .unwrap_or(&0.into()) .coerce_to_f64(activation)? .clamp_to_i32(); - if let Some(owner) = sound.owner(activation) { + if let Some(external) = sound.external() { + let transform = SoundTransform { + volume, + ..external.transform() + }; + external.set_transform(activation.context, sound.sound(), transform); + } else if let Some(owner) = sound.owner(activation) { let transform = SoundTransform { volume, ..owner.base().sound_transform() @@ -723,17 +807,49 @@ pub fn start<'gc>( ) -> Result, Error<'gc>> { if let NativeObject::Sound(sound) = this.native() { if sound.has_valid_owner(activation) { + let is_streaming = sound.external().is_some_and(|e| e.is_streaming()); + let start_offset = args.get(0).unwrap_or(&0.into()).coerce_to_f64(activation)?; - let loops = args.get(1).unwrap_or(&1.into()).coerce_to_f64(activation)?; + let loops = args + .get(1) + // Loops are ignored if the sound is streaming, per the docs. + .filter(|_| !is_streaming) + .unwrap_or(&1.into()) + .coerce_to_f64(activation)?; // TODO: Handle loops > u16::MAX. let loops = (loops as u16).max(1); - let play = QueuedPlay { - sound_object: this, - start_offset, - loops, - }; - sound.play(play, activation); + if let Some(sound_handle) = sound.sound() { + if is_streaming { + // Streaming MP3s can only have a single active instance. + if let Some(sound_instance) = sound.sound_instance() { + activation.context.stop_sound(sound_instance); + } + } + let owner = sound.owner(activation); + let sound_instance = activation.context.start_sound( + sound_handle, + &swf::SoundInfo { + event: swf::SoundEvent::Start, + in_sample: if start_offset > 0.0 { + Some((start_offset * 44100.0) as u32) + } else { + None + }, + out_sample: None, + num_loops: loops, + envelope: None, + }, + sound.external().map(|e| e.transform()), + owner, + Some(this), + ); + if sound_instance.is_some() { + sound.set_sound_instance(sound_instance); + } + } else { + avm_warn!(activation, "Sound.start: No sound is attached"); + } } } else { avm_warn!(activation, "Sound.start: Invalid sound"); @@ -748,11 +864,15 @@ fn stop<'gc>( args: &[Value<'gc>], ) -> Result, Error<'gc>> { if let NativeObject::Sound(sound) = this.native() { + if let Some(external) = sound.external() { + external.set_will_autoplay(false); + } if let Some(name) = args.get(0) { // Usage 1: Stop all instances of a particular sound, using the name parameter. + // If this parameter is given, but we're playing external sound, then do nothing. let name = name.coerce_to_string(activation)?; - let movie = if sound.use_global_sound() { + let movie = if sound.use_global_sound() && !sound.is_external() { activation.base_clip().avm1_root().movie() } else if let Some(owner) = sound.owner(activation) { owner.movie() @@ -766,20 +886,27 @@ fn stop<'gc>( .library_for_movie_mut(movie) .character_by_export_name(name) { - // TODO: This isn't entirely correct. We should only + // FIXME: This isn't entirely correct. We should only // stop sounds with this name on this particular sound object; // right now we're stopping *all* sound objects playing this sound. activation.context.stop_sounds_with_handle(sound); } else { avm_warn!(activation, "Sound.stop: Sound '{}' not found", name); } + } else if sound.is_external() { + // Usage 2: If there is no name and we're playing external sound, + // then stop any instances of that sound. + if let Some(sound) = sound.sound() { + activation.context.stop_sounds_with_handle(sound); + } + sound.set_sound_instance(None); } else if let Some(owner) = sound.owner(activation) { - // Usage 2: If there is no name and we have an owner, + // Usage 3: If there is no name and we have an owner, // then stop all sound running within a given clip. activation.context.stop_sounds_with_display_object(owner); sound.set_sound_instance(None); } else if sound.use_global_sound() { - // Usage 3: If there is no name and we are linked to global sound, + // Usage 4: If there is no name and we are linked to global sound, // this call acts like `stopAllSounds()`. activation.context.stop_all_sounds(); } diff --git a/core/src/backend/audio.rs b/core/src/backend/audio.rs index 41f6840ab56b2..0f63f6b114baa 100644 --- a/core/src/backend/audio.rs +++ b/core/src/backend/audio.rs @@ -732,6 +732,25 @@ impl<'gc> AudioManager<'gc> { } } + pub fn set_sound_transform_with_handle( + &mut self, + sound: SoundHandle, + sound_transform: display_object::SoundTransform, + ) { + let mut changed = false; + + for other in &mut self.sounds { + if other.sound == Some(sound) { + other.transform = sound_transform; + changed = true; + } + } + + if changed { + self.transforms_dirty = true; + } + } + /// Returns the number of seconds that a timeline audio stream should buffer before playing. /// /// Currently unused by Ruffle. @@ -839,9 +858,10 @@ pub struct SoundInstance<'gc> { /// The local sound transform of this sound. /// - /// Only AVM2 sounds have a local sound transform. In AVM1, sound instances - /// instead get the sound transform of the display object they're - /// associated with. + /// AVM2 sounds only have a local sound transform. In AVM1, sound instances + /// have a local transform if they have loaded a sound with `loadSound()`, + /// otherwise they get the sound transform of the display object + /// they're associated with. #[collect(require_static)] transform: display_object::SoundTransform, diff --git a/core/src/context.rs b/core/src/context.rs index b24e67dc50190..310a750ea6ed2 100644 --- a/core/src/context.rs +++ b/core/src/context.rs @@ -270,6 +270,16 @@ impl<'gc> UpdateContext<'gc> { .set_local_sound_transform(instance, sound_transform); } + /// Set the local sound transform for each instance in a handle. + pub fn set_sound_transform_with_handle( + &mut self, + sound: SoundHandle, + sound_transform: SoundTransform, + ) { + self.audio_manager + .set_sound_transform_with_handle(sound, sound_transform); + } + pub fn start_sound( &mut self, sound: SoundHandle, diff --git a/core/src/loader.rs b/core/src/loader.rs index 3023b045c0fe9..64cd9d8ee2227 100644 --- a/core/src/loader.rs +++ b/core/src/loader.rs @@ -1373,7 +1373,7 @@ pub fn load_sound_avm1<'gc>( uc: &UpdateContext<'gc>, sound_object: Object<'gc>, request: Request, - is_streaming: bool, + load_id: u32, ) -> OwnedFuture<(), Error> { let player = uc.player_handle(); let sound_object = ObjectHandle::stash(uc, sound_object); @@ -1383,47 +1383,87 @@ pub fn load_sound_avm1<'gc>( let response = wait_for_full_response(fetch).await; // Fire the load handler. - player.lock().unwrap().update(|uc| { - let sound_object = sound_object.fetch(uc); + player + .lock() + .unwrap() + .update(|uc| load_sound_avm1_data(uc, sound_object, response, load_id)) + }) +} - let NativeObject::Sound(sound) = sound_object.native() else { - panic!("NativeObject must be Sound"); - }; +/// Kick off a synchronous AVM1 audio load. +/// This will block execution until the sound has been loaded. +#[cfg(not(target_family = "wasm"))] +pub fn load_sound_avm1_blocking<'gc>( + uc: &mut UpdateContext<'gc>, + sound_object: Object<'gc>, + request: Request, + load_id: u32, +) -> Result<(), Error> { + let sound_object = ObjectHandle::stash(uc, sound_object); - let mut activation = Activation::from_stub(uc, ActivationIdentifier::root("[Loader]")); + let fetch = uc.navigator.fetch(request); + let response = futures::executor::block_on(wait_for_full_response(fetch)); - let success = response - .map_err(|e| e.error) - .and_then(|(body, _, _, _)| { - let handle = activation.context.audio.register_mp3(&body)?; - sound.load_sound(&mut activation, sound_object, handle); - sound.set_duration(Some(0)); - sound.load_id3(&mut activation, sound_object, &body)?; - let duration = activation - .context - .audio - .get_sound_duration(handle) - .map(|d| d.as_millis().round() as u32); - sound.set_duration(duration); - Ok(()) - }) - .is_ok(); + load_sound_avm1_data(uc, sound_object, response, load_id) +} - let _ = sound_object.call_method( - istr!("onLoad"), - &[success.into()], - &mut activation, - ExecutionReason::Special, - ); +fn load_sound_avm1_data<'gc>( + uc: &mut UpdateContext<'gc>, + sound_object: ObjectHandle, + response: Result<(Vec, String, u16, bool), ErrorResponse>, + load_id: u32, +) -> Result<(), Error> { + let sound_object = sound_object.fetch(uc); - // Streaming sounds should auto-play. - if is_streaming { - crate::avm1::start_sound(&mut activation, sound_object, &[])?; - } + let NativeObject::Sound(sound) = sound_object.native() else { + panic!("NativeObject must be Sound"); + }; + + let mut activation = Activation::from_stub(uc, ActivationIdentifier::root("[Loader]")); + + let external = sound + .external() + .expect("Loaded sound should have external sound data"); + + // If the load_id has changed, then it means loadSound() was called again + // before this load completed, so we need to stop here. + if external.load_id() != load_id { + return Ok(()); + } + let success = response + .map_err(|e| e.error) + .and_then(|(body, _, _, _)| { + let handle = activation.context.audio.register_mp3(&body)?; + sound.set_sound(&mut activation, sound_object, Some(handle)); + sound.set_duration(Some(0)); + sound.load_id3(&mut activation, sound_object, &body)?; + let duration = activation + .context + .audio + .get_sound_duration(handle) + .map(|d| d.as_millis().round() as u32); + sound.set_duration(duration); Ok(()) }) - }) + .is_ok(); + + external.set_is_loading(false); + + let _ = sound_object.call_method( + istr!("onLoad"), + &[success.into()], + &mut activation, + ExecutionReason::Special, + ); + + // Streaming sounds should auto-play, + // unless stop() has been called before it finished loading. + if external.will_autoplay() { + crate::avm1::start_sound(&mut activation, sound_object, &[])?; + } + + Ok(()) } /// Kick off an AVM2 audio load. diff --git a/tests/tests/swfs/avm1/sound_load_multiple_instances/output.txt b/tests/tests/swfs/avm1/sound_load_multiple_instances/output.txt new file mode 100644 index 0000000000000..33d15cd96c60c --- /dev/null +++ b/tests/tests/swfs/avm1/sound_load_multiple_instances/output.txt @@ -0,0 +1,19 @@ +frame 1 - loading sound.mp3 +frame 2 +frame 3 - starting more instances +frame 4 +frame 5 - muting volume +frame 6 +frame 7 - calling loadSound again +frame 8 +frame 9 - calling loadSound with streaming +frame 10 +frame 11 - starting more instances +frame 12 +frame 13 - calling loadSound, stopped +frame 14 +frame 15 - start +frame 16 +frame 17 - starting another +frame 18 - stopping +frame 19 diff --git a/tests/tests/swfs/avm1/sound_load_multiple_instances/sound.mp3 b/tests/tests/swfs/avm1/sound_load_multiple_instances/sound.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..893eeff8dcb782694a1bdf65065d96a8106e553e GIT binary patch literal 8586 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

-RC_;mqMoHR7QL#|Dh>=@H$460L}@yS$`M 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 Gqej7$?E0Gk literal 0 HcmV?d00001 diff --git a/tests/tests/swfs/avm1/sound_load_multiple_instances/test.toml b/tests/tests/swfs/avm1/sound_load_multiple_instances/test.toml new file mode 100644 index 0000000000000..cc0b3858a50ed --- /dev/null +++ b/tests/tests/swfs/avm1/sound_load_multiple_instances/test.toml @@ -0,0 +1,23 @@ +num_frames = 19 + +[player_options] +with_audio = true + +[audio_assertions.non_streaming_vol_50] +frames = [2] +min_max_amplitude = 0.29 +max_amplitude = 0.31 + +[audio_assertions.non_streaming_multiple] +frames = [4] +min_max_amplitude = 1.76 +max_amplitude = 1.78 + +[audio_assertions.single_vol_full] +frames = [8,10,12,16] +min_max_amplitude = 0.59 +max_amplitude = 0.61 + +[audio_assertions.silence] +frames = [6,14,19] +max_amplitude = 0.0 diff --git a/tests/tests/swfs/avm1/sound_load_multiple_remote/README.md b/tests/tests/swfs/avm1/sound_load_multiple_remote/README.md new file mode 100644 index 0000000000000..618c96cb9b745 --- /dev/null +++ b/tests/tests/swfs/avm1/sound_load_multiple_remote/README.md @@ -0,0 +1,12 @@ + # Running the test + + +To be able to serve the MP3 file, run the command `python -m http.server -d localhost` from this directory. Then, run 'test.swf' in either Flash Player or the Ruffle Desktop player. + +When running under flash player, you'll need to allow the SWF to make network connections. On Linux, this can be done by creating the file `/etc/adobe/FlashPlayerTrust/test.cfg` with the following contents: + +``` +/ancestor/of/swf/path +``` + +where `ancestor/of/swf/path` is any path that's an ancestor of the path of `test.swf` (e.g. `/home/username/`) diff --git a/tests/tests/swfs/avm1/sound_load_multiple_remote/localhost/noise.mp3 b/tests/tests/swfs/avm1/sound_load_multiple_remote/localhost/noise.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..862f41452ee420c77b6e151132e8235a042ba864 GIT binary patch literal 4180 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 I&M!;;2eVKzJpcdz literal 0 HcmV?d00001 diff --git a/tests/tests/swfs/avm1/sound_load_multiple_remote/output.txt b/tests/tests/swfs/avm1/sound_load_multiple_remote/output.txt new file mode 100644 index 0000000000000..b5ae83f9347c5 --- /dev/null +++ b/tests/tests/swfs/avm1/sound_load_multiple_remote/output.txt @@ -0,0 +1,6 @@ +loading sound +loading again +onLoad false +after loading again +onLoad true +Sound complete diff --git a/tests/tests/swfs/avm1/sound_load_multiple_remote/test.as b/tests/tests/swfs/avm1/sound_load_multiple_remote/test.as new file mode 100644 index 0000000000000..4f7e8adfbf9c9 --- /dev/null +++ b/tests/tests/swfs/avm1/sound_load_multiple_remote/test.as @@ -0,0 +1,15 @@ +var sound = new Sound(); +sound.onSoundComplete = function() { + trace("Sound complete"); +}; +sound.onLoad = function(s) { + trace("onLoad " + s); + sound.start(0,9); +}; + +trace("loading sound"); +sound.loadSound("http://localhost:8000/noise.mp3", true); +sound.stop(); +trace("loading again"); +sound.loadSound("http://localhost:8000/noise.mp3", true); +trace("after loading again"); diff --git a/tests/tests/swfs/avm1/sound_load_multiple_remote/test.swf b/tests/tests/swfs/avm1/sound_load_multiple_remote/test.swf new file mode 100644 index 0000000000000000000000000000000000000000..d5721b18727a982d041e636c17a9a25479a8d3f9 GIT binary patch literal 390 zcmZ`zu};G<6g<00;}B5QsSFHR$iP4%v7icxO08f)LTU#VBxJRrjg;C^Z6v+`F?IC9 z0$-XsF$ETu_uk#TyZ3G$X1zB8yuyD1dY`~C!K7~)jA>oh*PTZ@zklHfO1_8%W^I+C z?PIyF%3SA;jputP#hv5}xE4cczR@TxRfuKma#mVb!!#}BoR=%9^zbsvvQ)@I<>%|_ zqTP14l$OE-;iB2pp)GHD&95%2SQe`fuB6hY3bD Fe+Q=GQ_KJW literal 0 HcmV?d00001 diff --git a/tests/tests/swfs/avm1/sound_load_multiple_remote/test.toml b/tests/tests/swfs/avm1/sound_load_multiple_remote/test.toml new file mode 100644 index 0000000000000..a3e11406d5c93 --- /dev/null +++ b/tests/tests/swfs/avm1/sound_load_multiple_remote/test.toml @@ -0,0 +1,11 @@ +num_ticks = 150 + +[player_options] +with_audio = true + +[[compilers]] +type = "Rascal" +target = "test.swf" +scripts = ["test.as"] +swf_version = 15 +use_network = true diff --git a/tests/tests/swfs/avm1/sound_load_props/output.txt b/tests/tests/swfs/avm1/sound_load_props/output.txt new file mode 100644 index 0000000000000..02aee9cdc4ba9 --- /dev/null +++ b/tests/tests/swfs/avm1/sound_load_props/output.txt @@ -0,0 +1,112 @@ +// s_mc.getVolume() +undefined +// s_mc.getPan() +undefined +// s_mc.getTransform() +undefined + +// s_mc.getVolume() +50 +// s_mc.getPan() +90 +// s_mc.getTransform() +[object Object] + rl: 40 + rr: 30 + lr: 20 + ll: 10 + +// s_mc.loadSound() + +// s_mc.getVolume() +50 +// s_mc.getPan() +90 +// s_mc.getTransform() +[object Object] + rl: 40 + rr: 30 + lr: 20 + ll: 10 + +// s_mc.loadSound("") + +// s_mc.getVolume() +200 +// s_mc.getPan() +50 +// s_mc.getTransform() +[object Object] + rl: 0 + rr: 100 + lr: 0 + ll: 50 + +// s_mc.loadSound("invalid.mp3") + +// s_mc.getVolume() +100 +// s_mc.getPan() +0 +// s_mc.getTransform() +[object Object] + rl: 0 + rr: 100 + lr: 0 + ll: 100 + +// s_mc.getVolume() +1 +// s_mc.getPan() +97 +// s_mc.getTransform() +[object Object] + rl: 6 + rr: 5 + lr: 4 + ll: 3 + +// s_mc2.getVolume() +50 +// s_mc2.getPan() +90 +// s_mc2.getTransform() +[object Object] + rl: 40 + rr: 30 + lr: 20 + ll: 10 + +// s_glob.getVolume() +100 +// s_glob.getPan() +0 +// s_glob.getTransform() +[object Object] + rl: 0 + rr: 100 + lr: 0 + ll: 100 + +// s_mc2.getVolume() +10 +// s_mc2.getPan() +87 +// s_mc2.getTransform() +[object Object] + rl: 16 + rr: 15 + lr: 14 + ll: 13 + +// s_mc.getVolume() +1 +// s_mc.getPan() +97 +// s_mc.getTransform() +[object Object] + rl: 6 + rr: 5 + lr: 4 + ll: 3 + diff --git a/tests/tests/swfs/avm1/sound_load_props/test.as b/tests/tests/swfs/avm1/sound_load_props/test.as new file mode 100644 index 0000000000000..579d3dde5a1f3 --- /dev/null +++ b/tests/tests/swfs/avm1/sound_load_props/test.as @@ -0,0 +1,86 @@ +var s_mc = new Sound("mc"); +var s_mc2 = new Sound("mc"); +var s_glob = new Sound(); + +function traceProps(s, n) { + trace("// s_" + n + ".getVolume()"); + trace(s.getVolume()); + trace("// s_" + n + ".getPan()"); + trace(s.getPan()); + trace("// s_" + n + ".getTransform()"); + var t = s.getTransform() + trace(t); + for (var i in t) { + trace(" " + i + ": " + t[i]); + } + trace(""); +} + +s_mc.loadSound("invalid.mp3"); + +traceProps(s_mc, "mc"); + +createEmptyMovieClip("mc", 1); + +s_mc.setVolume(50); +s_mc.setPan(-25); +var o = new Object(); +o.ll = 10 +o.lr = 20 +o.rr = 30 +o.rl = 40 +s_mc.setTransform(o); + +traceProps(s_mc, "mc"); + +trace("// s_mc.loadSound()"); +trace(""); +s_mc.loadSound(); + +traceProps(s_mc, "mc"); + +trace("// s_mc.loadSound(\"\")"); +trace(""); +s_mc.loadSound(""); + +s_mc.setVolume(200); +s_mc.setPan(50); + +traceProps(s_mc, "mc"); + +trace("// s_mc.loadSound(\"invalid.mp3\")"); +trace(""); +s_mc.loadSound("invalid.mp3"); + +traceProps(s_mc, "mc"); + +s_mc.setVolume(1); +s_mc.setPan(-2); +var o = new Object(); +o.ll = 3 +o.lr = 4 +o.rr = 5 +o.rl = 6 +s_mc.setTransform(o); + +traceProps(s_mc, "mc"); + +traceProps(s_mc2, "mc2"); + +traceProps(s_glob, "glob"); + +s_mc2.loadSound("invalid.mp3"); + +s_mc2.setVolume(10); +s_mc2.setPan(-12); +var o = new Object(); +o.ll = 13 +o.lr = 14 +o.rr = 15 +o.rl = 16 +s_mc2.setTransform(o); + +traceProps(s_mc2, "mc2"); +traceProps(s_mc, "mc"); + +fscommand("quit"); diff --git a/tests/tests/swfs/avm1/sound_load_props/test.swf b/tests/tests/swfs/avm1/sound_load_props/test.swf new file mode 100644 index 0000000000000000000000000000000000000000..cd9f1f2d7f1a8bf91be606e53c0fce2d23b5140a GIT binary patch literal 1455 zcmb7^O>YuW6o%jVp!WjhquR=cwPF`ojiCwd8jVfR5*ONrT3wPUj16W!a0VJ(8>1_i zBrZ&hf55+C;t%l;xE9Zyp>S_Y8znHA^PGFn`<`=e*m<#^`5@g2U9CBOP38R!2!NF>6!fdaEiY4JB!+$%K+zXnoLN6sV+W zRHNqs0gN>0kH6@+3Bf?fEC>Z=nvpzjQe+yY`LtR6tmEfccuH1BY48+JGpYc-xP4E= z897dZXCu>5mZDs|Hnl1WXepuU#8uJf6>j8 z@iJi&T8U+^kSS7H5dyrxQdDDi28wK11tqqof->7sfUI=&9Q&lVDrPGNL~YT2n$m?0 z)7on%JIH%dRuMy@Hh)kR8Be-DJrvPBo!gm_lERv#@K47;s^VbhsQ&q%nHy9?nYpQg z5~F_&c}1BmC_pxF-pr}z>pY`gBVrYvRoUzc(qbCQWV+&B#4<6SO!xP+&lq2Lw=#-p sUSwGnlvqv$WtLZfY+%yoyq->S5pt30RRAaoNZ1`Yr;Sfec8BCYeCveJSg;(Q|X~z3knwOK~N%im9$w! zNV1Zw;6Jdxz>5g}({yK7v4VS;+4nJTW(z(CnkS&;;PVOk4Om&0Jy$uv?-Gw#p^%#_ z#?(}qnLd!|IuS}(_T}dS3l-%`n?^E<@huiI7s%wQ5O3+ai$t?FCR!+wqY!E)lT9kH zXQ*M%QKbt$2ssx-fb}icrs^Iz@k66j0yEksx4sx(&qs#WbDmKqJI#0WzXJb%rVZnv xKfWT*MtjEK1xBgT25y)d&9j^NdgxPo2uOM->3qo5(g^SGy-T96nLiz+OA=2WbD97E literal 213 zcmV;`04o1OS5psU0RRAaoU349jb~usU%I{Fo+|Y2{DW{NZcPtvx0@#K=Mo|^6WuiO-3L=s1*oFjv(wN PVija?0Ez$rWbi*1f$UtH diff --git a/tests/tests/swfs/avm1/sound_load_start_remote/README.md b/tests/tests/swfs/avm1/sound_load_start_remote/README.md new file mode 100644 index 0000000000000..618c96cb9b745 --- /dev/null +++ b/tests/tests/swfs/avm1/sound_load_start_remote/README.md @@ -0,0 +1,12 @@ + # Running the test + + +To be able to serve the MP3 file, run the command `python -m http.server -d localhost` from this directory. Then, run 'test.swf' in either Flash Player or the Ruffle Desktop player. + +When running under flash player, you'll need to allow the SWF to make network connections. On Linux, this can be done by creating the file `/etc/adobe/FlashPlayerTrust/test.cfg` with the following contents: + +``` +/ancestor/of/swf/path +``` + +where `ancestor/of/swf/path` is any path that's an ancestor of the path of `test.swf` (e.g. `/home/username/`) diff --git a/tests/tests/swfs/avm1/sound_load_start_remote/localhost/noise.mp3 b/tests/tests/swfs/avm1/sound_load_start_remote/localhost/noise.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..862f41452ee420c77b6e151132e8235a042ba864 GIT binary patch literal 4180 zcmcK7`#+Qa{|E4EGnzx0&0#r?jabx3#Jh6398--VhiJ1YGkhdEsNU8NWmvY^n5a~% zHyW`@Dis=0DuxR6E;@Om!khB0kM`NV|3J6f`*!=jetq4Z*Xz1p*W-G=UYF_zLID6U zM@Wvb5rC*GYhXbMz}*kT5W|+{qwm|M*KI@(V3)jRF7cUXF^)a`35No#0v(jlS3k&_ zBxp)Dj2OQ8bz)cgw%>pJ{`<#o83iA0?JL@EUyj9=YlSq+@BH`t-fmQi%u)3Nsn7zn zomcWI1q?(LtYLZ$8UaZzG55#}OW0rPAaWBIAV?CUI=LXDDnfhL(B0Xlr@`{};q)~% z4ge5QEX~XxS-0t30ERzU9qJVl#kSmBy)~der{_s}4g7h6$qjm%pa+0Nsnj4C04}(e zvnAn~75Q>Pi9u?Y(Hnxkhd)o>HpwUD{8QWW0j|s*?I#{>n1@eqeD%l}vJzaMEg*_* zLYY&yslC3`>Q{AZJx|aZa`l1-Sa%AJu?#0I>46LaLO^ zm*~!<3Am?n3@vvT=@xC+N9>d3`gchWq!zl^idB>SSh+{}JK!`jJKy)CEOy+2s@`8!KCU(T<-2Pce)qoH{K=0~&mI1u?1ZS2& z0lZ5+M75-~%tH7@gchWX=`uG!*Rinv)}SoQ%ZYb)N=Q|X*rzW5T^T2Y?M7%6e<}ib zf@9dQ_iA(Ej74eP8TKY@se2PtiHz|!rjL%6C;&se2DF?3!mwr3qE^CjPL?xOBD65l zZ>fE-ZeQ$$FHKqZ>Q(V~`RZIkK%LT@DoN`C6SgJd4Wkl;hzS|{bNleU#pqYrJTG4! zdSt`~$w(q!LiC5^Z>ZE>yZr0-;Gr@Xi^*pb8ju|TI@|Luz!ZQQ)F3K4tE>|xhiI)F zF}B4+szdGcyq9@hVvi1p=Z-R~TRP$n-|O0h6}s0TWbC-Mz5MAzPs;7Bo$x4Rg!g&h z(}p>rr+qGLl%Aic5+D#E$B?wfLsAE#;K5nCi~`bTxAq=S@5Do!aZfUR2P$OX^=|c z*Ap;@gf^CG3vf?6)Q`G2(b?S8EO40S4?vx0=P3H&{oKk^L6DvHEV_<p?I%>IY|#G#TJCA64Jn4^C^DgkG`_ckXehj5KCc z&C;#1gg#sO!6TdH-F9K(AQQL=uE;XqwG1j5!#y%`ThHB#Ox$1^WBuhy)^mEccz^-v zZoL|5jS6bifI=DIIu|hot!UsJcWdBF+OaIbpyeGGEYnz^h5IMU(Zs!Q_Pgp;(<}S*hYW}x)ZaAZ!Spo8H_icg z#;#eW0Yv}-*KB#o(pF+VEQ=sQLp>YaUKk)6QlkQOzm!u14V`B$uWW1*LF-B7?THe; z*rC(8nE9grOeRs{*Y>U=Vbw1B4xDQKXzJa=`%B{>R&&9KmFq^t$ON!PkX%-~Wv3`6VfE zB&l?EsU>+%$-q^%cL?S99HY82IbWc*Ha#IhLDL4*dh zi@_lVm$4UD%&?wKBmmPmB)F*EU&A?4awXQ6vJaSQ89Tc#c_5==urmcs&f z)+wI)a^HInllA3i3QEO^;t_g%H4y*?s+Vg8y>kdl$Vl2$0^3s4@Z(yU>M)!jtt12!jM{r;~A-rrOohFN~hFb!q1v0 z*;W3RdHgVaXE+A2ZmE&~KqvYdZxk{K^M5`7ms;k&+)T9^Zkp;?-J=1KfIyr&a_Ck> zdBMyy|0L2S@)M3%OCoWNptg=H_fU_uDwZIk3xv1T@__*9zB(bKp z+mdw$eOo{KH($8-WawtS_gKgqon@;|2H==IVyR22e*UpG#!Lr3t}<^fPqoXm@i{g$ z5)Wy(E#1vSd){P}tC zVx#xlltnG39>mR*m{1oJwP*T58^kZDpZ=JB+4M6~@6TdYa8oEY>>RGA?U3&cF_PL{ z;n4PJ38x3&;Z$dn^8mFt@oWUUISK=;*9m&60sV&oHanf5N?L(@*z7a~r_-( z*9mNUg9Ak3p8gU*^nJG(*Zd?Y{3-yrsES!PtI#oQ*rHa!`S5Tt$Em0_A(D zx9YvfHuIj<7u&vM!HZ#0j0VI4z@0W?wzL&d%$k`hkut5Q|A0qb+he=7M40Q=k(!vt zKW#IV4->EaH6Nw)MAEW>?3eOXFGx4u0JX=#*u!<{x{rf*!#XA=srxtXzzW=z#YyZC zf|Wl!@Q9MTLB`PS(15rAIOQTfPEhdGZ%8JRm?pk^bCqk`w!QL?7uQQ;<7t`8(9~!z z43$Rj)UJhBZ@9lwXwT^7l8ma>uRp%ug;9-k*B?ws4F~!6i-X`_-_4l$U})Py ziZ|)%gucE}dP@U334kxv8y-%uSz!9Jj&|<*SUmveOmfr3B*s{Qb-j`l1ZlbT@$u6*~ zJBhx^4C8kg?)nG5phEp7B(>FdLYn7Y|x%Ti8N^8G#U*3qP5PnJn zItPHn)dC8ROc285nI#72kq;te;I*lPr*tXME4x#o$qx@C6`gyS9`3K|{16xp0N#!~ z=!(o2!#PRNZF*S+kO$||!kTO`HjTOWCVmc4xdV1EOPyGE1QH7KgT~LG%r~o}fKdFw z0K>6m6kMzNa4IJV&`{VGg3jLgNX0YNT$KC)CDbh+nu^(n5q`D;?bxD1(hn9cHdc{n zhWBYw9nRwgmd4_EPTLuFb_&I#>LDbXyqhHXTav9k`VLBNXU!$d=ZtGW^#I7Q<&x31 zzD2OvOpyfIoBnz-tx?4WR#}-D+Ktg4y_)p?IM<78)Y|Q}f5;-&w54H4TcsKw3B}S< z;Sho&zbHEVm}oGi@-t=yDu4n!*xB{B)k&4=>v(CmC6}aUqR-WUt}zt2)jV}m=w)+e ziB!hMUK{IE>pIMcyu$2df|P@p1p7x){>`I4FT-}Oi`z_zu-jsQSdEPIbMmj<;;??< z)Vkq7FxiY`{-919w=+t(V<%0S59?ULjcCrkosO)CGeD>h!AM05BS5jXkOJERoYHBW zrb^;o@W%Xufa{JviL6^Q*?EKKt|z=Vj!g#C!-zzUO2lN=_&m0zo|wLeUBA(pv|zC? zCh;SjXZgJ48Q#CJ`-I*V)e!sn(!+VGup}4HrAI1eWCI$I0su+2>O-TtopQ3Kze%7= zYjrU|zByh`W|Ebs(<~ZypMH6n9ayZJPfalDle~9YodNN zG@yqJu*g=x<~bk)oV{ug{#Ql+W?lp81z@eNr`Zi;;{QdV=>KCO4QLQhG_Mq>lhDA1 zHTLq$gZ|A8|Efj<8ezcRtq_R#fRTY&<_aVO_*K!rnAd=wvJ^6iaf_=aQtk}(kN>~8 I&M!;;2eVKzJpcdz literal 0 HcmV?d00001 diff --git a/tests/tests/swfs/avm1/sound_load_start_remote/output.txt b/tests/tests/swfs/avm1/sound_load_start_remote/output.txt new file mode 100644 index 0000000000000..1357f4dc56b19 --- /dev/null +++ b/tests/tests/swfs/avm1/sound_load_start_remote/output.txt @@ -0,0 +1 @@ +onLoad true diff --git a/tests/tests/swfs/avm1/sound_load_start_remote/test.as b/tests/tests/swfs/avm1/sound_load_start_remote/test.as new file mode 100644 index 0000000000000..f8a595ca7d34f --- /dev/null +++ b/tests/tests/swfs/avm1/sound_load_start_remote/test.as @@ -0,0 +1,18 @@ +var i = 0; + +var sound = new Sound(); +sound.onSoundComplete = function() { + trace("Sound complete"); + if (i < 1) { + ++i; + sound.start(); + } +}; +sound.onLoad = function(s) { + trace("onLoad " + s); +}; + +sound.loadSound("http://localhost:8000/noise.mp3"); +sound.setVolume(50); +sound.start(); +sound.start(); diff --git a/tests/tests/swfs/avm1/sound_load_start_remote/test.swf b/tests/tests/swfs/avm1/sound_load_start_remote/test.swf new file mode 100644 index 0000000000000000000000000000000000000000..a7b28252fffb0d21f46c7af0dfcc91c0970a5943 GIT binary patch literal 380 zcmZXPKTE?v7{;GV6O&k>*rg5;I(2X`K?kK2EL7A%sHC8ygf`H^B^SIabnpW>Io?tP zziE8$Qc>vg{`uWK&vSS8GV8oEU<2mrBt5x}SCV5CC?%#MLG9%+ZR$kf)>Gv`+@i#sqCk@n!5Up#cb97NDALBda` zWY~B#VlVAY|Evx^+W>=e1||_&mG83q!PI02)^HaZragTG|AHd^<3qxG0s91F()|GH CaY(8F literal 0 HcmV?d00001 diff --git a/tests/tests/swfs/avm1/sound_load_start_remote/test.toml b/tests/tests/swfs/avm1/sound_load_start_remote/test.toml new file mode 100644 index 0000000000000..8ffbf0a128d39 --- /dev/null +++ b/tests/tests/swfs/avm1/sound_load_start_remote/test.toml @@ -0,0 +1,11 @@ +num_ticks = 100 + +[player_options] +with_audio = true + +[[compilers]] +type = "Rascal" +target = "test.swf" +scripts = ["test.as"] +swf_version = 15 +use_network = true diff --git a/tests/tests/swfs/avm1/sound_load_stops_when_dereferenced/output.txt b/tests/tests/swfs/avm1/sound_load_stops_when_dereferenced/output.txt new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/tests/tests/swfs/avm1/sound_load_stops_when_dereferenced/sound.mp3 b/tests/tests/swfs/avm1/sound_load_stops_when_dereferenced/sound.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..893eeff8dcb782694a1bdf65065d96a8106e553e GIT binary patch literal 8586 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

-RC_;mqMoHR7QL#|Dh>=@H$460L}@yS$`M zeto@wa0?_D)r}RJW(uZrRc@}T^$d5*wXnlK*>0FzSdg+ACID|(m(vc%K{UNW&vE@x zXauHHmMxwJg(n%F_we~ah9)*8D;+8tdOmh+>*d*}llwz>!?&F`FMnxo-O(PVAtuk- zWc&5y_w~}@mUZJWq~*rR6%>DG`d>+?mM!=_g%Gw{oi5(HKc_?x^_aD7`~a$gKCVMW ztH@FMVrlTNejcD?V6~%n`}#{I$1(fpIJ23vwqt{K2E$ES#TJGYS^g`yuE8qL)P~f# z)xS<*3fl2@1BmP8M1mvc3;RR-$u3QU?|Ch`-YNI~cOVM*IQ)Pz#0lSw+hp(e6n&oh zZ&!Bfh@wbopVUs~88aK+aPhhP&6&!tdJaT&*qR%Bn`vi(<0)=YPxCGskQxsfU6!u? zNgBb79n=82H6Un!#Dc2Iv2rZ405=99>;ldqYBvKP5C?*JH(4`^JjQ9Mbj)5#996gf zrIx(OlUFU#ITbt*FRf&7)Y{A@v@5_%C+(Z+y#FTq}Z>-Q9 zQ*b&#&W)kX56&`Fi<>GEYOb9#qJzhdIFmmeez>gLoCrZ$+jKC!BS^@H zgT0mfkZYAuS=C<7i3pU3TOZ3o?nTvTc01ydtEBz_4ehq zY?FHu@4ME?+yj|x@;?6_>s-egSIHwX`I^!;Xr9}CLWvWe`z-6~-o0sA+W?w{g!qd8 z#t=n3q;ih^uBCczjqd^VsHNU+N*|woa4Sxp*iEhVUT{WHrl|`3rt|^&BEK@KAcr01 zgD0E{MgzAvy!(%q^~$E(`sW%Yu7&6gSGzHE3iLM#gz{EZuiicGe_-F6wbBDuB)=yf z=>Gkx3PAi%M7e^1A=1Th5UkvT1v|1u8YX)Dt9yE~-Gq9?froI~!#Cc}i;JJesTa+| zNuLu{zI7r!hvW?AL($4Pce2dN zPxx=;^wH+{7Nq&>YCriwGzA2a?ESEjY+hT1AOYO=%^8?uqe56i3O+!OPZh(ilH%d8 zBeGGNdc|VY{rmn@)GwV7mQ^<&ML&u6`Qptk9QXOv!0|C+2ODjU>DtO&AEXXI{v)q3 zXb=sx0|!ha@Z<{uEKz;bLjxO}dXJKrpM?#ICK|6&^A2Oq7FlxjbSFNK*2rqy+;w}~ zd2(Dc@5MSR;<3j)iU&`z#LqbWi+3w;d9-i)j}ynQd29g9Fn7!`{^p?*c}lOj)@L1A zwM6c^AG{5K4+;$)q8d@@m``X4K@VONrE(2BQzc7+MmwLDaKXDWqp)_51VfQ_9BgO& z&De40%DTnnr&m92`%oZ`!^y<)DmPvuRm7BC6%+0$T-&-0K^pHD9=AkZ0Z8&b*ZshT z`a?E;@n)BUuqIlKD)KomjAQkeu!XpB{XBEjb`WemB7NyLR#u!RGndq>OZw?s`p*OH z>B=xGo6C2KYa5Cm+{>U{%Q}V5D9CivpAD(l0Gftya@@BMTv6DwQQIccj_DxCq|V^; z5M*|CWvAu@u;OF$6Hts|`l30(AUmVzvq*_(?(0*)R(3=+`*n?6^sZA}S(qoUW=eM< zNc8z7_#^)l2ga3!PeGYUj_b6{ezkLyJJ%7`p$iRXWpmrvhqN6 zf>G~u!a%5v{n*wA2;zTXw?C$_9)senu~AZ zWb}Mo{rs;fFKIiud7MA+KPyy|NOI;n1--(M%6a0u*JG=XTE27r3D{+R+4;czw}w(! zAeyKC`!t&8(hJ8fO=hvoK=4^d*SJts356i`)HWg$o>m%huL!KG(ls+}M$@wVw{#QP0#K)@<1&0mO(&VBIZ^~B2OD4RQ%ik_=& zeY%YHy6>%swC-Emo~6KSLB7FrR|l`uWXk{Vak)ePx7yz=2XIM;x7?Wk5bLt0u`wO` jZ%yX^;=UU|kIG)m zOY>6natn+Zf11lqt;9!`>%D~P9)BvJEW`G149Y7>gkT`<_0~0el MkYr#003%)$Y_y&#pa1{> literal 0 HcmV?d00001 diff --git a/tests/tests/swfs/avm1/sound_load_stops_when_dereferenced/test.toml b/tests/tests/swfs/avm1/sound_load_stops_when_dereferenced/test.toml new file mode 100644 index 0000000000000..fb6b8092985f8 --- /dev/null +++ b/tests/tests/swfs/avm1/sound_load_stops_when_dereferenced/test.toml @@ -0,0 +1,13 @@ +num_frames = 2 + +[player_options] +with_audio = true + +# Flash Player automatically stops externally loaded sounds +# when all references to the Sound object that started it are lost. +# Ruffle currently does not do this. + +[audio_assertions.silence] +frames = [2] +max_amplitude = 0.0 +known_failure = true diff --git a/tests/tests/swfs/avm1/sound_load_streaming_stop_remote/README.md b/tests/tests/swfs/avm1/sound_load_streaming_stop_remote/README.md new file mode 100644 index 0000000000000..618c96cb9b745 --- /dev/null +++ b/tests/tests/swfs/avm1/sound_load_streaming_stop_remote/README.md @@ -0,0 +1,12 @@ + # Running the test + + +To be able to serve the MP3 file, run the command `python -m http.server -d localhost` from this directory. Then, run 'test.swf' in either Flash Player or the Ruffle Desktop player. + +When running under flash player, you'll need to allow the SWF to make network connections. On Linux, this can be done by creating the file `/etc/adobe/FlashPlayerTrust/test.cfg` with the following contents: + +``` +/ancestor/of/swf/path +``` + +where `ancestor/of/swf/path` is any path that's an ancestor of the path of `test.swf` (e.g. `/home/username/`) diff --git a/tests/tests/swfs/avm1/sound_load_streaming_stop_remote/localhost/noise.mp3 b/tests/tests/swfs/avm1/sound_load_streaming_stop_remote/localhost/noise.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..862f41452ee420c77b6e151132e8235a042ba864 GIT binary patch literal 4180 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 I&M!;;2eVKzJpcdz literal 0 HcmV?d00001 diff --git a/tests/tests/swfs/avm1/sound_load_streaming_stop_remote/output.txt b/tests/tests/swfs/avm1/sound_load_streaming_stop_remote/output.txt new file mode 100644 index 0000000000000..a5f623ea02b5d --- /dev/null +++ b/tests/tests/swfs/avm1/sound_load_streaming_stop_remote/output.txt @@ -0,0 +1,3 @@ +before +after +onLoad true diff --git a/tests/tests/swfs/avm1/sound_load_streaming_stop_remote/test.as b/tests/tests/swfs/avm1/sound_load_streaming_stop_remote/test.as new file mode 100644 index 0000000000000..117739cd22760 --- /dev/null +++ b/tests/tests/swfs/avm1/sound_load_streaming_stop_remote/test.as @@ -0,0 +1,12 @@ +var sound = new Sound(); +sound.onSoundComplete = function() { + trace("Sound complete"); +}; +sound.onLoad = function(s) { + trace("onLoad " + s); +}; + +trace("before"); +sound.loadSound("http://localhost:8000/noise.mp3", true); +trace("after"); +sound.stop(); diff --git a/tests/tests/swfs/avm1/sound_load_streaming_stop_remote/test.swf b/tests/tests/swfs/avm1/sound_load_streaming_stop_remote/test.swf new file mode 100644 index 0000000000000000000000000000000000000000..6b86eaaf9facd64db6dbf6024ebae8a298a55e80 GIT binary patch literal 293 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 I&M!;;2eVKzJpcdz literal 0 HcmV?d00001 diff --git a/tests/tests/swfs/avm1/sound_mixed_attach_load/output.txt b/tests/tests/swfs/avm1/sound_mixed_attach_load/output.txt new file mode 100644 index 0000000000000..22b297aa13fb6 --- /dev/null +++ b/tests/tests/swfs/avm1/sound_mixed_attach_load/output.txt @@ -0,0 +1,13 @@ +frame 1 - attaching sound +frame 2 +frame 3 - loading noise then stopping +frame 4 +frame 5 - starting 3 instances +frame 6 +frame 7 - muting volume +frame 8 +frame 9 - loading again, as streaming this time +frame 10 +frame 11 - trying to start multiple +frame 12 +frame 14 - only the original attached should be playing diff --git a/tests/tests/swfs/avm1/sound_mixed_attach_load/sound.mp3 b/tests/tests/swfs/avm1/sound_mixed_attach_load/sound.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..893eeff8dcb782694a1bdf65065d96a8106e553e GIT binary patch literal 8586 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

-RC_;mqMoHR7QL#|Dh>=@H$460L}@yS$`M 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