diff --git a/Cargo.lock b/Cargo.lock index e5e016e74..419d7febf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3167,6 +3167,15 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "realfft" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6b8e8f0c6d2234aa58048d7290c60bf92cd36fd2888cd8331c66ad4f2e1d2" +dependencies = [ + "rustfft", +] + [[package]] name = "redox_syscall" version = "0.2.16" @@ -3306,9 +3315,11 @@ dependencies = [ "nellymoser-rs", "num-derive", "num-traits", + "once_cell", "percent-encoding", "quick-xml", "rand", + "realfft", "regress", "ruffle_macros", "ruffle_render", diff --git a/core/Cargo.toml b/core/Cargo.toml index 89a4cab0e..f2d36a817 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -49,6 +49,8 @@ static_assertions = "1.1.0" rustversion = "1.0.12" bytemuck = "1.13.1" clap = { version = "4.1.8", features = ["derive"], optional=true } +realfft = "3.2.0" +once_cell = "1.8.0" [target.'cfg(not(target_family = "wasm"))'.dependencies.futures] version = "0.3.26" diff --git a/core/src/avm2/globals/flash/media/soundmixer.rs b/core/src/avm2/globals/flash/media/soundmixer.rs index f89f108c1..a8a7ca07a 100644 --- a/core/src/avm2/globals/flash/media/soundmixer.rs +++ b/core/src/avm2/globals/flash/media/soundmixer.rs @@ -1,6 +1,7 @@ //! `flash.media.SoundMixer` builtin/prototype use std::cell::RefMut; +use std::sync::Arc; use crate::avm2::activation::Activation; use crate::avm2::bytearray::ByteArrayStorage; @@ -16,6 +17,7 @@ use crate::avm2::QName; use crate::avm2_stub_getter; use crate::display_object::SoundTransform; use gc_arena::GcCell; +use once_cell::sync::Lazy; /// Implements `flash.media.SoundMixer`'s instance constructor. pub fn instance_init<'gc>( @@ -147,34 +149,48 @@ pub fn compute_spectrum<'gc>( 0 }; - // This is actually more like a DCT, but at least it's related to an FFT. if fft { + // Flash Player appears to do a 2048-long FFT with only the first 512 samples filled in... + static FFT: Lazy>> = + Lazy::new(|| realfft::RealFftPlanner::new().plan_fft_forward(2048)); + + let fft = FFT.as_ref(); + + let mut in_left = fft.make_input_vec(); + let mut in_right = fft.make_input_vec(); + + for ((il, ir), h) in in_left + .iter_mut() + .zip(in_right.iter_mut()) + .zip(hist) + .take(512) + { + *il = h[0]; + *ir = h[1]; + } + + let mut out_left = fft.make_output_vec(); + let mut out_right = fft.make_output_vec(); + + // An error is only returned if any of the slices are the wrong size, + // but they can't be, because the fft made them itself. + let mut scratch = fft.make_scratch_vec(); + let _ = fft.process_with_scratch(&mut in_left, &mut out_left, &mut scratch); + let _ = fft.process_with_scratch(&mut in_right, &mut out_right, &mut scratch); + // This function was reverse-engineered with blood and tears. #[inline] fn postproc(x: f32) -> f32 { x.abs().ln().max(0.0) / 4.0 } - // Precompute a single period of the cosine function to be used as a lookup table. - let mut cos_lut = [0.0f32; 2048]; - for (i, c) in cos_lut.iter_mut().enumerate() { - *c = (i as f32 / 2048.0 * 2.0 * std::f32::consts::PI).cos(); + for (h, (ol, or)) in hist + .iter_mut() + .zip((out_left.iter()).zip(out_right.iter())) + .take(512) + { + *h = [postproc(ol.re), postproc(or.re)]; } - - // The actual DCT, with a naive implementation. - let mut outp = [[0.0, 0.0]; 512]; - for (freq, o) in outp.iter_mut().enumerate() { - // Only the first 512 frames are taken into account. - for (i, sample) in hist.iter().take(512).enumerate() { - let coeff = cos_lut[(freq * i) % 2048]; - o[0] += sample[0] * coeff; - o[1] += sample[1] * coeff; - } - *o = [postproc(o[0]), postproc(o[1])]; - } - - // Only the first 512 elements are used later. - hist[..512].copy_from_slice(&outp); } // A stretch factor of 0 appears to be "special" in that it squishes the