using Amphora;
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices.JavaScript;
using System.Threading.Tasks;
namespace Susurrus.WebAudio.Testbed
{
///
/// Runs the module against offline contexts, which render a graph to a buffer with no speakers, and reports what
/// the buffers hold. Open the testbed page with ?test to run it.
///
public static class SelfTests
{
public const int Rate = Tones.SampleRate;
// A mono sound at the center is split evenly between the two sides, which leaves each side at this gain.
public const double MonoAtCenter = 0.70710678;
///
/// A loaded module playing into an offline context of stereo and a given length.
///
public class Session : IDisposable
{
private readonly JSObject context;
public WebAudioModule Module { get; }
private Session(JSObject context, WebAudioModule module)
{
this.context = context;
this.Module = module;
}
// The limiter is off unless asked for, so the other tests read levels the way the sounds were made.
public static Session Open(int seconds, int voiceCount = 4, bool withoutCodecs = false,
bool limiter = false)
{
int frameCount = seconds * Rate;
JSObject context = withoutCodecs ?
TestbedInterop.NewOfflineContextWithoutCodecs(2, frameCount, Rate) :
TestbedInterop.NewOfflineContext(2, frameCount, Rate);
WebAudioModule module = new WebAudioModule(voiceCount, context);
module.LimiterEnabled = limiter;
module.Load();
return new Session(context, module);
}
///
/// Renders everything started so far, then lets the browser report the voices that ended, and updates the
/// module so it sees them.
///
public async Task RenderAsync()
{
JSObject rendered = await TestbedInterop.Render(context);
await Task.Delay(100);
Module.Update();
return rendered;
}
///
/// Renders as does, but updates the module every tenth of a second of the sound,
/// and calls with the number of ticks so far.
///
public async Task RenderWithUpdatesAsync(Action atTick = null)
{
int ticks = 0;
JSObject rendered = await TestbedInterop.RenderWithTicks(context, intervalSeconds: 0.1, () =>
{
++ticks;
if (atTick != null)
{
atTick(ticks);
}
Module.Update();
});
await Task.Delay(100);
Module.Update();
return rendered;
}
public async Task WaitUntilDone(SoundLoad load)
{
for (int i = 0; i < 500 && !load.IsDone; ++i)
{
await Task.Delay(10);
Module.Update();
}
Expect(load.IsDone, "the load finishes within five seconds");
}
public void Dispose()
{
Module.Unload();
context.Dispose();
}
}
public class Failure : Exception
{
public Failure(string message) : base(message)
{
}
}
public static void Expect(bool condition, string what)
{
if (!condition)
{
throw new Failure("expected " + what);
}
}
public static void ExpectNear(string what, double expected, double actual, double tolerance)
{
if (Math.Abs(expected - actual) > tolerance)
{
throw new Failure($"{what}: expected {expected} within {tolerance}, got {actual}");
}
}
private static double Peak(JSObject rendered, int channel)
{
return TestbedInterop.Peak(rendered, channel, 0, rendered.GetPropertyAsInt32("length"));
}
private static async Task VolumeOne_PlaysAtTheSoundsOwnLevel()
{
using (Session session = Session.Open(seconds: 1))
{
Sound sound = session.Module.LoadSound(Tones.Sine(440, amplitude: 0.8, frameCount: Rate / 2));
session.Module.Play(sound, volume: 1f);
ExpectNear("peak", 0.8 * MonoAtCenter, Peak(await session.RenderAsync(), channel: 0), 0.01);
}
}
private static async Task VolumeHalf_HalvesTheLevel()
{
using (Session session = Session.Open(seconds: 1))
{
Sound sound = session.Module.LoadSound(Tones.Sine(440, amplitude: 0.8, frameCount: Rate / 2));
session.Module.Play(sound, volume: 0.5f);
ExpectNear("peak", 0.4 * MonoAtCenter, Peak(await session.RenderAsync(), channel: 0), 0.01);
}
}
private static async Task SetVolume_ChangesTheLevel()
{
using (Session session = Session.Open(seconds: 1))
{
Sound sound = session.Module.LoadSound(Tones.Sine(440, amplitude: 0.8, frameCount: Rate / 2));
Voice voice = session.Module.Play(sound, volume: 1f);
session.Module.SetVolume(voice, 0.25f);
ExpectNear("peak", 0.2 * MonoAtCenter, Peak(await session.RenderAsync(), channel: 0), 0.01);
}
}
private static async Task TwoVoices_AddTogether()
{
using (Session session = Session.Open(seconds: 1))
{
Sound sound = session.Module.LoadSound(Tones.Sine(440, amplitude: 0.3, frameCount: Rate / 2));
session.Module.Play(sound, volume: 1f);
session.Module.Play(sound, volume: 1f);
ExpectNear("peak", 0.6 * MonoAtCenter, Peak(await session.RenderAsync(), channel: 0), 0.01);
}
}
private static async Task AStereoSound_KeepsItsChannelsApart()
{
using (Session session = Session.Open(seconds: 1))
{
Sound sound = session.Module.LoadSound(
Tones.Chord(leftHertz: 440, rightHertz: 0, amplitude: 0.5, frameCount: Rate / 2));
session.Module.Play(sound, volume: 1f);
JSObject rendered = await session.RenderAsync();
ExpectNear("left peak", 0.5, Peak(rendered, channel: 0), 0.01);
ExpectNear("right peak", 0, Peak(rendered, channel: 1), 0.001);
}
}
private static async Task ASoundAtAnotherRate_KeepsItsDuration()
{
using (Session session = Session.Open(seconds: 2))
{
// One second of audio at 22050 frames per second.
Sound sound = session.Module.LoadSound(
Tones.Sine(440, amplitude: 0.8, frameCount: 22050, sampleRate: 22050));
session.Module.Play(sound, volume: 1f);
int lastLoud = TestbedInterop.LastLoudFrame(await session.RenderAsync(), channel: 0, threshold: 0.01);
ExpectNear("last loud frame", Rate, lastLoud, 300);
}
}
private static async Task AMonoSound_IsPlacedByTheCoreLaw()
{
foreach (float pan in new[] { -1f, -0.5f, 0f, 0.5f, 1f })
{
using (Session session = Session.Open(seconds: 1))
{
Sound sound = session.Module.LoadSound(Tones.Sine(440, amplitude: 0.8, frameCount: Rate / 2));
session.Module.Play(sound, volume: 1f, pan: pan);
PanMatrix law = Pan.Matrix(pan, sourceChannels: 1);
JSObject rendered = await session.RenderAsync();
ExpectNear($"left peak at {pan}", 0.8 * law.LeftToLeft, Peak(rendered, channel: 0), 0.01);
ExpectNear($"right peak at {pan}", 0.8 * law.LeftToRight, Peak(rendered, channel: 1), 0.01);
}
}
}
private static async Task AStereoSoundWithOnlyALeftChannel_IsPlacedByTheCoreLaw()
{
foreach (float pan in new[] { -1f, -0.5f, 0f, 0.5f, 1f })
{
using (Session session = Session.Open(seconds: 1))
{
Sound sound = session.Module.LoadSound(
Tones.Chord(leftHertz: 440, rightHertz: 0, amplitude: 0.8, frameCount: Rate / 2));
session.Module.Play(sound, volume: 1f, pan: pan);
PanMatrix law = Pan.Matrix(pan, sourceChannels: 2);
JSObject rendered = await session.RenderAsync();
ExpectNear($"left peak at {pan}", 0.8 * law.LeftToLeft, Peak(rendered, channel: 0), 0.01);
ExpectNear($"right peak at {pan}", 0.8 * law.LeftToRight, Peak(rendered, channel: 1), 0.01);
}
}
}
private static async Task AStereoSoundWithOnlyARightChannel_IsPlacedByTheCoreLaw()
{
foreach (float pan in new[] { -1f, -0.5f, 0f, 0.5f, 1f })
{
using (Session session = Session.Open(seconds: 1))
{
Sound sound = session.Module.LoadSound(
Tones.Chord(leftHertz: 0, rightHertz: 440, amplitude: 0.8, frameCount: Rate / 2));
session.Module.Play(sound, volume: 1f, pan: pan);
PanMatrix law = Pan.Matrix(pan, sourceChannels: 2);
JSObject rendered = await session.RenderAsync();
ExpectNear($"left peak at {pan}", 0.8 * law.RightToLeft, Peak(rendered, channel: 0), 0.01);
ExpectNear($"right peak at {pan}", 0.8 * law.RightToRight, Peak(rendered, channel: 1), 0.01);
}
}
}
private static async Task SetPan_MovesAPlayingVoice()
{
using (Session session = Session.Open(seconds: 1))
{
Sound sound = session.Module.LoadSound(Tones.Sine(440, amplitude: 0.8, frameCount: Rate / 2));
Voice voice = session.Module.Play(sound, volume: 1f);
session.Module.SetPan(voice, -1f);
JSObject rendered = await session.RenderAsync();
ExpectNear("left peak", 0.8, Peak(rendered, channel: 0), 0.01);
ExpectNear("right peak", 0, Peak(rendered, channel: 1), 0.001);
}
}
private static async Task AWholeLoop_KeepsSoundingPastTheEndOfTheSound()
{
using (Session session = Session.Open(seconds: 1))
{
Sound sound = session.Module.LoadSound(Tones.Sine(440, amplitude: 0.8, frameCount: Rate / 4));
session.Module.Play(sound, volume: 1f, loop: Loop.Whole);
int lastLoud = TestbedInterop.LastLoudFrame(await session.RenderAsync(), channel: 0, threshold: 0.01);
ExpectNear("last loud frame", Rate, lastLoud, 300);
}
}
private static async Task ARegionLoop_KeepsSoundingPastTheEndOfTheSound()
{
using (Session session = Session.Open(seconds: 1))
{
Sound sound = session.Module.LoadSound(Tones.Sine(440, amplitude: 0.8, frameCount: Rate / 4));
Loop loop = new Loop(TimeSpan.FromSeconds(0.1), TimeSpan.FromSeconds(0.2));
session.Module.Play(sound, volume: 1f, loop: loop);
int lastLoud = TestbedInterop.LastLoudFrame(await session.RenderAsync(), channel: 0, threshold: 0.01);
ExpectNear("last loud frame", Rate, lastLoud, 300);
}
}
private static async Task AnOctaveUp_PlaysInHalfTheTime()
{
using (Session session = Session.Open(seconds: 2))
{
Sound sound = session.Module.LoadSound(Tones.Sine(440, amplitude: 0.8, frameCount: Rate));
session.Module.Play(sound, volume: 1f, pitch: 2f);
int lastLoud = TestbedInterop.LastLoudFrame(await session.RenderAsync(), channel: 0, threshold: 0.01);
ExpectNear("last loud frame", Rate / 2, lastLoud, 300);
}
}
private static async Task AnOctaveDown_PlaysInTwiceTheTime()
{
using (Session session = Session.Open(seconds: 3))
{
Sound sound = session.Module.LoadSound(Tones.Sine(440, amplitude: 0.8, frameCount: Rate));
session.Module.Play(sound, volume: 1f, pitch: 0.5f);
int lastLoud = TestbedInterop.LastLoudFrame(await session.RenderAsync(), channel: 0, threshold: 0.01);
ExpectNear("last loud frame", Rate * 2, lastLoud, 300);
}
}
private static async Task TheHighestPitch_PlaysTheWholeSoundInAThousandthOfTheTime()
{
using (Session session = Session.Open(seconds: 1))
{
Sound sound = session.Module.LoadSound(Tones.Sine(440, amplitude: 0.8, frameCount: Rate));
session.Module.Play(sound, volume: 1f, pitch: Pitch.Max);
int lastLoud = TestbedInterop.LastLoudFrame(await session.RenderAsync(), channel: 0, threshold: 0.01);
ExpectNear("last loud frame", Rate / Pitch.Max, lastLoud, 3);
}
}
private static async Task TheLowestPitch_PlaysTheStartOfTheSoundAcrossTheWholeRender()
{
using (Session session = Session.Open(seconds: 1))
{
Sound sound = session.Module.LoadSound(Tones.Sine(440, amplitude: 0.8, frameCount: Rate));
session.Module.Play(sound, volume: 1f, pitch: Pitch.Min);
int lastLoud = TestbedInterop.LastLoudFrame(await session.RenderAsync(), channel: 0, threshold: 0.0001);
ExpectNear("last loud frame", Rate - 1, lastLoud, 1);
}
}
private static async Task SetPitch_ChangesTheRateOfAPlayingVoice()
{
using (Session session = Session.Open(seconds: 2))
{
Sound sound = session.Module.LoadSound(Tones.Sine(440, amplitude: 0.8, frameCount: Rate));
Voice voice = session.Module.Play(sound, volume: 1f);
session.Module.SetPitch(voice, 2f);
int lastLoud = TestbedInterop.LastLoudFrame(await session.RenderAsync(), channel: 0, threshold: 0.01);
ExpectNear("last loud frame", Rate / 2, lastLoud, 300);
}
}
private static async Task APitchOrPanOutOfRange_Throws()
{
using (Session session = Session.Open(seconds: 1))
{
Sound sound = session.Module.LoadSound(Tones.Sine(440, amplitude: 0.3, frameCount: 10));
Voice voice = session.Module.Play(sound, volume: 1f);
int thrown = 0;
foreach (Action action in new Action[]
{
() => session.Module.Play(sound, volume: 1f, pitch: 0f),
() => session.Module.Play(sound, volume: 1f, pan: 2f),
() => session.Module.SetPitch(voice, 5000f),
() => session.Module.SetPan(voice, float.NaN),
})
{
try
{
action();
}
catch (ArgumentOutOfRangeException)
{
++thrown;
}
}
Expect(thrown == 4, "all four calls to throw, but " + thrown + " did");
await Task.CompletedTask;
}
}
private static async Task Stop_SilencesTheVoiceAndFreesItsSlot()
{
using (Session session = Session.Open(seconds: 1, voiceCount: 1))
{
Sound sound = session.Module.LoadSound(Tones.Sine(440, amplitude: 0.8, frameCount: Rate / 2));
Voice voice = session.Module.Play(sound, volume: 1f);
session.Module.Stop(voice);
Expect(!session.Module.IsPlaying(voice), "the voice to stop playing at once");
ExpectNear("peak", 0, Peak(await session.RenderAsync(), channel: 0), 0.001);
Expect(!session.Module.Play(sound, volume: 1f).IsNone, "the slot to be free after an update");
}
}
private static async Task WhenEveryVoiceIsBusy_PlayReturnsNone()
{
using (Session session = Session.Open(seconds: 1, voiceCount: 2))
{
Sound sound = session.Module.LoadSound(Tones.Sine(440, amplitude: 0.3, frameCount: Rate / 2));
session.Module.Play(sound, volume: 1f);
session.Module.Play(sound, volume: 1f);
Expect(session.Module.Play(sound, volume: 1f).IsNone, "no voice for a third sound");
await Task.CompletedTask;
}
}
private static async Task AFinishedVoice_IsReleasedAtUpdate()
{
using (Session session = Session.Open(seconds: 1, voiceCount: 1))
{
Sound sound = session.Module.LoadSound(Tones.Sine(440, amplitude: 0.3, frameCount: Rate / 10));
Voice voice = session.Module.Play(sound, volume: 1f);
Expect(session.Module.IsPlaying(voice), "the voice to be playing before it renders");
await session.RenderAsync();
Expect(!session.Module.IsPlaying(voice), "the voice to have finished after it renders");
}
}
private static async Task ASoundWithNoSamples_EndsAtTheNextUpdate()
{
using (Session session = Session.Open(seconds: 1))
{
Sound sound = session.Module.LoadSound(Tones.Sine(440, amplitude: 0.3, frameCount: 0));
Voice voice = session.Module.Play(sound, volume: 1f);
Expect(session.Module.IsPlaying(voice), "the voice to be playing until an update");
session.Module.Update();
Expect(!session.Module.IsPlaying(voice), "the voice to have ended at the update");
await Task.CompletedTask;
}
}
private static async Task WavBytes_LoadAtOnce()
{
using (Session session = Session.Open(seconds: 1))
{
byte[] file = Tones.Wav(Tones.Sine(440, amplitude: 0.5, frameCount: Rate / 2));
SoundLoad load = session.Module.LoadSound(new ByteView(file));
Expect(load.IsDone, "a WAV file to load at once");
session.Module.Play(load.Result.Value, volume: 1f);
ExpectNear("peak", 0.5 * MonoAtCenter, Peak(await session.RenderAsync(), channel: 0), 0.01);
}
}
private static async Task AWavWithTooManyChannels_FailsTheLoad()
{
using (Session session = Session.Open(seconds: 1))
{
byte[] file = Tones.Wav(Tones.Sine(440, amplitude: 0.5, frameCount: 100, channelCount: 3));
SoundLoad load = session.Module.LoadSound(new ByteView(file));
Expect(load.IsDone, "the load to be done");
Expect(load.Result.Kind == SoundFailures.TooManyChannels, "the load to fail for its channels");
await Task.CompletedTask;
}
}
private static async Task ASampleRateTheBrowserRefuses_FailsTheLoad()
{
using (Session session = Session.Open(seconds: 1))
{
byte[] file = Tones.Wav(Tones.Sine(440, amplitude: 0.5, frameCount: 10, sampleRate: 100000000));
SoundLoad load = session.Module.LoadSound(new ByteView(file));
Expect(load.IsDone, "the load to be done");
Expect(load.Result.Kind == WebAudioFailures.BufferRejected, "the load to fail for its rate");
await Task.CompletedTask;
}
}
private static async Task OggDecodedByTheBrowser_PlaysAtItsOwnLength()
{
using (Session session = Session.Open(seconds: 2))
{
byte[] file = Tones.OggFile();
TimeSpan expected = SoundDecoder.Decode(new ByteView(file)).Value.Duration;
SoundLoad load = session.Module.LoadSound(new ByteView(file));
Expect(!load.IsDone, "the browser's decode to still be going");
await session.WaitUntilDone(load);
session.Module.Play(load.Result.Value, volume: 1f);
ExpectNear("duration", expected.TotalSeconds, load.Result.Value.Duration.TotalSeconds, 0.05);
Expect(Peak(await session.RenderAsync(), channel: 0) > 0.05, "sound in the rendered buffer");
}
}
private static async Task OggDecodedByManagedCode_WhenTheBrowserCannot()
{
using (Session session = Session.Open(seconds: 2, withoutCodecs: true))
{
byte[] file = Tones.OggFile();
TimeSpan expected = SoundDecoder.Decode(new ByteView(file)).Value.Duration;
SoundLoad load = session.Module.LoadSound(new ByteView(file));
await session.WaitUntilDone(load);
Expect(load.Result.IsOk, "the managed decoder to load what the browser couldn't");
session.Module.Play(load.Result.Value, volume: 1f);
ExpectNear("duration", expected.TotalSeconds, load.Result.Value.Duration.TotalSeconds, 0.0001);
Expect(Peak(await session.RenderAsync(), channel: 0) > 0.05, "sound in the rendered buffer");
}
}
private static async Task ATruncatedOgg_FailsTheLoad()
{
using (Session session = Session.Open(seconds: 1))
{
byte[] file = Tones.OggFile();
byte[] truncated = new byte[300];
Array.Copy(file, truncated, truncated.Length);
SoundLoad load = session.Module.LoadSound(new ByteView(truncated));
await session.WaitUntilDone(load);
Expect(load.Result.Kind == SoundFailures.OggFailed, "the load to fail with an Ogg error");
}
}
private static async Task UnloadingWhileAFileDecodes_FailsTheLoad()
{
SoundLoad load;
using (Session session = Session.Open(seconds: 1))
{
load = session.Module.LoadSound(new ByteView(Tones.OggFile()));
session.Module.Unload();
}
Expect(load.IsDone, "the load to be done after the unload");
Expect(load.Result.Kind == WebAudioFailures.Unloaded, "the load to fail for the unload");
await Task.CompletedTask;
}
private static async Task PlayWhileTheBrowserKeepsSoundSilent_ReturnsNoneUntilAGesture()
{
JSObject context = await TestbedInterop.NewSuspendedAudioContext();
WebAudioModule module = new WebAudioModule(voiceCount: 2, context);
module.Load();
try
{
Sound sound = module.LoadSound(Tones.Sine(440, amplitude: 0.3, frameCount: Rate * 5));
Expect(!module.IsRunning, "the module not to be running while the context is suspended");
Expect(module.Play(sound, volume: 1f).IsNone, "no voice before the browser allows sound");
TestbedInterop.PressPointer();
for (int i = 0; i < 200 && !module.IsRunning; ++i)
{
await Task.Delay(10);
}
Expect(module.IsRunning, "the gesture to start the context");
Expect(!module.Play(sound, volume: 1f).IsNone, "a voice once the browser allows sound");
}
finally
{
module.Unload();
TestbedInterop.CloseContext(context);
context.Dispose();
}
}
private static async Task AModuleThatIsNotLoaded_RefusesToLoadSounds()
{
WebAudioModule module = new WebAudioModule();
bool threw = false;
try
{
module.LoadSound(Tones.Sine(440, amplitude: 0.3, frameCount: 10));
}
catch (InvalidOperationException)
{
threw = true;
}
Expect(threw, "an exception for a sound loaded before the module");
await Task.CompletedTask;
}
// Two renders of the same sound should agree to within what a float sample can round to.
private static void ExpectSameAudio(JSObject expected, JSObject actual)
{
ExpectNear("largest difference", 0, TestbedInterop.MaxDifference(expected, actual, channel: 0), 0.001);
}
private static StreamedSound OpenStream(WebAudioModule module, byte[] file)
{
SoundLoad load = module.LoadStream(new ByteChunks(new ByteView(file)));
Expect(load.IsDone && load.Result.IsOk, "the stream to open");
return (StreamedSound)load.Result.Value;
}
private static StreamedSound OpenToneStream(WebAudioModule module, int frameCount, int channelCount = 1)
{
return OpenStream(module,
Tones.Wav(Tones.Sine(440, amplitude: 0.8, frameCount: frameCount, channelCount: channelCount)));
}
private static async Task AStreamedSound_RendersWhatTheWholeSoundDoes()
{
JSObject resident;
using (Session session = Session.Open(seconds: 3))
{
session.Module.Play(session.Module.LoadSound(Tones.Sine(440, 0.8, frameCount: Rate * 2)), volume: 1f);
resident = await session.RenderAsync();
}
using (Session session = Session.Open(seconds: 3))
{
session.Module.Play(OpenToneStream(session.Module, frameCount: Rate * 2), volume: 1f);
JSObject streamed = await session.RenderWithUpdatesAsync();
ExpectSameAudio(resident, streamed);
ExpectNear("peak", 0.8 * MonoAtCenter, Peak(streamed, channel: 0), 0.01);
}
}
private static async Task AStreamedLoop_JoinsWithoutAGap()
{
JSObject resident;
using (Session session = Session.Open(seconds: 2))
{
// A sound that isn't a whole number of chunks, so the loop joins inside a chunk.
session.Module.Play(session.Module.LoadSound(Tones.Sine(440, 0.8, frameCount: Rate * 3 / 10)),
volume: 1f, loop: Loop.Whole);
resident = await session.RenderAsync();
}
using (Session session = Session.Open(seconds: 2))
{
session.Module.Play(OpenToneStream(session.Module, frameCount: Rate * 3 / 10), volume: 1f,
loop: Loop.Whole);
ExpectSameAudio(resident, await session.RenderWithUpdatesAsync());
}
}
private static async Task AStreamedRegionLoop_RepeatsTheRegion()
{
JSObject resident;
Loop loop = new Loop(TimeSpan.FromSeconds(0.1), TimeSpan.FromSeconds(0.2));
using (Session session = Session.Open(seconds: 2))
{
session.Module.Play(session.Module.LoadSound(Tones.Sine(440, 0.8, frameCount: Rate / 4)),
volume: 1f, loop: loop);
resident = await session.RenderAsync();
}
using (Session session = Session.Open(seconds: 2))
{
session.Module.Play(OpenToneStream(session.Module, frameCount: Rate / 4), volume: 1f, loop: loop);
ExpectSameAudio(resident, await session.RenderWithUpdatesAsync());
}
}
private static async Task AStereoStreamedSound_KeepsItsChannelsApart()
{
using (Session session = Session.Open(seconds: 1))
{
DecodedSound chord = Tones.Chord(leftHertz: 440, rightHertz: 0, amplitude: 0.5, frameCount: Rate / 2);
session.Module.Play(OpenStream(session.Module, Tones.Wav(chord)), volume: 1f);
JSObject rendered = await session.RenderWithUpdatesAsync();
ExpectNear("left peak", 0.5, Peak(rendered, channel: 0), 0.01);
ExpectNear("right peak", 0, Peak(rendered, channel: 1), 0.001);
}
}
private static async Task AStreamedSoundAtAnotherRate_KeepsItsDuration()
{
using (Session session = Session.Open(seconds: 2))
{
byte[] file = Tones.Wav(Tones.Sine(440, amplitude: 0.8, frameCount: 22050 * 2, sampleRate: 22050));
session.Module.Play(OpenStream(session.Module, file), volume: 1f);
int lastLoud = TestbedInterop.LastLoudFrame(
await session.RenderWithUpdatesAsync(), channel: 0, threshold: 0.01);
ExpectNear("last loud frame", Rate * 2 - Rate / 10, lastLoud, Rate / 10 + 300);
}
}
private static async Task AnOggStream_PlaysThroughTheManagedDecoder()
{
using (Session session = Session.Open(seconds: 1))
{
session.Module.Play(OpenStream(session.Module, Tones.OggFile()), volume: 1f);
Expect(Peak(await session.RenderWithUpdatesAsync(), channel: 0) > 0.05, "sound in the rendered buffer");
}
}
private static async Task AStreamedVoice_EndsWhenItsSoundDoesAndFreesTheSound()
{
using (Session session = Session.Open(seconds: 2))
{
StreamedSound sound = OpenToneStream(session.Module, frameCount: Rate / 2);
Voice voice = session.Module.Play(sound, volume: 1f);
Expect(session.Module.IsPlaying(voice), "the voice to be playing before it renders");
Expect(sound.IsHeld, "the sound to be held while it plays");
await session.RenderWithUpdatesAsync();
Expect(!session.Module.IsPlaying(voice), "the voice to have ended");
Expect(!sound.IsHeld, "the sound to be free again");
}
}
private static async Task AStreamedSoundThatIsPlaying_GivesNoVoiceAndPlaysAgainWhenFree()
{
using (Session session = Session.Open(seconds: 2))
{
StreamedSound sound = OpenToneStream(session.Module, frameCount: Rate / 2);
session.Module.Play(sound, volume: 1f);
Expect(session.Module.Play(sound, volume: 1f).IsNone, "no second voice for the sound");
await session.RenderWithUpdatesAsync();
Expect(!session.Module.Play(sound, volume: 1f).IsNone, "a voice once the first has ended");
}
}
private static async Task Stop_SilencesAStreamedVoiceAndFreesTheSound()
{
using (Session session = Session.Open(seconds: 1, voiceCount: 1))
{
StreamedSound sound = OpenToneStream(session.Module, frameCount: Rate);
Voice voice = session.Module.Play(sound, volume: 1f);
session.Module.Stop(voice);
Expect(!session.Module.IsPlaying(voice), "the voice to stop playing at once");
ExpectNear("peak", 0, Peak(await session.RenderWithUpdatesAsync(), channel: 0), 0.001);
Expect(!sound.IsHeld, "the sound to be free after an update");
Expect(!session.Module.Play(sound, volume: 1f).IsNone, "the slot to be free after an update");
}
}
private static async Task SetPitch_ChangesTheRateOfAStreamedVoice()
{
using (Session session = Session.Open(seconds: 2))
{
StreamedSound sound = OpenToneStream(session.Module, frameCount: Rate);
Voice voice = session.Module.Play(sound, volume: 1f);
session.Module.SetPitch(voice, 2f);
int lastLoud = TestbedInterop.LastLoudFrame(
await session.RenderWithUpdatesAsync(), channel: 0, threshold: 0.01);
ExpectNear("last loud frame", Rate / 2, lastLoud, 300);
}
}
private static async Task SetPitchPartWayThrough_KeepsTheStreamGoing()
{
using (Session session = Session.Open(seconds: 3))
{
StreamedSound sound = OpenToneStream(session.Module, frameCount: Rate * 2);
Voice voice = session.Module.Play(sound, volume: 1f);
// Half a second at the natural rate, then the remaining one and a half seconds in three quarters.
JSObject rendered = await session.RenderWithUpdatesAsync(tick =>
{
if (tick == 5)
{
session.Module.SetPitch(voice, 2f);
}
});
int lastLoud = TestbedInterop.LastLoudFrame(rendered, channel: 0, threshold: 0.01);
ExpectNear("last loud frame", Rate * 5 / 4, lastLoud, 600);
ExpectNear("peak", 0.8 * MonoAtCenter, Peak(rendered, channel: 0), 0.01);
}
}
private static async Task SetPan_MovesAStreamedVoice()
{
using (Session session = Session.Open(seconds: 1))
{
Voice voice = session.Module.Play(OpenToneStream(session.Module, frameCount: Rate / 2), volume: 1f);
session.Module.SetPan(voice, -1f);
JSObject rendered = await session.RenderWithUpdatesAsync();
ExpectNear("left peak", 0.8, Peak(rendered, channel: 0), 0.01);
ExpectNear("right peak", 0, Peak(rendered, channel: 1), 0.001);
}
}
private static async Task AStreamedSoundWithTooManyChannels_FailsTheLoad()
{
using (Session session = Session.Open(seconds: 1))
{
byte[] file = Tones.Wav(Tones.Sine(440, amplitude: 0.5, frameCount: 100, channelCount: 3));
SoundLoad load = session.Module.LoadStream(new ByteChunks(new ByteView(file)));
Expect(load.IsDone, "the load to be done");
Expect(load.Result.Kind == SoundFailures.TooManyChannels, "the load to fail for its channels");
await Task.CompletedTask;
}
}
private static async Task ABrokenStream_FailsTheLoad()
{
using (Session session = Session.Open(seconds: 1))
{
SoundLoad load = session.Module.LoadStream(new ByteChunks(new ByteView(new byte[100])));
Expect(load.IsDone, "the load to be done");
Expect(load.Result.Kind == SoundFailures.UnrecognizedFormat, "the load to fail for its format");
await Task.CompletedTask;
}
}
private static async Task AStreamedSoundTheBrowserRefusesTheRateOf_GivesNoVoice()
{
using (Session session = Session.Open(seconds: 1))
{
byte[] file = Tones.Wav(Tones.Sine(440, amplitude: 0.5, frameCount: 100, sampleRate: 100000000));
StreamedSound sound = OpenStream(session.Module, file);
Expect(session.Module.Play(sound, volume: 1f).IsNone, "no voice for a rate the browser refuses");
Expect(session.Module.LastFailure.Kind == WebAudioFailures.BufferRejected, "the failure to be kept");
Expect(!sound.IsHeld, "the sound to be free");
await Task.CompletedTask;
}
}
private static async Task ADisposedStreamedSound_Throws()
{
using (Session session = Session.Open(seconds: 1))
{
StreamedSound sound = OpenToneStream(session.Module, frameCount: 100);
sound.Dispose();
bool threw = false;
try
{
session.Module.Play(sound, volume: 1f);
}
catch (ObjectDisposedException)
{
threw = true;
}
Expect(threw, "an exception for a disposed sound");
await Task.CompletedTask;
}
}
private static async Task UnloadingFreesAStreamedSound()
{
StreamedSound sound;
using (Session session = Session.Open(seconds: 1))
{
sound = OpenToneStream(session.Module, frameCount: Rate);
session.Module.Play(sound, volume: 1f);
session.Module.Unload();
}
Expect(!sound.IsHeld, "the sound to be free after the unload");
await Task.CompletedTask;
}
// Pauses the voice at one tick and resumes it at another. A tick is a tenth of a second of the render.
private static Action PausedBetween(WebAudioModule module, Voice voice, int pauseTick, int resumeTick)
{
return ticks =>
{
if (ticks == pauseTick)
{
module.SetPaused(voice, true);
}
else if (ticks == resumeTick)
{
module.SetPaused(voice, false);
}
};
}
private static void ExpectSilentBetween(JSObject rendered, double fromSeconds, double toSeconds)
{
ExpectNear($"peak from {fromSeconds} s to {toSeconds} s", 0,
TestbedInterop.Peak(rendered, channel: 0, (int)(fromSeconds * Rate), (int)(toSeconds * Rate)), 0.001);
}
private static void ExpectLoudBetween(JSObject rendered, double fromSeconds, double toSeconds)
{
ExpectNear($"peak from {fromSeconds} s to {toSeconds} s", 0.8 * MonoAtCenter,
TestbedInterop.Peak(rendered, channel: 0, (int)(fromSeconds * Rate), (int)(toSeconds * Rate)), 0.01);
}
private static void ExpectSoundEndsAt(JSObject rendered, double seconds)
{
int lastLoud = TestbedInterop.LastLoudFrame(rendered, channel: 0, threshold: 0.01);
ExpectNear("last loud frame", seconds * Rate, lastLoud, Rate / 50);
}
private static async Task APausedSound_IsSilentThenFinishesWhereItStopped()
{
using (Session session = Session.Open(seconds: 3))
{
Voice voice = session.Module.Play(
session.Module.LoadSound(Tones.Sine(440, 0.8, frameCount: Rate)), volume: 1f);
JSObject rendered = await session.RenderWithUpdatesAsync(
PausedBetween(session.Module, voice, pauseTick: 3, resumeTick: 13));
ExpectLoudBetween(rendered, 0.1, 0.3);
ExpectSilentBetween(rendered, 0.4, 1.2);
ExpectLoudBetween(rendered, 1.4, 1.9);
ExpectSoundEndsAt(rendered, 2.0);
}
}
private static async Task ASoundPausedBeforeItHasPlayed_StartsOnResume()
{
using (Session session = Session.Open(seconds: 3))
{
Voice voice = session.Module.Play(
session.Module.LoadSound(Tones.Sine(440, 0.8, frameCount: Rate)), volume: 1f);
session.Module.SetPaused(voice, true);
JSObject rendered = await session.RenderWithUpdatesAsync(
PausedBetween(session.Module, voice, pauseTick: -1, resumeTick: 5));
ExpectSilentBetween(rendered, 0, 0.45);
ExpectSoundEndsAt(rendered, 1.5);
}
}
private static async Task APausedSoundThatIsStopped_EndsAndFreesItsSlot()
{
using (Session session = Session.Open(seconds: 2, voiceCount: 1))
{
Sound sound = session.Module.LoadSound(Tones.Sine(440, 0.8, frameCount: Rate));
Voice voice = session.Module.Play(sound, volume: 1f);
await session.RenderWithUpdatesAsync(ticks =>
{
if (ticks == 3)
{
session.Module.SetPaused(voice, true);
}
else if (ticks == 5)
{
session.Module.Stop(voice);
}
});
Expect(!session.Module.IsPlaying(voice), "the stopped voice to have ended");
Expect(!session.Module.Play(sound, volume: 1f).IsNone, "the slot to be free");
}
}
private static async Task SetPitchWhilePaused_TakesEffectOnResume()
{
using (Session session = Session.Open(seconds: 3))
{
Voice voice = session.Module.Play(
session.Module.LoadSound(Tones.Sine(440, 0.8, frameCount: Rate)), volume: 1f);
Action pauseAndResume = PausedBetween(session.Module, voice, pauseTick: 3, resumeTick: 13);
JSObject rendered = await session.RenderWithUpdatesAsync(ticks =>
{
pauseAndResume(ticks);
if (ticks == 4)
{
session.Module.SetPitch(voice, 2f);
}
});
ExpectSilentBetween(rendered, 0.4, 1.2);
ExpectSoundEndsAt(rendered, 1.65);
}
}
private static async Task APausedLoop_KeepsLoopingAfterResume()
{
using (Session session = Session.Open(seconds: 3))
{
Voice voice = session.Module.Play(
session.Module.LoadSound(Tones.Sine(440, 0.8, frameCount: Rate / 2)), volume: 1f,
loop: Loop.Whole);
Action pauseAndResume = PausedBetween(session.Module, voice, pauseTick: 7, resumeTick: 17);
JSObject rendered = await session.RenderWithUpdatesAsync(ticks =>
{
pauseAndResume(ticks);
if (ticks == 22)
{
session.Module.Stop(voice);
}
});
ExpectSilentBetween(rendered, 0.8, 1.6);
ExpectLoudBetween(rendered, 1.8, 2.1);
ExpectSoundEndsAt(rendered, 2.2);
}
}
private static async Task APausedStreamedSound_IsSilentThenFinishesWhereItStopped()
{
using (Session session = Session.Open(seconds: 3))
{
Voice voice = session.Module.Play(OpenToneStream(session.Module, frameCount: Rate), volume: 1f);
JSObject rendered = await session.RenderWithUpdatesAsync(
PausedBetween(session.Module, voice, pauseTick: 3, resumeTick: 13));
ExpectLoudBetween(rendered, 0.1, 0.3);
ExpectSilentBetween(rendered, 0.4, 1.2);
ExpectLoudBetween(rendered, 1.4, 1.9);
ExpectSoundEndsAt(rendered, 2.0);
}
}
private static async Task AStreamedSoundPausedBeforeItHasPlayed_StartsOnResume()
{
using (Session session = Session.Open(seconds: 3))
{
Voice voice = session.Module.Play(OpenToneStream(session.Module, frameCount: Rate), volume: 1f);
session.Module.SetPaused(voice, true);
JSObject rendered = await session.RenderWithUpdatesAsync(
PausedBetween(session.Module, voice, pauseTick: -1, resumeTick: 5));
ExpectSilentBetween(rendered, 0, 0.45);
ExpectSoundEndsAt(rendered, 1.5);
}
}
private static async Task APausedStreamedVoice_StaysPlayingAndHoldsItsSound()
{
using (Session session = Session.Open(seconds: 3))
{
StreamedSound sound = OpenToneStream(session.Module, frameCount: Rate);
Voice voice = session.Module.Play(sound, volume: 1f);
await session.RenderWithUpdatesAsync(
PausedBetween(session.Module, voice, pauseTick: 3, resumeTick: -1));
Expect(session.Module.IsPlaying(voice), "the paused voice to be playing");
Expect(sound.IsHeld, "the sound to be held");
}
}
private static async Task APausedStreamedVoiceThatIsStopped_FreesItsSound()
{
using (Session session = Session.Open(seconds: 2))
{
StreamedSound sound = OpenToneStream(session.Module, frameCount: Rate);
Voice voice = session.Module.Play(sound, volume: 1f);
await session.RenderWithUpdatesAsync(ticks =>
{
if (ticks == 3)
{
session.Module.SetPaused(voice, true);
}
else if (ticks == 5)
{
session.Module.Stop(voice);
}
});
Expect(!session.Module.IsPlaying(voice), "the stopped voice to have ended");
Expect(!sound.IsHeld, "the sound to be free");
}
}
private static async Task SetPitchWhileAStreamIsPaused_TakesEffectOnResume()
{
using (Session session = Session.Open(seconds: 3))
{
Voice voice = session.Module.Play(OpenToneStream(session.Module, frameCount: Rate), volume: 1f);
Action pauseAndResume = PausedBetween(session.Module, voice, pauseTick: 3, resumeTick: 13);
JSObject rendered = await session.RenderWithUpdatesAsync(ticks =>
{
pauseAndResume(ticks);
if (ticks == 4)
{
session.Module.SetPitch(voice, 2f);
}
});
ExpectSilentBetween(rendered, 0.4, 1.2);
ExpectSoundEndsAt(rendered, 1.65);
}
}
private const int ChorusSize = 12;
private static async Task LoudChorusPeakAsync(bool limiter)
{
using (Session session = Session.Open(seconds: 1, voiceCount: ChorusSize, limiter: limiter))
{
Sound sound = session.Module.LoadSound(Tones.Sine(440, amplitude: 0.8, frameCount: Rate / 2));
for (int i = 0; i < ChorusSize; ++i)
{
session.Module.Play(sound, volume: 1f);
}
return Peak(await session.RenderAsync(), channel: 0);
}
}
private static async Task AChorusOfLoudVoices_ClipsWithoutTheLimiter()
{
Expect(await LoudChorusPeakAsync(limiter: false) > 4, "the unlimited chorus to reach far past full scale");
}
private static async Task AChorusOfLoudVoices_StaysNearFullScaleWithTheLimiter()
{
double peak = await LoudChorusPeakAsync(limiter: true);
Expect(peak <= 1 && peak > 0.7, $"the limited chorus to peak between 0.7 and full scale, got {peak}");
}
private static async Task ASoundBelowTheCeiling_PassesTheLimiterAtItsOwnLevel()
{
using (Session session = Session.Open(seconds: 1, limiter: true))
{
Sound sound = session.Module.LoadSound(Tones.Sine(440, amplitude: 0.3, frameCount: Rate / 2));
session.Module.Play(sound, volume: 1f);
ExpectNear("peak", 0.3 * MonoAtCenter, Peak(await session.RenderAsync(), channel: 0), 0.02);
}
}
private static async Task TurningTheLimiterOffAfterLoad_LetsTheChorusThrough()
{
using (Session session = Session.Open(seconds: 1, voiceCount: ChorusSize, limiter: true))
{
session.Module.LimiterEnabled = false;
Sound sound = session.Module.LoadSound(Tones.Sine(440, amplitude: 0.8, frameCount: Rate / 2));
for (int i = 0; i < ChorusSize; ++i)
{
session.Module.Play(sound, volume: 1f);
}
Expect(Peak(await session.RenderAsync(), channel: 0) > 4, "the chorus to pass at full strength");
}
}
public static async Task RunAll()
{
List>> tests = new List>>
{
new KeyValuePair>(nameof(VolumeOne_PlaysAtTheSoundsOwnLevel),
VolumeOne_PlaysAtTheSoundsOwnLevel),
new KeyValuePair>(nameof(VolumeHalf_HalvesTheLevel), VolumeHalf_HalvesTheLevel),
new KeyValuePair>(nameof(SetVolume_ChangesTheLevel), SetVolume_ChangesTheLevel),
new KeyValuePair>(nameof(TwoVoices_AddTogether), TwoVoices_AddTogether),
new KeyValuePair>(nameof(AStereoSound_KeepsItsChannelsApart),
AStereoSound_KeepsItsChannelsApart),
new KeyValuePair>(nameof(ASoundAtAnotherRate_KeepsItsDuration),
ASoundAtAnotherRate_KeepsItsDuration),
new KeyValuePair>(nameof(AMonoSound_IsPlacedByTheCoreLaw),
AMonoSound_IsPlacedByTheCoreLaw),
new KeyValuePair>(nameof(AStereoSoundWithOnlyALeftChannel_IsPlacedByTheCoreLaw),
AStereoSoundWithOnlyALeftChannel_IsPlacedByTheCoreLaw),
new KeyValuePair>(nameof(AStereoSoundWithOnlyARightChannel_IsPlacedByTheCoreLaw),
AStereoSoundWithOnlyARightChannel_IsPlacedByTheCoreLaw),
new KeyValuePair>(nameof(SetPan_MovesAPlayingVoice), SetPan_MovesAPlayingVoice),
new KeyValuePair>(nameof(AWholeLoop_KeepsSoundingPastTheEndOfTheSound),
AWholeLoop_KeepsSoundingPastTheEndOfTheSound),
new KeyValuePair>(nameof(ARegionLoop_KeepsSoundingPastTheEndOfTheSound),
ARegionLoop_KeepsSoundingPastTheEndOfTheSound),
new KeyValuePair>(nameof(AnOctaveUp_PlaysInHalfTheTime),
AnOctaveUp_PlaysInHalfTheTime),
new KeyValuePair>(nameof(AnOctaveDown_PlaysInTwiceTheTime),
AnOctaveDown_PlaysInTwiceTheTime),
new KeyValuePair>(
nameof(TheHighestPitch_PlaysTheWholeSoundInAThousandthOfTheTime),
TheHighestPitch_PlaysTheWholeSoundInAThousandthOfTheTime),
new KeyValuePair>(
nameof(TheLowestPitch_PlaysTheStartOfTheSoundAcrossTheWholeRender),
TheLowestPitch_PlaysTheStartOfTheSoundAcrossTheWholeRender),
new KeyValuePair>(nameof(SetPitch_ChangesTheRateOfAPlayingVoice),
SetPitch_ChangesTheRateOfAPlayingVoice),
new KeyValuePair>(nameof(APitchOrPanOutOfRange_Throws),
APitchOrPanOutOfRange_Throws),
new KeyValuePair>(nameof(Stop_SilencesTheVoiceAndFreesItsSlot),
Stop_SilencesTheVoiceAndFreesItsSlot),
new KeyValuePair>(nameof(WhenEveryVoiceIsBusy_PlayReturnsNone),
WhenEveryVoiceIsBusy_PlayReturnsNone),
new KeyValuePair>(nameof(AFinishedVoice_IsReleasedAtUpdate),
AFinishedVoice_IsReleasedAtUpdate),
new KeyValuePair>(nameof(ASoundWithNoSamples_EndsAtTheNextUpdate),
ASoundWithNoSamples_EndsAtTheNextUpdate),
new KeyValuePair>(nameof(WavBytes_LoadAtOnce), WavBytes_LoadAtOnce),
new KeyValuePair>(nameof(AWavWithTooManyChannels_FailsTheLoad),
AWavWithTooManyChannels_FailsTheLoad),
new KeyValuePair>(nameof(ASampleRateTheBrowserRefuses_FailsTheLoad),
ASampleRateTheBrowserRefuses_FailsTheLoad),
new KeyValuePair>(nameof(OggDecodedByTheBrowser_PlaysAtItsOwnLength),
OggDecodedByTheBrowser_PlaysAtItsOwnLength),
new KeyValuePair>(nameof(OggDecodedByManagedCode_WhenTheBrowserCannot),
OggDecodedByManagedCode_WhenTheBrowserCannot),
new KeyValuePair>(nameof(ATruncatedOgg_FailsTheLoad), ATruncatedOgg_FailsTheLoad),
new KeyValuePair>(nameof(UnloadingWhileAFileDecodes_FailsTheLoad),
UnloadingWhileAFileDecodes_FailsTheLoad),
new KeyValuePair>(
nameof(PlayWhileTheBrowserKeepsSoundSilent_ReturnsNoneUntilAGesture),
PlayWhileTheBrowserKeepsSoundSilent_ReturnsNoneUntilAGesture),
new KeyValuePair>(nameof(AModuleThatIsNotLoaded_RefusesToLoadSounds),
AModuleThatIsNotLoaded_RefusesToLoadSounds),
new KeyValuePair>(nameof(AStreamedSound_RendersWhatTheWholeSoundDoes),
AStreamedSound_RendersWhatTheWholeSoundDoes),
new KeyValuePair>(nameof(AStreamedLoop_JoinsWithoutAGap),
AStreamedLoop_JoinsWithoutAGap),
new KeyValuePair>(nameof(AStreamedRegionLoop_RepeatsTheRegion),
AStreamedRegionLoop_RepeatsTheRegion),
new KeyValuePair>(nameof(AStereoStreamedSound_KeepsItsChannelsApart),
AStereoStreamedSound_KeepsItsChannelsApart),
new KeyValuePair>(nameof(AStreamedSoundAtAnotherRate_KeepsItsDuration),
AStreamedSoundAtAnotherRate_KeepsItsDuration),
new KeyValuePair>(nameof(AnOggStream_PlaysThroughTheManagedDecoder),
AnOggStream_PlaysThroughTheManagedDecoder),
new KeyValuePair>(nameof(AStreamedVoice_EndsWhenItsSoundDoesAndFreesTheSound),
AStreamedVoice_EndsWhenItsSoundDoesAndFreesTheSound),
new KeyValuePair>(
nameof(AStreamedSoundThatIsPlaying_GivesNoVoiceAndPlaysAgainWhenFree),
AStreamedSoundThatIsPlaying_GivesNoVoiceAndPlaysAgainWhenFree),
new KeyValuePair>(nameof(Stop_SilencesAStreamedVoiceAndFreesTheSound),
Stop_SilencesAStreamedVoiceAndFreesTheSound),
new KeyValuePair>(nameof(SetPitch_ChangesTheRateOfAStreamedVoice),
SetPitch_ChangesTheRateOfAStreamedVoice),
new KeyValuePair>(nameof(SetPitchPartWayThrough_KeepsTheStreamGoing),
SetPitchPartWayThrough_KeepsTheStreamGoing),
new KeyValuePair>(nameof(SetPan_MovesAStreamedVoice), SetPan_MovesAStreamedVoice),
new KeyValuePair>(nameof(AStreamedSoundWithTooManyChannels_FailsTheLoad),
AStreamedSoundWithTooManyChannels_FailsTheLoad),
new KeyValuePair>(nameof(ABrokenStream_FailsTheLoad), ABrokenStream_FailsTheLoad),
new KeyValuePair>(nameof(AStreamedSoundTheBrowserRefusesTheRateOf_GivesNoVoice),
AStreamedSoundTheBrowserRefusesTheRateOf_GivesNoVoice),
new KeyValuePair>(nameof(ADisposedStreamedSound_Throws),
ADisposedStreamedSound_Throws),
new KeyValuePair>(nameof(UnloadingFreesAStreamedSound),
UnloadingFreesAStreamedSound),
new KeyValuePair>(nameof(APausedSound_IsSilentThenFinishesWhereItStopped),
APausedSound_IsSilentThenFinishesWhereItStopped),
new KeyValuePair>(nameof(ASoundPausedBeforeItHasPlayed_StartsOnResume),
ASoundPausedBeforeItHasPlayed_StartsOnResume),
new KeyValuePair>(nameof(APausedSoundThatIsStopped_EndsAndFreesItsSlot),
APausedSoundThatIsStopped_EndsAndFreesItsSlot),
new KeyValuePair>(nameof(SetPitchWhilePaused_TakesEffectOnResume),
SetPitchWhilePaused_TakesEffectOnResume),
new KeyValuePair>(nameof(APausedLoop_KeepsLoopingAfterResume),
APausedLoop_KeepsLoopingAfterResume),
new KeyValuePair>(
nameof(APausedStreamedSound_IsSilentThenFinishesWhereItStopped),
APausedStreamedSound_IsSilentThenFinishesWhereItStopped),
new KeyValuePair>(nameof(AStreamedSoundPausedBeforeItHasPlayed_StartsOnResume),
AStreamedSoundPausedBeforeItHasPlayed_StartsOnResume),
new KeyValuePair>(nameof(APausedStreamedVoice_StaysPlayingAndHoldsItsSound),
APausedStreamedVoice_StaysPlayingAndHoldsItsSound),
new KeyValuePair>(nameof(APausedStreamedVoiceThatIsStopped_FreesItsSound),
APausedStreamedVoiceThatIsStopped_FreesItsSound),
new KeyValuePair>(nameof(SetPitchWhileAStreamIsPaused_TakesEffectOnResume),
SetPitchWhileAStreamIsPaused_TakesEffectOnResume),
new KeyValuePair>(nameof(AChorusOfLoudVoices_ClipsWithoutTheLimiter),
AChorusOfLoudVoices_ClipsWithoutTheLimiter),
new KeyValuePair>(nameof(AChorusOfLoudVoices_StaysNearFullScaleWithTheLimiter),
AChorusOfLoudVoices_StaysNearFullScaleWithTheLimiter),
new KeyValuePair>(nameof(ASoundBelowTheCeiling_PassesTheLimiterAtItsOwnLevel),
ASoundBelowTheCeiling_PassesTheLimiterAtItsOwnLevel),
new KeyValuePair>(nameof(TurningTheLimiterOffAfterLoad_LetsTheChorusThrough),
TurningTheLimiterOffAfterLoad_LetsTheChorusThrough),
};
int passed = 0;
int failed = 0;
foreach (KeyValuePair> test in tests)
{
try
{
await test.Value();
++passed;
TestbedInterop.Report(test.Key, "PASS");
}
catch (Exception exception)
{
++failed;
TestbedInterop.Report($"{test.Key}: {exception.Message}", "FAIL");
}
}
TestbedInterop.Report($"SUMMARY {passed} passed, {failed} failed", failed == 0 ? "PASS" : "FAIL");
}
}
}