audio: Support loading external MP3s
This commit is contained in:
parent
93607aa86e
commit
3137306975
|
@ -29,6 +29,7 @@ pub use error::Error;
|
|||
pub use function::ExecutionReason;
|
||||
pub use globals::context_menu::make_context_menu_state;
|
||||
pub use globals::shared_object::flush;
|
||||
pub use globals::sound::start as start_sound;
|
||||
pub use globals::system::SystemProperties;
|
||||
pub use object::array_object::ArrayObject;
|
||||
pub use object::script_object::ScriptObject;
|
||||
|
|
|
@ -48,7 +48,7 @@ mod point;
|
|||
mod rectangle;
|
||||
mod selection;
|
||||
pub(crate) mod shared_object;
|
||||
mod sound;
|
||||
pub(crate) mod sound;
|
||||
mod stage;
|
||||
pub(crate) mod string;
|
||||
pub(crate) mod system;
|
||||
|
|
|
@ -6,6 +6,7 @@ use crate::avm1::error::Error;
|
|||
use crate::avm1::property_decl::{define_properties_on, Declaration};
|
||||
use crate::avm1::{Object, ScriptObject, SoundObject, TObject, Value};
|
||||
use crate::avm_warn;
|
||||
use crate::backend::navigator::Request;
|
||||
use crate::character::Character;
|
||||
use crate::display_object::{SoundTransform, TDisplayObject};
|
||||
use gc_arena::MutationContext;
|
||||
|
@ -240,11 +241,24 @@ fn id3<'gc>(
|
|||
|
||||
fn load_sound<'gc>(
|
||||
activation: &mut Activation<'_, 'gc, '_>,
|
||||
_this: Object<'gc>,
|
||||
_args: &[Value<'gc>],
|
||||
this: Object<'gc>,
|
||||
args: &[Value<'gc>],
|
||||
) -> Result<Value<'gc>, Error<'gc>> {
|
||||
if activation.swf_version() >= 6 {
|
||||
avm_warn!(activation, "Sound.loadSound: Unimplemented");
|
||||
if let Some(sound) = this.as_sound_object() {
|
||||
if let Some(url) = args.get(0) {
|
||||
let url = url.coerce_to_string(activation)?;
|
||||
let is_streaming = args
|
||||
.get(1)
|
||||
.unwrap_or(&Value::Undefined)
|
||||
.as_bool(activation.swf_version());
|
||||
let future = activation.context.load_manager.load_sound(
|
||||
activation.context.player.clone(),
|
||||
sound,
|
||||
Request::get(url.to_utf8_lossy().into_owned()),
|
||||
is_streaming,
|
||||
);
|
||||
activation.context.navigator.spawn_future(future);
|
||||
}
|
||||
}
|
||||
Ok(Value::Undefined)
|
||||
}
|
||||
|
@ -356,7 +370,7 @@ fn set_volume<'gc>(
|
|||
Ok(Value::Undefined)
|
||||
}
|
||||
|
||||
fn start<'gc>(
|
||||
pub fn start<'gc>(
|
||||
activation: &mut Activation<'_, 'gc, '_>,
|
||||
this: Object<'gc>,
|
||||
args: &[Value<'gc>],
|
||||
|
|
|
@ -43,8 +43,13 @@ pub enum RegisterError {
|
|||
pub trait AudioBackend: Downcast {
|
||||
fn play(&mut self);
|
||||
fn pause(&mut self);
|
||||
|
||||
/// Registers an sound embedded in an SWF.
|
||||
fn register_sound(&mut self, swf_sound: &swf::Sound) -> Result<SoundHandle, RegisterError>;
|
||||
|
||||
/// Registers MP3 audio from an external source.
|
||||
fn register_mp3(&mut self, data: &[u8]) -> Result<SoundHandle, DecodeError>;
|
||||
|
||||
/// Plays a sound.
|
||||
fn start_sound(
|
||||
&mut self,
|
||||
|
@ -174,6 +179,19 @@ impl AudioBackend for NullAudioBackend {
|
|||
}))
|
||||
}
|
||||
|
||||
fn register_mp3(&mut self, _data: &[u8]) -> Result<SoundHandle, DecodeError> {
|
||||
Ok(self.sounds.insert(NullSound {
|
||||
size: 0,
|
||||
duration: 0.0,
|
||||
format: swf::SoundFormat {
|
||||
compression: swf::AudioCompression::Mp3,
|
||||
sample_rate: 44100,
|
||||
is_stereo: true,
|
||||
is_16_bit: true,
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
fn start_sound(
|
||||
&mut self,
|
||||
_sound: SoundHandle,
|
||||
|
|
|
@ -363,7 +363,7 @@ impl AudioMixer {
|
|||
sound_instances.retain(|_, sound| sound.active);
|
||||
}
|
||||
|
||||
/// Registers a sound with the audio mixer.
|
||||
/// Registers an embedded SWF sound with the audio mixer.
|
||||
pub fn register_sound(&mut self, swf_sound: &swf::Sound) -> Result<SoundHandle, RegisterError> {
|
||||
// Slice off latency seek for MP3 data.
|
||||
let (skip_sample_frames, data) = if swf_sound.format.compression == AudioCompression::Mp3 {
|
||||
|
@ -385,6 +385,26 @@ impl AudioMixer {
|
|||
Ok(self.sounds.insert(sound))
|
||||
}
|
||||
|
||||
/// Registers an external MP3 with the audio mixer.
|
||||
pub fn register_mp3(&mut self, data: &[u8]) -> Result<SoundHandle, DecodeError> {
|
||||
let mut sound = Sound {
|
||||
format: swf::SoundFormat {
|
||||
compression: AudioCompression::Mp3,
|
||||
// This value is ignored; the sample rate from the MP3 data itself will be used.
|
||||
sample_rate: 44100,
|
||||
is_stereo: true,
|
||||
is_16_bit: true,
|
||||
},
|
||||
data: Arc::from(data),
|
||||
num_sample_frames: 0, // TODO
|
||||
skip_sample_frames: 0,
|
||||
};
|
||||
let data = Cursor::new(ArcAsRef(Arc::clone(&sound.data)));
|
||||
let decoder = Self::make_seekable_decoder(&sound.format, data)?;
|
||||
sound.num_sample_frames = decoder.count() as u32;
|
||||
Ok(self.sounds.insert(sound))
|
||||
}
|
||||
|
||||
/// Starts a timeline audio stream.
|
||||
pub fn start_stream(
|
||||
&mut self,
|
||||
|
@ -871,6 +891,11 @@ macro_rules! impl_audio_mixer_backend {
|
|||
self.$mixer.register_sound(swf_sound)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn register_mp3(&mut self, data: &[u8]) -> Result<SoundHandle, DecodeError> {
|
||||
self.$mixer.register_mp3(data)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn start_stream(
|
||||
&mut self,
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
use crate::avm1::Avm1;
|
||||
use crate::avm1::ExecutionReason;
|
||||
use crate::avm1::{Activation, ActivationIdentifier};
|
||||
use crate::avm1::{Object, TObject, Value};
|
||||
use crate::avm1::{Object, SoundObject, TObject, Value};
|
||||
use crate::avm2::bytearray::ByteArrayStorage;
|
||||
use crate::avm2::object::ByteArrayObject;
|
||||
use crate::avm2::object::EventObject as Avm2EventObject;
|
||||
|
@ -129,6 +129,9 @@ pub enum Error {
|
|||
#[error("Invalid bitmap")]
|
||||
InvalidBitmap(#[from] ruffle_render::error::Error),
|
||||
|
||||
#[error("Invalid sound: {0}")]
|
||||
InvalidSound(#[from] crate::backend::audio::DecodeError),
|
||||
|
||||
#[error("Unexpected content of type {1}, expected {0}")]
|
||||
UnexpectedData(ContentType, ContentType),
|
||||
|
||||
|
@ -179,6 +182,7 @@ impl<'gc> LoadManager<'gc> {
|
|||
| Loader::Form { self_handle, .. }
|
||||
| Loader::LoadVars { self_handle, .. }
|
||||
| Loader::LoadURLLoader { self_handle, .. } => *self_handle = Some(handle),
|
||||
Loader::Sound { self_handle, .. } => *self_handle = Some(handle),
|
||||
}
|
||||
handle
|
||||
}
|
||||
|
@ -306,6 +310,25 @@ impl<'gc> LoadManager<'gc> {
|
|||
let loader = self.get_loader_mut(handle).unwrap();
|
||||
loader.load_url_loader(player, request, data_format)
|
||||
}
|
||||
|
||||
/// Kick off an audio load.
|
||||
///
|
||||
/// Returns the loader's async process, which you will need to spawn.
|
||||
pub fn load_sound(
|
||||
&mut self,
|
||||
player: Weak<Mutex<Player>>,
|
||||
target_object: SoundObject<'gc>,
|
||||
request: Request,
|
||||
is_streaming: bool,
|
||||
) -> OwnedFuture<(), Error> {
|
||||
let loader = Loader::Sound {
|
||||
self_handle: None,
|
||||
target_object,
|
||||
};
|
||||
let handle = self.add_loader(loader);
|
||||
let loader = self.get_loader_mut(handle).unwrap();
|
||||
loader.sound_loader(player, request, is_streaming)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'gc> Default for LoadManager<'gc> {
|
||||
|
@ -398,6 +421,16 @@ pub enum Loader<'gc> {
|
|||
/// The target `URLLoader` to load data into.
|
||||
target_object: Avm2Object<'gc>,
|
||||
},
|
||||
|
||||
/// Loader that is loading an MP3 into an AVM1 Sound object.
|
||||
Sound {
|
||||
/// The handle to refer to this loader instance.
|
||||
#[collect(require_static)]
|
||||
self_handle: Option<Handle>,
|
||||
|
||||
/// The target AVM1 object to load the audio into.
|
||||
target_object: SoundObject<'gc>,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'gc> Loader<'gc> {
|
||||
|
@ -914,6 +947,67 @@ impl<'gc> Loader<'gc> {
|
|||
})
|
||||
}
|
||||
|
||||
/// Creates a future for a Sound load call.
|
||||
fn sound_loader(
|
||||
&mut self,
|
||||
player: Weak<Mutex<Player>>,
|
||||
request: Request,
|
||||
is_streaming: bool,
|
||||
) -> OwnedFuture<(), Error> {
|
||||
let handle = match self {
|
||||
Loader::Sound { self_handle, .. } => self_handle.expect("Loader not self-introduced"),
|
||||
_ => return Box::pin(async { Err(Error::NotLoadVarsLoader) }),
|
||||
};
|
||||
|
||||
let player = player
|
||||
.upgrade()
|
||||
.expect("Could not upgrade weak reference to player");
|
||||
|
||||
Box::pin(async move {
|
||||
let fetch = player.lock().unwrap().navigator().fetch(request);
|
||||
let data = fetch.await;
|
||||
|
||||
// Fire the load handler.
|
||||
player.lock().unwrap().update(|uc| {
|
||||
let loader = uc.load_manager.get_loader(handle);
|
||||
let sound_object = match loader {
|
||||
Some(&Loader::Sound { target_object, .. }) => target_object,
|
||||
None => return Err(Error::Cancelled),
|
||||
_ => return Err(Error::NotLoadVarsLoader),
|
||||
};
|
||||
|
||||
let success = data
|
||||
.and_then(|data| {
|
||||
let handle = uc.audio.register_mp3(&data.body)?;
|
||||
sound_object.set_sound(uc.gc_context, Some(handle));
|
||||
let duration = uc
|
||||
.audio
|
||||
.get_sound_duration(handle)
|
||||
.map(|d| d.round() as u32);
|
||||
sound_object.set_duration(uc.gc_context, duration);
|
||||
Ok(())
|
||||
})
|
||||
.is_ok();
|
||||
|
||||
let mut activation =
|
||||
Activation::from_stub(uc.reborrow(), ActivationIdentifier::root("[Loader]"));
|
||||
let _ = sound_object.call_method(
|
||||
"onLoad".into(),
|
||||
&[success.into()],
|
||||
&mut activation,
|
||||
ExecutionReason::Special,
|
||||
);
|
||||
|
||||
// Streaming sounds should auto-play.
|
||||
if is_streaming {
|
||||
crate::avm1::start_sound(&mut activation, sound_object.into(), &[])?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Report a movie loader start event to script code.
|
||||
fn movie_loader_start(handle: Index, uc: &mut UpdateContext<'_, 'gc, '_>) -> Result<(), Error> {
|
||||
let me = uc.load_manager.get_loader_mut(handle);
|
||||
|
|
Loading…
Reference in New Issue