Skip to content

audio: Implement Sound.position #5482

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Oct 17, 2021
Merged
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
35 changes: 15 additions & 20 deletions core/src/avm1/globals/sound.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,16 +113,15 @@ fn duration<'gc>(
this: Object<'gc>,
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
if activation.swf_version() >= 6 {
if let Some(sound_object) = this.as_sound_object() {
return Ok(sound_object
.duration()
.map_or(Value::Undefined, |d| d.into()));
} else {
avm_warn!(activation, "Sound.duration: this is not a Sound");
}
// TODO: Sound.duration was only added in SWFv6, but it is not version gated.
// Return undefined for player <6 if we ever add player version emulation.
if let Some(sound_object) = this.as_sound_object() {
return Ok(sound_object
.duration()
.map_or(Value::Undefined, |d| d.into()));
} else {
avm_warn!(activation, "Sound.duration: this is not a Sound");
}

Ok(Value::Undefined)
}

Expand Down Expand Up @@ -253,18 +252,14 @@ fn position<'gc>(
this: Object<'gc>,
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
if activation.swf_version() >= 6 {
if let Some(sound_object) = this.as_sound_object() {
// TODO: The position is "sticky"; even if the sound is no longer playing, it should return
// the previous valid position.
// Needs some audio backend work for this.
if sound_object.sound().is_some() {
avm_warn!(activation, "Sound.position: Unimplemented");
return Ok(sound_object.position().into());
}
} else {
avm_warn!(activation, "Sound.position: this is not a Sound");
// TODO: Sound.position was only added in SWFv6, but it is not version gated.
// Return undefined for player <6 if we ever add player version emulation.
if let Some(sound_object) = this.as_sound_object() {
if sound_object.sound().is_some() {
return Ok(sound_object.position().into());
}
} else {
avm_warn!(activation, "Sound.position: this is not a Sound");
}
Ok(Value::Undefined)
}
Expand Down
2 changes: 1 addition & 1 deletion core/src/avm2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ pub use crate::avm2::domain::Domain;
pub use crate::avm2::events::Event;
pub use crate::avm2::names::{Namespace, QName};
pub use crate::avm2::object::{
ArrayObject, ClassObject, Object, ScriptObject, StageObject, TObject,
ArrayObject, ClassObject, Object, ScriptObject, SoundChannelObject, StageObject, TObject,
};
pub use crate::avm2::value::Value;

Expand Down
12 changes: 3 additions & 9 deletions core/src/avm2/globals/flash/media/sound.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ pub fn play<'gc>(
.get(1)
.cloned()
.unwrap_or_else(|| 0.into())
.coerce_to_i32(activation)? as u16;
.coerce_to_i32(activation)?;
let sound_transform = args
.get(2)
.cloned()
Expand All @@ -138,14 +138,8 @@ pub fn play<'gc>(
}
}

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)
Some((position / 1000.0 * 44100.0) as u32)
} else {
None
};
Expand All @@ -154,7 +148,7 @@ pub fn play<'gc>(
event: SoundEvent::Start,
in_sample,
out_sample: None,
num_loops,
num_loops: num_loops.max(1) as u16,
envelope: None,
};

Expand Down
22 changes: 17 additions & 5 deletions core/src/avm2/globals/flash/media/soundchannel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,13 @@ pub fn right_peak<'gc>(
/// Impl `SoundChannel.position`
pub fn position<'gc>(
_activation: &mut Activation<'_, 'gc, '_>,
_this: Option<Object<'gc>>,
this: Option<Object<'gc>>,
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
Err("Sound.position is a stub.".into())
if let Some(instance) = this.and_then(|this| this.as_sound_channel()) {
return Ok(instance.position().into());
}
Ok(Value::Undefined)
}

/// Implements `soundTransform`'s getter
Expand All @@ -65,7 +68,10 @@ pub fn sound_transform<'gc>(
this: Option<Object<'gc>>,
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
if let Some(instance) = this.and_then(|this| this.as_sound_instance()) {
if let Some(instance) = this
.and_then(|this| this.as_sound_channel())
.and_then(|channel| channel.instance())
{
let dobj_st = activation.context.local_sound_transform(instance).cloned();

if let Some(dobj_st) = dobj_st {
Expand All @@ -82,7 +88,10 @@ pub fn set_sound_transform<'gc>(
this: Option<Object<'gc>>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
if let Some(instance) = this.and_then(|this| this.as_sound_instance()) {
if let Some(instance) = this
.and_then(|this| this.as_sound_channel())
.and_then(|channel| channel.instance())
{
let as3_st = args
.get(0)
.cloned()
Expand All @@ -104,7 +113,10 @@ pub fn stop<'gc>(
this: Option<Object<'gc>>,
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
if let Some(instance) = this.and_then(|this| this.as_sound_instance()) {
if let Some(instance) = this
.and_then(|this| this.as_sound_channel())
.and_then(|channel| channel.instance())
{
activation.context.stop_sound(instance);
}

Expand Down
2 changes: 1 addition & 1 deletion core/src/avm2/object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1197,7 +1197,7 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy
fn set_sound(self, _mc: MutationContext<'gc, '_>, _sound: SoundHandle) {}

/// Unwrap this object's sound instance handle.
fn as_sound_instance(self) -> Option<SoundInstanceHandle> {
fn as_sound_channel(self) -> Option<SoundChannelObject<'gc>> {
None
}

Expand Down
44 changes: 35 additions & 9 deletions core/src/avm2/object/soundchannel_object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ pub fn soundchannel_allocator<'gc>(

Ok(SoundChannelObject(GcCell::allocate(
activation.context.gc_context,
SoundChannelObjectData { base, sound: None },
SoundChannelObjectData {
base,
sound: None,
position: 0.0,
},
))
.into())
}
Expand All @@ -38,14 +42,17 @@ pub struct SoundChannelObjectData<'gc> {
/// The sound this object holds.
#[collect(require_static)]
sound: Option<SoundInstanceHandle>,

/// Position of the last playing sound in milliseconds.
position: f64,
}

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<Object<'gc>, Error> {
) -> Result<Self, Error> {
let class = activation.avm2().classes().soundchannel;
let proto = class
.get_property(
Expand All @@ -56,20 +63,35 @@ impl<'gc> SoundChannelObject<'gc> {
.coerce_to_object(activation)?;
let base = ScriptObjectData::base_new(Some(proto), Some(class));

let mut sound_object: Object<'gc> = SoundChannelObject(GcCell::allocate(
let mut sound_object = SoundChannelObject(GcCell::allocate(
activation.context.gc_context,
SoundChannelObjectData {
base,
sound: Some(sound),
position: 0.0,
},
))
.into();
));
sound_object.install_instance_traits(activation, class)?;

class.call_native_init(Some(sound_object), &[], activation, Some(class))?;
class.call_native_init(Some(sound_object.into()), &[], activation, Some(class))?;

Ok(sound_object)
}

/// Return the backend handle to the currently playing sound instance.
pub fn instance(self) -> Option<SoundInstanceHandle> {
self.0.read().sound
}

/// Return the position of the playing sound in seconds.
pub fn position(self) -> f64 {
self.0.read().position
}

/// Set the position of the playing sound in seconds.
pub fn set_position(self, mc: MutationContext<'gc, '_>, value: f64) {
self.0.write(mc).position = value;
}
}

impl<'gc> TObject<'gc> for SoundChannelObject<'gc> {
Expand All @@ -94,13 +116,17 @@ impl<'gc> TObject<'gc> for SoundChannelObject<'gc> {

Ok(SoundChannelObject(GcCell::allocate(
activation.context.gc_context,
SoundChannelObjectData { base, sound: None },
SoundChannelObjectData {
base,
sound: None,
position: 0.0,
},
))
.into())
}

fn as_sound_instance(self) -> Option<SoundInstanceHandle> {
self.0.read().sound
fn as_sound_channel(self) -> Option<SoundChannelObject<'gc>> {
Some(self)
}

fn set_sound_instance(self, mc: MutationContext<'gc, '_>, sound: SoundInstanceHandle) {
Expand Down
31 changes: 21 additions & 10 deletions core/src/backend/audio.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::{
avm1::SoundObject,
avm2::Event as Avm2Event,
avm2::Object as Avm2Object,
avm2::SoundChannelObject,
display_object::{self, DisplayObject, MovieClip, TDisplayObject},
};
use downcast_rs::Downcast;
Expand Down Expand Up @@ -85,7 +85,7 @@ pub trait AudioBackend: Downcast {

/// Get the position of a sound instance in milliseconds.
/// Returns `None` if ther sound is not/no longer playing
fn get_sound_position(&self, instance: SoundInstanceHandle) -> Option<u32>;
fn get_sound_position(&self, instance: SoundInstanceHandle) -> Option<f64>;

/// Get the duration of a sound in milliseconds.
/// Returns `None` if sound is not registered.
Expand Down Expand Up @@ -191,8 +191,8 @@ impl AudioBackend for NullAudioBackend {
fn stop_sound(&mut self, _sound: SoundInstanceHandle) {}

fn stop_all_sounds(&mut self) {}
fn get_sound_position(&self, _instance: SoundInstanceHandle) -> Option<u32> {
Some(0)
fn get_sound_position(&self, _instance: SoundInstanceHandle) -> Option<f64> {
Some(0.0)
}
fn get_sound_duration(&self, sound: SoundHandle) -> Option<f64> {
if let Some(sound) = self.sounds.get(sound) {
Expand Down Expand Up @@ -271,12 +271,21 @@ impl<'gc> AudioManager<'gc> {
if let Some(pos) = audio.get_sound_position(sound.instance) {
// Sounds still playing; update position.
if let Some(avm1_object) = sound.avm1_object {
avm1_object.set_position(gc_context, pos);
avm1_object.set_position(gc_context, pos.round() as u32);
} else if let Some(avm2_object) = sound.avm2_object {
avm2_object.set_position(gc_context, pos);
}
true
} else {
// Sound ended; fire end event.
// Sound ended.
let duration = sound
.sound
.and_then(|sound| audio.get_sound_duration(sound))
.unwrap_or_default();
if let Some(object) = sound.avm1_object {
object.set_position(gc_context, duration.round() as u32);

// Fire soundComplete event.
action_queue.queue_actions(
root,
crate::context::ActionType::Method {
Expand All @@ -289,13 +298,15 @@ impl<'gc> AudioManager<'gc> {
}

if let Some(object) = sound.avm2_object {
object.set_position(gc_context, duration);

//TODO: AVM2 events are usually not queued, but we can't
//hold the update context in the audio manager yet.
action_queue.queue_actions(
root,
crate::context::ActionType::Event2 {
event: Avm2Event::new("soundComplete"),
target: object,
target: object.into(),
},
false,
)
Expand Down Expand Up @@ -338,7 +349,7 @@ impl<'gc> AudioManager<'gc> {
pub fn attach_avm2_sound_channel(
&mut self,
instance: SoundInstanceHandle,
avm2_object: Avm2Object<'gc>,
avm2_object: SoundChannelObject<'gc>,
) {
if let Some(i) = self
.sounds
Expand Down Expand Up @@ -548,8 +559,8 @@ pub struct SoundInstance<'gc> {
/// The AVM1 `Sound` object associated with this sound, if any.
avm1_object: Option<SoundObject<'gc>>,

/// The AVM2 `Sound` object associated with this sound, if any.
avm2_object: Option<Avm2Object<'gc>>,
/// The AVM2 `SoundChannel` object associated with this sound, if any.
avm2_object: Option<SoundChannelObject<'gc>>,
}

/// A sound transform for a playing sound, for use by audio backends.
Expand Down
12 changes: 12 additions & 0 deletions core/src/backend/audio/decoders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,18 @@ pub fn make_decoder<R: 'static + Send + Read>(
Ok(decoder)
}

impl Decoder for Box<dyn Decoder + Send> {
#[inline]
fn num_channels(&self) -> u8 {
self.as_ref().num_channels()
}

/// The sample rate of this audio decoder.
fn sample_rate(&self) -> u16 {
self.as_ref().sample_rate()
}
}

/// A "stream" sound is a sound that has its data distributed across `SoundStreamBlock` tags,
/// one per each frame of a MovieClip. The sound is synced to the MovieClip's timeline, and will
/// stop/seek as the MovieClip stops/seeks.
Expand Down
Loading