Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions core/common/src/avm_string/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,7 @@ ruffle_macros::define_common_strings! {
"rotate270",
"rotate90",
"rr",
"sampleData",
"save",
"Selection",
"separatorBefore",
Expand Down
1 change: 1 addition & 0 deletions core/src/avm2/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1085,6 +1085,7 @@ make_error_fn!(make_error_2030, 2030, eof_error);
make_error_fn!(make_error_2037, 2037, error);
make_error_fn!(make_error_2058, 2058, io_error);
make_error_fn!(make_error_2067, 2067, error);
make_error_fn!(make_error_2068, 2068, argument_error);
make_error_fn!(make_error_2078, 2078, illegal_operation_error);
make_error_fn!(make_error_2082, 2082, argument_error);
make_error_fn!(make_error_2083, 2083, argument_error);
Expand Down
7 changes: 6 additions & 1 deletion core/src/avm2/globals/flash/media/Sound.as
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@ package flash.media {
public native function extract(target:ByteArray, length:Number, startPosition:Number = -1):Number;
public native function close():void;

public native function load(stream:URLRequest, context:SoundLoaderContext = null):void;
// As in Flash, `load` calls `_load`, which shows in stack traces.
public function load(stream:URLRequest, context:SoundLoaderContext = null):void {
this._load(stream, context);
}

private native function _load(stream:URLRequest, context:SoundLoaderContext):void;

[API("674")]
public native function loadCompressedDataFromByteArray(bytes:ByteArray, bytesLength:uint):void;
Expand Down
54 changes: 39 additions & 15 deletions core/src/avm2/globals/flash/media/sound.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
use crate::avm2::Avm2;
use crate::avm2::Error;
use crate::avm2::activation::Activation;
use crate::avm2::error::{make_error_2037, make_error_2084};
use crate::avm2::error::{make_error_2037, make_error_2068, make_error_2084};
use crate::avm2::function::FunctionArgs;
use crate::avm2::globals::slots::flash_events_event_dispatcher as event_dispatcher_slots;
use crate::avm2::globals::slots::flash_net_url_request as url_request_slots;
use crate::avm2::object::{
EventObject, QueuedPlay, SoundChannelObject, SoundLoadingState, TObject as _,
Expand All @@ -15,6 +16,7 @@
use crate::character::Character;
use crate::display_object::SoundTransform;
use crate::{avm2_stub_getter, avm2_stub_method};
use ruffle_macros::istr;
use swf::{SoundEvent, SoundInfo};

pub use crate::avm2::object::sound_allocator;
Expand Down Expand Up @@ -170,6 +172,35 @@
None
};

if sound_object.sound_handle().is_none()
&& matches!(
sound_object.loading_state(),
SoundLoadingState::New | SoundLoadingState::Generated
)
{
// A sound with no data plays what its `sampleData` listeners
// generate, and is invalid without any.
let has_listener = this
.get_slot(event_dispatcher_slots::DISPATCH_LIST)
.as_object()
.and_then(|list| {
list.as_dispatch_mut(activation.gc())
.map(|list| list.has_event_listener(istr!("sampleData")))
})
.unwrap_or(false);
if !has_listener {
return Err(make_error_2068(activation));
}

sound_object.set_loading_state(SoundLoadingState::Generated);
let sound_channel = SoundChannelObject::generated(activation, sound_object);
let start = in_sample.unwrap_or(0);
if sound_channel.start_generator(activation.context, sound_transform, start) {
return Ok(sound_channel.into());
}
return Ok(Value::Null);
}

let sound_channel = SoundChannelObject::empty(activation);

let queued_play = QueuedPlay {
Expand All @@ -193,21 +224,14 @@
pub fn extract<'gc>(
activation: &mut Activation<'_, 'gc>,
_this: Value<'gc>,
args: FunctionArgs<'_, 'gc>,
_args: FunctionArgs<'_, 'gc>,

Check warning on line 227 in core/src/avm2/globals/flash/media/sound.rs

View workflow job for this annotation

GitHub Actions / Coverage Report

Coverage

Uncovered line (227)
) -> Result<Value<'gc>, Error<'gc>> {
avm2_stub_method!(activation, "flash.media.Sound", "extract");

let bytearray = args.try_get_object(0);
let length = args.get_f64(1);

if let Some(bytearray) = bytearray
&& let Some(mut bytearray) = bytearray.as_bytearray_mut()
{
bytearray
.write_bytes(vec![0u8; length.ceil() as usize].as_slice())
.map_err(|e| e.to_avm(activation))?;
}

// Extract nothing. `sampleData` listeners often call this with lengths
// computed from the sound, which must not be allocated as a buffer of
// silence. Returning `undefined` rather than 0 lets loops such as
// `while (len > 0) len -= sound.extract(bytes, len)` end.
Ok(Value::Undefined)
}

Expand All @@ -221,8 +245,8 @@
Ok(Value::Undefined)
}

/// `Sound.load`
pub fn load<'gc>(
/// `Sound._load`, called by `Sound.load`
pub fn _load<'gc>(
activation: &mut Activation<'_, 'gc>,
this: Value<'gc>,
args: FunctionArgs<'_, 'gc>,
Expand Down
23 changes: 23 additions & 0 deletions core/src/avm2/object/event_object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,29 @@ impl<'gc> EventObject<'gc> {
)
}

pub fn sample_data_event(
activation: &mut Activation<'_, 'gc>,
position: f64,
data: Object<'gc>,
) -> EventObject<'gc> {
let sample_data_event_cls = activation.avm2().classes().sampledataevent;
Self::from_class_and_args(
activation,
sample_data_event_cls,
&[
istr!("sampleData").into(),
// bubbles
false.into(),
// cancelable
false.into(),
// theposition
position.into(),
// thedata
data.into(),
],
)
}

pub fn focus_event(
activation: &mut Activation<'_, 'gc>,
event_type: &str,
Expand Down
28 changes: 27 additions & 1 deletion core/src/avm2/object/sound_object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ use crate::avm2::Avm2;
use crate::avm2::Error;
use crate::avm2::EventObject;
use crate::avm2::activation::Activation;
use crate::avm2::bytearray::ByteArrayStorage;
use crate::avm2::globals::slots::flash_media_id3info as id3_slots;
use crate::avm2::object::script_object::ScriptObjectData;
use crate::avm2::object::{ClassObject, Object, TObject};
use crate::avm2::object::{ByteArrayObject, ClassObject, Object, TObject};
use crate::backend::audio::{AudioManager, SoundHandle};
use crate::context::UpdateContext;
use crate::display_object::SoundTransform;
Expand Down Expand Up @@ -41,6 +42,7 @@ pub fn sound_allocator<'gc>(
queued_plays: Vec::new(),
}),
id3: Lock::new(None),
sample_data: Lock::new(None),
},
))
.into())
Expand Down Expand Up @@ -90,6 +92,11 @@ pub struct SoundObjectData<'gc> {

/// ID3Info Object
id3: Lock<Option<Object<'gc>>>,

/// The `ByteArray` that `sampleData` listeners write generated samples
/// into. Like in Flash, every request reuses it, rewound but not cleared,
/// whichever channel it is for.
sample_data: Lock<Option<ByteArrayObject<'gc>>>,
}

#[derive(Collect)]
Expand Down Expand Up @@ -120,6 +127,9 @@ pub enum SoundLoadingState {
New,
Loading,
Loaded,
/// Played with no data, so its `sampleData` listeners generate what it
/// plays, and `load` throws.
Generated,
}

impl<'gc> SoundObject<'gc> {
Expand Down Expand Up @@ -182,6 +192,22 @@ impl<'gc> SoundObject<'gc> {
self.set_loading_state(SoundLoadingState::Loaded);
}

/// The `ByteArray` that `sampleData` listeners write generated samples into.
pub fn sample_data(self, context: &mut UpdateContext<'gc>) -> ByteArrayObject<'gc> {
if let Some(sample_data) = self.0.sample_data.get() {
return sample_data;
}
let storage = ByteArrayStorage::new(context);
let sample_data = ByteArrayObject::from_storage(context, storage);
unlock!(
Gc::write(context.gc(), self.0),
SoundObjectData,
sample_data
)
.set(Some(sample_data));
sample_data
}

pub fn id3(self) -> Option<Object<'gc>> {
self.0.id3.get()
}
Expand Down
Loading
Loading