avm2: Impl `Sound.play`

This does not (yet) implement the third `SoundTransform` parameter on `play`.
This commit is contained in:
David Wendt 2021-08-17 18:19:59 -04:00 committed by kmeisthax
parent 8a58956f1f
commit 116fb1f323
12 changed files with 159 additions and 6 deletions

View File

@ -4,11 +4,12 @@ use crate::avm2::activation::Activation;
use crate::avm2::class::{Class, ClassAttributes};
use crate::avm2::method::{Method, NativeMethodImpl};
use crate::avm2::names::{Namespace, QName};
use crate::avm2::object::{sound_allocator, Object, TObject};
use crate::avm2::object::{sound_allocator, Object, SoundChannelObject, TObject};
use crate::avm2::value::Value;
use crate::avm2::Error;
use crate::character::Character;
use gc_arena::{GcCell, MutationContext};
use swf::{SoundEvent, SoundInfo};
/// Implements `flash.media.Sound`'s instance constructor.
pub fn instance_init<'gc>(
@ -106,6 +107,67 @@ pub fn length<'gc>(
Ok(Value::Undefined)
}
/// Implements `Sound.play`
pub fn play<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
this: Option<Object<'gc>>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
if let Some(sound) = this.and_then(|this| this.as_sound()) {
let position = args
.get(0)
.cloned()
.unwrap_or_else(|| 0.0.into())
.coerce_to_number(activation)?;
let num_loops = args
.get(1)
.cloned()
.unwrap_or_else(|| 0.into())
.coerce_to_i32(activation)? as u16;
let sound_transform = args
.get(2)
.cloned()
.unwrap_or(Value::Null)
.coerce_to_object(activation)
.ok();
if let Some(duration) = activation.context.audio.get_sound_duration(sound) {
if position > duration {
return Ok(Value::Null);
}
}
let sample_rate = if let Some(format) = activation.context.audio.get_sound_format(sound) {
format.sample_rate
} else {
return Ok(Value::Null);
};
let in_sample = if position > 0.0 {
Some((position / 1000.0 * sample_rate as f64) as u32)
} else {
None
};
let sound_info = SoundInfo {
event: SoundEvent::Start,
in_sample,
out_sample: None,
num_loops,
envelope: None,
};
if let Some(instance) = activation
.context
.start_sound(sound, &sound_info, None, None)
{
return Ok(SoundChannelObject::from_sound_instance(activation, instance)?.into());
}
}
Ok(Value::Null)
}
/// Construct `Sound`'s class.
pub fn create_class<'gc>(mc: MutationContext<'gc, '_>) -> GcCell<'gc, Class<'gc>> {
let class = Class::new(
@ -135,5 +197,8 @@ pub fn create_class<'gc>(mc: MutationContext<'gc, '_>) -> GcCell<'gc, Class<'gc>
];
write.define_public_builtin_instance_properties(mc, PUBLIC_INSTANCE_PROPERTIES);
const PUBLIC_INSTANCE_METHODS: &[(&str, NativeMethodImpl)] = &[("play", play)];
write.define_public_builtin_instance_methods(mc, PUBLIC_INSTANCE_METHODS);
class
}

View File

@ -47,15 +47,11 @@ pub struct SoundChannelObjectData<'gc> {
impl<'gc> SoundChannelObject<'gc> {
/// Convert a bare sound instance into it's object representation.
///
/// In AS3, playing 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_instance(
activation: &mut Activation<'_, 'gc, '_>,
class: Object<'gc>,
sound: SoundInstanceHandle,
) -> Result<Object<'gc>, Error> {
let class = activation.avm2().classes().soundchannel;
let proto = class
.get_property(
class,

View File

@ -94,6 +94,9 @@ pub trait AudioBackend: Downcast {
/// This is specifically measured in compressed bytes.
fn get_sound_size(&self, sound: SoundHandle) -> Option<u32>;
/// Get the sound format that a given sound was added with.
fn get_sound_format(&self, sound: SoundHandle) -> Option<&swf::SoundFormat>;
/// Set the volume transform for a sound instance.
fn set_sound_transform(&mut self, instance: SoundInstanceHandle, transform: SoundTransform);
@ -120,6 +123,9 @@ struct NullSound {
/// The compressed size of the sound data, excluding MP3 latency seek data.
size: u32,
/// The stated format of the sound data.
format: swf::SoundFormat,
}
/// Audio backend that ignores all audio.
@ -154,6 +160,7 @@ impl AudioBackend for NullAudioBackend {
Ok(self.sounds.insert(NullSound {
duration,
size: data.len() as u32,
format: sound.format.clone(),
}))
}
@ -196,6 +203,10 @@ impl AudioBackend for NullAudioBackend {
}
}
fn get_sound_format(&self, sound: SoundHandle) -> Option<&swf::SoundFormat> {
self.sounds.get(sound).map(|s| &s.format)
}
fn set_sound_transform(&mut self, _instance: SoundInstanceHandle, _transform: SoundTransform) {}
}

View File

@ -417,6 +417,10 @@ impl AudioBackend for CpalAudioBackend {
self.sounds.get(sound).map(|s| s.data.len() as u32)
}
fn get_sound_format(&self, sound: SoundHandle) -> Option<&swf::SoundFormat> {
self.sounds.get(sound).map(|s| &s.format)
}
fn set_sound_transform(&mut self, instance: SoundInstanceHandle, transform: SoundTransform) {
let mut sound_instances = self.sound_instances.lock().unwrap();
if let Some(instance) = sound_instances.get_mut(instance) {

View File

@ -658,6 +658,7 @@ swf_tests! {
(as3_movieclip_soundtransform, "avm2/movieclip_soundtransform", 49),
(as3_simplebutton_soundtransform, "avm2/simplebutton_soundtransform", 49),
(as3_soundmixer_soundtransform, "avm2/soundmixer_soundtransform", 49),
(as3_sound_play, "avm2/sound_play", 1),
}
// TODO: These tests have some inaccuracies currently, so we use approx_eq to test that numeric values are close enough.

View File

@ -0,0 +1,53 @@
package {
public class Test {
}
}
trace("///var silence = new Silence();");
var silence = new Silence();
trace("///var silence_channel = silence.play();");
var silence_channel = silence.play();
trace(silence_channel);
trace("///var noise = new Noise();");
var noise = new Noise();
trace("///var noise_channel = noise.play(2000);");
var noise_channel = noise.play(2000);
trace(noise_channel);
trace("///noise_channel = noise.play(1800);");
noise_channel = noise.play(1800);
trace(noise_channel);
trace("///noise_channel = noise.play(1600);");
noise_channel = noise.play(1600);
trace(noise_channel);
trace("///noise_channel = noise.play(1400);");
noise_channel = noise.play(1400);
trace(noise_channel);
trace("///noise_channel = noise.play(1200);");
noise_channel = noise.play(1200);
trace(noise_channel);
trace("///noise_channel = noise.play(1000);");
noise_channel = noise.play(1000);
trace(noise_channel);
trace("///var lofi_silence = new LofiSilence();");
var lofi_silence = new LofiSilence();
trace("///var lofi_silence_channel = lofi_silence.play(700);");
var lofi_silence_channel = lofi_silence.play(700);
trace(lofi_silence_channel);

Binary file not shown.

View File

@ -0,0 +1,19 @@
///var silence = new Silence();
///var silence_channel = silence.play();
[object SoundChannel]
///var noise = new Noise();
///var noise_channel = noise.play(2000);
null
///noise_channel = noise.play(1800);
null
///noise_channel = noise.play(1600);
null
///noise_channel = noise.play(1400);
null
///noise_channel = noise.play(1200);
null
///noise_channel = noise.play(1000);
[object SoundChannel]
///var lofi_silence = new LofiSilence();
///var lofi_silence_channel = lofi_silence.play(700);
[object SoundChannel]

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1043,6 +1043,10 @@ impl AudioBackend for WebAudioBackend {
self.sounds.get(sound).map(|s| s.size)
}
fn get_sound_format(&self, sound: SoundHandle) -> Option<&swf::SoundFormat> {
self.sounds.get(sound).map(|s| &s.format)
}
fn set_sound_transform(&mut self, instance: SoundInstanceHandle, transform: SoundTransform) {
SOUND_INSTANCES.with(|instances| {
let mut instances = instances.borrow_mut();