using Amphora;
using Azimuth;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices.JavaScript;
using System.Threading.Tasks;
namespace Susurrus.WebAudio.Testbed
{
///
/// The page a person plays sounds on. Its buttons call .
///
public static partial class Playground
{
private static WebAudioModule module;
private static Sound tone;
private static Sound chord;
private static Sound sweep;
private static SoundLoad ogg;
private static Voice fading;
private static int fadeStep;
private static SoundId pausable;
private static Mixer mixer;
private static Bus music;
private static SoundId capped;
private static SoundId rattle;
private static SoundId variants;
private static SoundId drone;
private static SoundId varied;
private static SoundId passing;
private static void Say(string line)
{
TestbedInterop.Report(line, "");
}
private static void DefineMixerSounds()
{
mixer = new Mixer(
module, new Xoshiro256PlusPlus(seed: 1));
Bus effects = mixer.AddBus("Effects");
music = mixer.AddBus("Music");
Sound[] pops =
{
module.LoadSound(Tones.Sine(400, amplitude: 0.3, frameCount: Tones.SampleRate * 2 / 5)),
module.LoadSound(Tones.Sine(500, amplitude: 0.3, frameCount: Tones.SampleRate * 2 / 5)),
module.LoadSound(Tones.Sine(600, amplitude: 0.3, frameCount: Tones.SampleRate * 2 / 5)),
};
capped = mixer.Define(new SoundDefinition(pops, effects, maxInstances: 3, neverRepeatClip: true));
rattle = mixer.Define(new SoundDefinition(pops[0], effects, minRetriggerSeconds: 0.1f));
variants = mixer.Define(new SoundDefinition(pops, effects, neverRepeatClip: true));
drone = mixer.Define(new SoundDefinition(
module.LoadSound(Tones.Sine(220, amplitude: 0.3, frameCount: Tones.SampleRate * 6)), music,
baseVolume: 0.6f));
varied = mixer.Define(new SoundDefinition(
module.LoadSound(Tones.Sine(440, amplitude: 0.3, frameCount: Tones.SampleRate / 4)), effects,
volumeDecibels: new Variation(-9, 0), pitchSemitones: Variation.PlusOrMinus(3)));
pausable = mixer.Define(new SoundDefinition(
module.LoadSound(Tones.RisingSweepOfFloats(amplitude: 0.3, frameCount: Tones.SampleRate * 2)), music,
loop: Loop.Whole));
mixer.View = new View(Vector.Zero, 10);
passing = mixer.Define(new SoundDefinition(
module.LoadSound(Tones.Sine(440, amplitude: 0.3, frameCount: Tones.SampleRate * 4)), effects,
falloff: new Falloff(fullVolumeDistance: 10, silentDistance: 30)));
}
private static async Task Pump(Mixer small, int milliseconds)
{
Stopwatch watch = Stopwatch.StartNew();
double last = 0;
while (watch.ElapsedMilliseconds < milliseconds)
{
double now = watch.Elapsed.TotalSeconds;
small.Update((float)(now - last));
last = now;
await Task.Delay(16);
}
}
private static async Task RunStealDemo()
{
Say("A mixer with 4 voices, all taken by drones. " +
"Four pops of higher priority, 100 ms apart, each fade one out.");
Mixer small = new Mixer(
module, new Xoshiro256PlusPlus(seed: 2), maxVoices: 4);
Bus effects = small.AddBus("Effects");
double[] pitches = { 220, 277, 330, 415 };
foreach (double pitch in pitches)
{
small.Play(small.Define(new SoundDefinition(
module.LoadSound(Tones.Sine(pitch, amplitude: 0.2, frameCount: Tones.SampleRate * 6)), effects)));
}
SoundId pop = small.Define(new SoundDefinition(
module.LoadSound(Tones.Sine(800, amplitude: 0.3, frameCount: Tones.SampleRate * 3 / 10)), effects,
priority: 1));
await Pump(small, 800);
for (int i = 0; i < 4; ++i)
{
small.Play(pop);
Say($"playing {small.ActiveCount}, fading {small.FadingCount}");
await Pump(small, 100);
}
await Pump(small, 1500);
Say("A 300 Hz tone stopped mid-cycle, which should end without a click.");
Voice tone = small.Play(small.Define(new SoundDefinition(
module.LoadSound(Tones.Sine(300, amplitude: 0.3, frameCount: Tones.SampleRate * 3)), effects)));
await Pump(small, 1013);
small.Stop(tone);
await Pump(small, 500);
}
private static SoundId DefineStreamedTrack(DecodedSound track)
{
SoundLoad load = module.LoadStream(new ByteChunks(new ByteView(Tones.Wav(track))));
return mixer.Define(new SoundDefinition(load.Result.Value, music, loop: Loop.Whole, fadeInSeconds: 1.5f,
fadeOutSeconds: 1.5f));
}
private static async Task RunCrossfadeDemo()
{
Say("Two streamed tracks on the music bus, a chord at 220 and 277 Hz then one at 330 and 415 Hz, " +
"crossfading over 1.5 s every 3 s. Each is decoded as it plays.");
int frameCount = Tones.SampleRate * 2;
SoundId first = DefineStreamedTrack(Tones.Chord(220, 277, amplitude: 0.3, frameCount));
SoundId second = DefineStreamedTrack(Tones.Chord(330, 415, amplitude: 0.3, frameCount));
Voice current = mixer.Play(first);
for (int round = 0; round < 3; ++round)
{
await Task.Delay(3000);
mixer.Stop(current);
current = mixer.Play(round % 2 == 0 ? second : first);
Say($"crossfading, playing {mixer.ActiveCount}, fading {mixer.FadingCount}");
}
await Task.Delay(3000);
mixer.Stop(current);
}
private static async Task RunPauseDemo()
{
Say("A looping sweep on the music bus, paused for 1.5 s while a blip on the effects bus keeps sounding, " +
"then resumed from where it stopped.");
Voice voice = mixer.Play(pausable);
await Task.Delay(1000);
music.IsPaused = true;
for (int i = 0; i < 3; ++i)
{
await Task.Delay(500);
mixer.Play(rattle);
}
music.IsPaused = false;
await Task.Delay(1500);
mixer.Stop(voice);
}
private static async Task RunBackgroundDemo()
{
Say("A looping sweep, then the game goes to the background for 1.5 s under each policy in turn: mute, " +
"duck to a quarter and pause. Each time it comes back for a second.");
BackgroundPolicy before = mixer.BackgroundPolicy;
Voice voice = mixer.Play(pausable);
await Task.Delay(1000);
BackgroundPolicy[] policies = { BackgroundPolicy.Mute, BackgroundPolicy.Duck, BackgroundPolicy.Pause };
foreach (BackgroundPolicy policy in policies)
{
Say($"background, {policy}");
mixer.BackgroundPolicy = policy;
mixer.IsInBackground = true;
await Task.Delay(1500);
mixer.IsInBackground = false;
Say("foreground");
await Task.Delay(1000);
}
mixer.BackgroundPolicy = before;
mixer.Stop(voice);
}
private static async Task RunFactoryDemo()
{
Say("Three factories at the left, middle and right of the view share one hum. They switch on at 0, 1.5 " +
"and 3 s and off at 6, 7.5 and 9 s. Each reports every frame while it is on.");
SoundId hum = mixer.Define(new SoundDefinition(
module.LoadSound(Tones.Sine(110, amplitude: 0.3, frameCount: Tones.SampleRate)), music,
loop: Loop.Whole, fadeInSeconds: 0.5f, fadeOutSeconds: 0.5f,
falloff: new Falloff(fullVolumeDistance: 10, silentDistance: 30)));
double[] positions = { -8, 0, 8 };
double[] onAt = { 0, 1.5, 3 };
double[] offAt = { 6, 7.5, 9 };
Stopwatch watch = Stopwatch.StartNew();
int lastCount = -1;
while (watch.Elapsed.TotalSeconds < 10.5)
{
double now = watch.Elapsed.TotalSeconds;
int on = 0;
for (int i = 0; i < positions.Length; ++i)
{
if (now >= onAt[i] && now < offAt[i])
{
mixer.Sustain(hum, new Vector(positions[i], 0), volume: 0.7f);
++on;
}
}
if (on != lastCount)
{
lastCount = on;
Say($"{on} factories on, {mixer.ActiveCount} voice playing");
}
await Task.Delay(16);
}
}
private static async Task RunLimiterDemo()
{
Say("Twelve tones at once, a harmonic series from 220 Hz, three times: limited, unlimited, limited. " +
"The unlimited one should sound harsh.");
bool before = module.LimiterEnabled;
Sound[] tones = new Sound[12];
for (int i = 0; i < tones.Length; ++i)
{
tones[i] = module.LoadSound(Tones.Sine(220 * (i + 1), amplitude: 0.3, frameCount: Tones.SampleRate));
}
foreach (bool limited in new[] { true, false, true })
{
module.LimiterEnabled = limited;
Say(limited ? "limited" : "unlimited");
foreach (Sound tone in tones)
{
module.Play(tone, volume: 1f);
}
await Task.Delay(1800);
}
module.LimiterEnabled = before;
}
///
/// Tells the mixer whether the page is hidden, which it answers as its background policy says.
///
[JSExport]
public static void SetBackground(bool hidden)
{
if (mixer != null)
{
mixer.IsInBackground = hidden;
}
}
private static async Task SweepPan(Sound sound)
{
Voice voice = module.Play(sound, volume: 1f, pan: -1f);
for (int step = 0; step <= 60; ++step)
{
module.SetPan(voice, -1f + step / 30f);
await Task.Delay(50);
}
module.Stop(voice);
}
private static async Task RunPanDemo()
{
Say("A mono tone panned from the left to the right, at constant loudness.");
await SweepPan(module.LoadSound(Tones.Sine(330, amplitude: 0.3, frameCount: Tones.SampleRate * 4)));
Say("A stereo sound, 440 Hz left and 660 Hz right, panned the same way. Each side folds into the other.");
await SweepPan(module.LoadSound(Tones.Chord(440, 660, amplitude: 0.3, frameCount: Tones.SampleRate * 4)));
}
private static async Task RunLoopDemo()
{
Say("A 2 s rising sweep, played to 1.2 s then repeating 0.4 s to 1.2 s, stopped at 5 s.");
Voice voice = module.Play(
sweep, volume: 1f, loop: new Loop(TimeSpan.FromSeconds(0.4), TimeSpan.FromSeconds(1.2)));
await Task.Delay(5000);
Say($"Still playing after the sweep's own 2 s: {module.IsPlaying(voice)}");
module.Stop(voice);
}
private static async Task RunPitchDemo()
{
Say("A 220 Hz tone swept from two octaves down to two octaves up, then stopped.");
Voice voice = module.Play(
module.LoadSound(Tones.Sine(220, amplitude: 0.3, frameCount: Tones.SampleRate * 8)), volume: 1f,
pitch: Pitch.FromSemitones(-24f));
for (int step = 0; step <= 80; ++step)
{
module.SetPitch(voice, Pitch.FromSemitones(-24f + step * 0.6f));
await Task.Delay(50);
}
module.Stop(voice);
}
private static async Task RunMixerDemo(string what)
{
if (what == "steal")
{
await RunStealDemo();
}
else if (what == "cap")
{
int started = 0;
for (int i = 0; i < 12; ++i)
{
if (!mixer.Play(capped).IsNone)
{
++started;
}
}
Say($"A dozen plays in one frame of a sound capped at 3: {started} started.");
}
else if (what == "rattle")
{
int started = 0;
for (int i = 0; i < 60; ++i)
{
if (!mixer.Play(rattle).IsNone)
{
++started;
}
await Task.Delay(20);
}
Say($"A play every 20 ms for a second, with a 100 ms interval: {started} started.");
}
else if (what == "variants")
{
for (int i = 0; i < 6; ++i)
{
mixer.Play(variants);
await Task.Delay(500);
}
Say("Six plays of three clips, never the same clip twice in a row.");
}
else if (what == "variation")
{
Say("One 440 Hz blip, each play drawing 3 semitones either way and -9 to 0 dB, panned left to right.");
for (int i = 0; i <= 12; ++i)
{
mixer.Play(varied, pan: -1f + i / 6f);
await Task.Delay(350);
}
}
else if (what == "placement")
{
Say("A tone moves left to right across a view 10 units each way, fading out beyond it.");
Say($"A play 40 units away is dropped: {mixer.Play(passing, new Vector(-40, 0)).IsNone}");
Voice moving = mixer.Play(passing, new Vector(-29, 0));
for (int step = 0; step <= 58; ++step)
{
mixer.SetPosition(moving, new Vector(-29 + step, 0));
await Task.Delay(50);
}
}
else if (what == "bus")
{
Say("Music slides down in decibels over two seconds, is muted, then comes back.");
mixer.Play(drone);
for (int step = 0; step <= 20; ++step)
{
music.Volume = Volume.FromSlider(1f - step / 20f, floorDecibels: -40f);
mixer.Play(variants);
await Task.Delay(100);
}
music.Volume = 1f;
music.IsMuted = true;
await Task.Delay(700);
music.IsMuted = false;
}
}
private static void Burst()
{
int started = 0;
for (int i = 0; i < 100; ++i)
{
DecodedSound decoded = Tones.Sine(300 + 6 * i, amplitude: 0.1, frameCount: Tones.SampleRate / 3);
if (!module.Play(module.LoadSound(decoded), volume: 1f).IsNone)
{
++started;
}
}
Say($"{started} of 100 started, and the rest found every voice busy.");
}
private static void UpdateFade()
{
if (fadeStep < 0)
{
return;
}
if (fadeStep > 40)
{
module.Stop(fading);
fadeStep = -1;
return;
}
module.SetVolume(fading, 1f - fadeStep / 40f);
++fadeStep;
}
///
/// Plays one of the testbed's sounds, by the name its button gives.
///
[JSExport]
public static void Play(string what)
{
if (module == null)
{
Say("The module isn't loaded yet.");
return;
}
Voice voice = Voice.None;
switch (what)
{
case "tone":
voice = module.Play(tone, volume: 1f);
break;
case "stereo":
voice = module.Play(chord, volume: 1f);
break;
case "sweep":
voice = module.Play(sweep, volume: 1f);
break;
case "ogg":
if (!ogg.IsDone)
{
Say("The Ogg file is still decoding.");
return;
}
voice = module.Play(ogg.Result.Value, volume: 1f);
break;
case "fade":
fading = module.Play(tone, volume: 1f);
fadeStep = 0;
voice = fading;
break;
case "burst":
Burst();
return;
case "pan":
_ = RunPanDemo();
return;
case "pitch":
_ = RunPitchDemo();
return;
case "loop":
_ = RunLoopDemo();
return;
case "steal":
case "cap":
case "rattle":
case "variants":
case "variation":
case "placement":
case "bus":
_ = RunMixerDemo(what);
return;
case "crossfade":
_ = RunCrossfadeDemo();
return;
case "pause":
_ = RunPauseDemo();
return;
case "background":
_ = RunBackgroundDemo();
return;
case "factories":
_ = RunFactoryDemo();
return;
case "limiter":
_ = RunLimiterDemo();
return;
}
Say(voice.IsNone ? $"{what}: no voice, or the browser hasn't allowed sound yet." : $"{what}: playing.");
}
///
/// Loads the module and the sounds, then updates the module every 16 milliseconds, for as long as the page is
/// open.
///
public static async Task RunAsync()
{
module = new WebAudioModule();
module.Load();
tone = module.LoadSound(Tones.Sine(440, amplitude: 0.3, frameCount: Tones.SampleRate * 2));
chord = module.LoadSound(Tones.Chord(440, 660, amplitude: 0.3, frameCount: Tones.SampleRate * 2));
sweep = module.LoadSound(Tones.RisingSweepOfFloats(amplitude: 0.3, frameCount: Tones.SampleRate * 2));
ogg = module.LoadSound(new ByteView(Tones.OggFile()));
DefineMixerSounds();
Say("Ready. Click a button to play, which also lets the browser start audio.");
bool wasRunning = false;
Stopwatch watch = Stopwatch.StartNew();
double last = 0;
while (true)
{
double now = watch.Elapsed.TotalSeconds;
mixer.Update((float)(now - last));
last = now;
UpdateFade();
if (module.IsRunning != wasRunning)
{
wasRunning = module.IsRunning;
Say(wasRunning ? "The browser now allows sound." : "The browser has stopped sound.");
}
await Task.Delay(16);
}
}
}
}