Repository navigation
Conversation
89d7cc9 to
cf5123f
Compare
|
Could you please cherry-pick the 5 commits from the PR linked above that just add tests, and see whether they also pass on this branch? |
|
Or even vice versa, whether your tests show something wrong in that other PR - but you don't have to check that, it's just my curiosity. Also, it's possible that of those 5 tests, some test entirely the same thing as one of your tests, and there's no need to introduce redundancy in the final form. |
|
And |
|
Purely for inspiration, don't take it over as is, it's a mess: https://github.com/torokati44/ruffle/tree/einsz-avm2-sound-sampledata |
|
Thanks! Your tests found three differences, which I checked in Flash Player 32 (Linux projector). Your branch matches my measurements for
New test: |
7a97046 to
c5d5a22
Compare
sampleData listeners commonly call it, and it used to allocate a buffer of the requested length, which overflowed on the web build for lengths computed from the sound. It still returns undefined, so that loops such as while (len > 0) len -= sound.extract(bytes, len) end.
|
FYI @einsz in addition to rebasing this PR onto latest |
A SampleQueue holds sample frames shared between the player, which queues them, and the audio backend, which plays them as they arrive, after AudioBackend::start_sample_queue. The mixer plays them with a new stream that plays silence while it waits for more and counts only the samples it played, and that ends once the queue has been ended and played out. This is for AVM2 sounds with no data, which ask their sampleData listeners for samples as they play.
A Sound with no data plays what its sampleData listeners write into SampleDataEvent.data. Ruffle never dispatched the event, so play() queued forever and such sounds were silent. play() on such a sound now starts a channel that asks the listeners for samples as it plays, following Flash Player 32: requests come inside play() until a few blocks are queued (one of 2048 samples, three of 4096, five of 8192), and a block of fewer than 2048 samples, or more than 8192, ends the sound. Samples are counted up to data.position, data is one ByteArray per sound, rewound but not cleared, and play() without a listener throws ArgumentError ruffle-rs#2068. Once play() has generated samples, load() throws Error ruffle-rs#2037, even if play() returned null because no channel was free. The samples go into a SampleQueue, which counts only played samples, so SoundChannel.position needs no special case while playing. Later requests happen once per Player::tick, keeping the queue a tick and a mixing buffer ahead of playback.
Output checked against Flash Player 32. The audio test runs by ticks: with num_frames the harness calls run_frame without Player::tick, so nothing would ask for more samples.
For a sound without data, Flash Player ignores play()'s start time for the samples (a generated sound takes as long to complete with play(1000) as with play(0)), but adds it to SoundChannel.position and to the position of every sampleData event dispatched after play() returns. The events before that report positions from 0.
Flash Player's Sound.load calls a native _load, which shows in stack traces, such as that of error 2037 when loading into a sound that generated samples. Co-authored-by: TÖRÖK Attila <torokati44@gmail.com>
Checks that every channel playing a `Sound` asks its `sampleData` listeners to write into the same `ByteArray`, not cleared between channels, and that another `Sound` has its own.
c5d5a22 to
eb86716
Compare
|
(pinging those who have reviewed #14203 in the past) |
Description
A
Soundwith no data plays what itssampleDatalisteners write intoSampleDataEvent.data. Ruffle never dispatched the event, soplay()queued forever, and synths, trackers and engines that mix their own audio were silent.play()on such a sound now starts a channel that asks the listeners for samples as it plays. The samples go into aSampleQueueshared with the audio backend, which a new mixer stream plays as they arrive. The stream plays silence while it waits, and counts only the samples it played, soSoundChannel.positionworks through the existingget_sound_position.AudioBackendgets one new method,start_sample_queue, implemented in the mixer macro and the null backend.I checked what Flash Player 32 does with probe SWFs in the release projector, with its audio going to a PulseAudio null sink, which runs in real time:
sampleDataevent is dispatched insideplay(), at position 0. Flash keeps asking while the listeners write blocks of 8192 samples, up to five, beforeplay()returns.positioncounts the samples generated so far on that channel. Every channel of a sound asks separately, starting from 0.soundCompletereportschannel.positionat the end of the samples. Writing more than 8192 plays none of them and ends the sound.data.position, notdata.length:datais the sameByteArrayfor every request of a sound, rewound to 0 but not cleared.ByteArray's endianness; a little-endian sine plays the same as a big-endian one.play()on a sound with no data and nosampleDatalistener throwsArgumentErrorSWF calculator does not work in 2020-12-22 #2068. A sound that has calledload()never asks its listeners.Flash asks from its audio thread. Ruffle asks once per
Player::tick, after the frames have run, keeping the queue ahead of playback by 4096 samples plus two ticks, or two mixing buffers on backends that report one (position_resolution, i.e. web), capped at a second. That is more latency than Flash's, but avoids gaps between ticks.Testing
Two new tests, with output checked against Flash Player 32:
avm2/sound_sampledata: event positions, the reusedByteArray, ending on short, empty, rewound and oversized blocks, replaying, several channels, error SWF calculator does not work in 2020-12-22 #2068, andload().avm2/sound_sampledata_audio: audio assertions for little-endian samples and aSoundTransform. Recorded in Flash, the same SWF peaks at the same levels.Both use
num_ticks: withnum_framesthe harness callsrun_framewithoutPlayer::tick, so nothing asks for more samples (NetStreamaudio has the same limitation there).Real-world content: three animations that play AHX and ProTracker modules through
sampleDatawere silent (amplitude 0 over 240 ticks in a localaudio_assertionsrun) and now play, audible on every tick. One of them is the loop from #7879, so this should fix its missing sound; I haven't looked at the slow start reported there. #14815, #14156 and #22912 look like the same gap, but I haven't tested them.Sound.extract()is still a stub, so content that generates sound from other sounds (e.g. gapless MP3 loops) stays silent; that is a separate PR. Such content now calls the stub from itssampleDatalistener, though, and the stub allocated a buffer of the requested length, with lengths computed from the sound. That panicked with a capacity overflow on the web build, so the stub now extracts nothing. It still returnsundefined: a loop likewhile (len > 0) len -= sound.extract(bytes, len)then ends, where 0 would make it spin until the script timeout.Checklist