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); } } } }