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
473 changes: 300 additions & 173 deletions core/src/avm1/globals/sound.rs

Large diffs are not rendered by default.

26 changes: 23 additions & 3 deletions core/src/backend/audio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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,

Expand Down
10 changes: 10 additions & 0 deletions core/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
108 changes: 74 additions & 34 deletions core/src/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1373,7 +1373,7 @@
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);
Expand All @@ -1383,47 +1383,87 @@
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<u8>, 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");

Check warning on line 1419 in core/src/loader.rs

View workflow job for this annotation

GitHub Actions / Coverage Report

Coverage

Uncovered line (1419)
};

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 {
Comment thread
ChrisCPI marked this conversation as resolved.
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.
Expand Down
19 changes: 19 additions & 0 deletions tests/tests/swfs/avm1/sound_load_multiple_instances/output.txt
Original file line number Diff line number Diff line change
@@ -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
Binary file not shown.
Binary file not shown.
23 changes: 23 additions & 0 deletions tests/tests/swfs/avm1/sound_load_multiple_instances/test.toml
Original file line number Diff line number Diff line change
@@ -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
12 changes: 12 additions & 0 deletions tests/tests/swfs/avm1/sound_load_multiple_remote/README.md
Original file line number Diff line number Diff line change
@@ -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/`)
Binary file not shown.
6 changes: 6 additions & 0 deletions tests/tests/swfs/avm1/sound_load_multiple_remote/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
loading sound
loading again
onLoad false
after loading again
onLoad true
Sound complete
15 changes: 15 additions & 0 deletions tests/tests/swfs/avm1/sound_load_multiple_remote/test.as
Original file line number Diff line number Diff line change
@@ -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");
Binary file not shown.
11 changes: 11 additions & 0 deletions tests/tests/swfs/avm1/sound_load_multiple_remote/test.toml
Original file line number Diff line number Diff line change
@@ -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
112 changes: 112 additions & 0 deletions tests/tests/swfs/avm1/sound_load_props/output.txt
Original file line number Diff line number Diff line change
@@ -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

Loading
Loading