using Amphora; using Azimuth; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Threading; namespace Susurrus.XAudio2.Testbed { /// /// Plays sounds through for a person to listen to, and prints what the engine reports. /// Files named on the command line play first, and each stage after that shows one behavior of the module. /// class Program { const int SampleRate = 48000; static void Pump(XAudio2Module module, int milliseconds) { DateTime end = DateTime.UtcNow.AddMilliseconds(milliseconds); while (DateTime.UtcNow < end) { module.Update(); Thread.Sleep(16); } } static void PumpUntilFinished(XAudio2Module module, Voice voice) { while (module.IsPlaying(voice)) { module.Update(); Thread.Sleep(16); } } static float Envelope(int frame, int frameCount) { // A short fade at each end, so a sound doesn't click when it starts and stops. int fade = SampleRate / 100; return Math.Min(1f, Math.Min(frame, frameCount - 1 - frame) / (float)fade); } static DecodedSound Tone(double leftHertz, double rightHertz, double seconds, int sampleRate, int channelCount) { int frameCount = (int)(seconds * sampleRate); short[] samples = new short[frameCount * channelCount]; for (int frame = 0; frame < frameCount; ++frame) { double time = frame / (double)sampleRate; float level = 0.3f * Envelope(frame, frameCount) * short.MaxValue; samples[frame * channelCount] = (short)(level * Math.Sin(2 * Math.PI * leftHertz * time)); if (channelCount == 2) { samples[frame * channelCount + 1] = (short)(level * Math.Sin(2 * Math.PI * rightHertz * time)); } } byte[] bytes = new byte[samples.Length * 2]; Buffer.BlockCopy(samples, 0, bytes, 0, bytes.Length); return new DecodedSound(new ByteView(bytes), sampleRate, channelCount, SampleFormat.Pcm16); } static DecodedSound RisingSweepOfFloats(double seconds) { int frameCount = (int)(seconds * SampleRate); float[] samples = new float[frameCount]; double phase = 0; for (int frame = 0; frame < frameCount; ++frame) { double hertz = 300 + 900 * frame / (double)frameCount; phase += 2 * Math.PI * hertz / SampleRate; samples[frame] = 0.3f * Envelope(frame, frameCount) * (float)Math.Sin(phase); } byte[] bytes = new byte[samples.Length * 4]; Buffer.BlockCopy(samples, 0, bytes, 0, bytes.Length); return new DecodedSound(new ByteView(bytes), SampleRate, channelCount: 1, SampleFormat.Float32); } static void PrintPerformance(XAudio2Module module, string when) { XAudio2PerformanceData data = module.GetPerformanceData(); Console.WriteLine($" [{when}] active voices {data.ActiveSourceVoiceCount} of " + $"{data.TotalSourceVoiceCount}, glitches {data.GlitchesSinceEngineStarted}, " + $"latency {data.CurrentLatencyInSamples} samples, memory {data.MemoryUsageInBytes} bytes"); } static void PlayAndWait(XAudio2Module module, string name, Sound sound, float volume) { Console.WriteLine($" {name}: {sound.ChannelCount} channel(s), {sound.Duration.TotalSeconds:0.00} s"); Voice voice = module.Play(sound, volume); if (voice.IsNone) { Console.WriteLine(" didn't start: " + module.LastFailure.Kind); return; } Pump(module, milliseconds: 50); PrintPerformance(module, "playing"); PumpUntilFinished(module, voice); Pump(module, milliseconds: 250); } static void PlayFiles(XAudio2Module module, string[] paths) { Console.WriteLine("Files:"); foreach (string path in paths) { StatusOr decoded = SoundDecoder.Decode(new ByteView(File.ReadAllBytes(path))); if (!decoded.IsOk) { Console.WriteLine($" {path}: {decoded.Status.Describe()}"); continue; } PlayAndWait(module, Path.GetFileName(path), module.LoadSound(decoded.Value), volume: 0.5f); } } static void PlayKinds(XAudio2Module module) { Console.WriteLine("Mono, stereo (440 Hz left, 660 Hz right) and 32-bit float (a rising sweep):"); PlayAndWait(module, "mono 440 Hz", module.LoadSound(Tone(440, 440, 1, SampleRate, 1)), volume: 1f); PlayAndWait(module, "stereo", module.LoadSound(Tone(440, 660, 1, SampleRate, 2)), volume: 1f); PlayAndWait(module, "float sweep", module.LoadSound(RisingSweepOfFloats(1.5)), volume: 1f); } static void PlayAtOtherRates(XAudio2Module module) { Console.WriteLine("The same 440 Hz tone made at three sample rates, which should all sound alike:"); foreach (int rate in new[] { 22050, 48000, 96000 }) { PlayAndWait(module, $"{rate} Hz", module.LoadSound(Tone(440, 440, 0.7, rate, 1)), volume: 1f); } } static void FadeOut(XAudio2Module module) { Console.WriteLine("A tone fading out over two seconds, then stopped:"); Voice voice = module.Play(module.LoadSound(Tone(330, 330, 5, SampleRate, 1)), volume: 1f); for (int step = 0; step <= 40; ++step) { module.SetVolume(voice, 1f - step / 40f); Pump(module, milliseconds: 50); } module.Stop(voice); Console.WriteLine($" still playing after Stop: {module.IsPlaying(voice)}"); Pump(module, milliseconds: 250); } static void PanDemo(XAudio2Module module) { Console.WriteLine($"Panning on a device with {module.DeviceChannelCount} channels."); Console.WriteLine("A mono tone panned from the left to the right, at constant loudness:"); SweepPan(module, module.LoadSound(Tone(330, 330, 4, SampleRate, 1))); Console.WriteLine("A stereo sound, 440 Hz left and 660 Hz right, panned the same way."); Console.WriteLine("Each side folds into the other as it nears the edge, so none of it is lost:"); SweepPan(module, module.LoadSound(Tone(440, 660, 4, SampleRate, 2))); } static void SweepPan(XAudio2Module module, Sound sound) { Voice voice = module.Play(sound, volume: 1f, pan: -1f); for (int step = 0; step <= 60; ++step) { module.SetPan(voice, -1f + step / 30f); Pump(module, milliseconds: 50); } module.Stop(voice); Pump(module, milliseconds: 250); } static void PitchDemo(XAudio2Module module) { Console.WriteLine("A 220 Hz tone swept from two octaves down to two octaves up, then stopped:"); Voice voice = module.Play(module.LoadSound(Tone(220, 220, 8, SampleRate, 1)), volume: 1f, pitch: Pitch.FromSemitones(-24f)); for (int step = 0; step <= 80; ++step) { module.SetPitch(voice, Pitch.FromSemitones(-24f + step * 0.6f)); Pump(module, milliseconds: 50); } module.Stop(voice); Pump(module, milliseconds: 250); } static void Burst(XAudio2Module module) { Console.WriteLine("A burst of 100 blips at once, in rising pitches:"); List blips = new List(); for (int i = 0; i < 100; ++i) { blips.Add(module.LoadSound(Tone(300 + 6 * i, 300 + 6 * i, 0.4, SampleRate, 1))); } int started = 0; foreach (Sound blip in blips) { if (!module.Play(blip, volume: 0.1f).IsNone) { ++started; } } Console.WriteLine($" {started} of 100 started, and the rest found every mono voice busy"); Pump(module, milliseconds: 100); PrintPerformance(module, "burst playing"); Pump(module, milliseconds: 600); PrintPerformance(module, "burst over"); } static void Run(Mixer mixer, int milliseconds) { Stopwatch watch = Stopwatch.StartNew(); double last = 0; while (watch.ElapsedMilliseconds < milliseconds) { double now = watch.Elapsed.TotalSeconds; mixer.Update((float)(now - last)); last = now; Thread.Sleep(16); } } static void MixerDemo(XAudio2Module module) { Console.WriteLine("The mixer: caps, retrigger intervals, clip variants and buses."); Mixer mixer = new Mixer( module, new Xoshiro256PlusPlus(seed: 1)); Bus effects = mixer.AddBus("Effects"); Bus music = mixer.AddBus("Music"); Sound[] pops = { module.LoadSound(Tone(400, 400, 0.4, SampleRate, 1)), module.LoadSound(Tone(500, 500, 0.4, SampleRate, 1)), module.LoadSound(Tone(600, 600, 0.4, SampleRate, 1)), }; SoundId capped = mixer.Define(new SoundDefinition(pops, effects, maxInstances: 3, neverRepeatClip: true)); SoundId rattle = mixer.Define(new SoundDefinition(pops[0], effects, minRetriggerSeconds: 0.1f)); SoundId variants = mixer.Define(new SoundDefinition(pops, effects, neverRepeatClip: true)); SoundId drone = mixer.Define(new SoundDefinition( module.LoadSound(Tone(220, 220, 6, SampleRate, 1)), music, baseVolume: 0.6f)); int started = 0; for (int i = 0; i < 12; ++i) { if (!mixer.Play(capped).IsNone) { ++started; } } Console.WriteLine($" a dozen plays in one frame of a sound capped at 3: {started} started"); Run(mixer, milliseconds: 800); started = 0; for (int frame = 0; frame < 60; ++frame) { if (!mixer.Play(rattle).IsNone) { ++started; } Run(mixer, milliseconds: 20); } Console.WriteLine($" a play every 20 ms for a second, with a 100 ms interval: {started} started"); Run(mixer, milliseconds: 500); Console.WriteLine(" six plays of three clips, never the same clip twice in a row:"); for (int i = 0; i < 6; ++i) { mixer.Play(variants); Run(mixer, milliseconds: 500); } Console.WriteLine(" a drone on the Music bus, with pops on Effects over it."); Console.WriteLine(" 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); Run(mixer, milliseconds: 100); } music.Volume = 1f; music.IsMuted = true; Run(mixer, milliseconds: 700); music.IsMuted = false; Run(mixer, milliseconds: 700); Console.WriteLine($" voices in use: {mixer.ActiveCount}"); Run(mixer, milliseconds: 3000); } static void StealDemo(XAudio2Module module) { Console.WriteLine("Stealing: a mixer with 4 voices, all taken by quiet drones of priority 0."); Mixer mixer = new Mixer( module, new Xoshiro256PlusPlus(seed: 2), maxVoices: 4); Bus effects = mixer.AddBus("Effects"); SoundId[] drones = new SoundId[4]; double[] pitches = { 220, 277, 330, 415 }; for (int i = 0; i < drones.Length; ++i) { drones[i] = mixer.Define(new SoundDefinition( module.LoadSound(Tone(pitches[i], pitches[i], 6, SampleRate, 1)), effects, baseVolume: 0.5f)); } SoundId pop = mixer.Define(new SoundDefinition( module.LoadSound(Tone(800, 800, 0.3, SampleRate, 1)), effects, priority: 1)); foreach (SoundId drone in drones) { mixer.Play(drone); } Run(mixer, milliseconds: 800); Console.WriteLine(" four high-priority pops 100 ms apart, each fading out the oldest drone, no click:"); for (int i = 0; i < 4; ++i) { mixer.Play(pop); Console.WriteLine($" playing {mixer.ActiveCount}, fading {mixer.FadingCount}"); Run(mixer, milliseconds: 100); } Run(mixer, milliseconds: 1500); Run(mixer, milliseconds: 1000); Console.WriteLine(" a 300 Hz tone stopped mid-cycle, which should end without a click:"); Voice tone = mixer.Play(mixer.Define(new SoundDefinition( module.LoadSound(Tone(300, 300, 3, SampleRate, 1)), effects))); Run(mixer, milliseconds: 1013); mixer.Stop(tone); Run(mixer, milliseconds: 500); } static void VariationDemo(XAudio2Module module) { Console.WriteLine("Variation: one 440 Hz blip, each play drawing 3 semitones either way and -9 to 0 dB."); Mixer mixer = new Mixer(module, new Xoshiro256PlusPlus(seed: 3)); Bus effects = mixer.AddBus("Effects"); SoundId blip = mixer.Define(new SoundDefinition( module.LoadSound(Tone(440, 440, 0.25, SampleRate, 1)), effects, volumeDecibels: new Variation(-9, 0), pitchSemitones: Variation.PlusOrMinus(3))); for (int i = 0; i < 12; ++i) { Voice voice = mixer.Play(blip); Console.WriteLine($" play {i + 1}: pitch {module.PitchOf(voice):0.000}"); Run(mixer, milliseconds: 350); } Console.WriteLine(" the same blip with the pan set per play, left to right:"); for (int i = 0; i <= 8; ++i) { mixer.Play(blip, pan: -1f + i / 4f); Run(mixer, milliseconds: 350); } Console.WriteLine(" a longer tone that starts two octaves up, then SetPitch takes it two octaves down:"); SoundId held = mixer.Define(new SoundDefinition( module.LoadSound(Tone(440, 440, 3, SampleRate, 1)), effects, baseVolume: 0.5f)); Voice sweeping = mixer.Play(held, pitch: 4f); for (int step = 0; step <= 40; ++step) { mixer.SetPitch(sweeping, Pitch.FromSemitones(24f - step * 1.2f)); Run(mixer, milliseconds: 50); } mixer.Stop(sweeping); Run(mixer, milliseconds: 300); } static void PlacementDemo(XAudio2Module module) { Console.WriteLine("Placement: a tone moves from far left to far right across a view 10 units each way."); Mixer mixer = new Mixer(module, new Xoshiro256PlusPlus(seed: 4)); Bus effects = mixer.AddBus("Effects"); mixer.View = new View(Vector.Zero, 10); SoundId passing = mixer.Define(new SoundDefinition( module.LoadSound(Tone(440, 440, 3.2, SampleRate, 1)), effects, baseVolume: 0.5f, falloff: new Falloff(fullVolumeDistance: 10, silentDistance: 30))); Console.WriteLine($" a play 40 units away is dropped: {mixer.Play(passing, new Vector(-40, 0)).IsNone}"); Voice tone = mixer.Play(passing, new Vector(-29, 0)); for (int step = 0; step <= 58; ++step) { mixer.SetPosition(tone, new Vector(-29 + step, 0)); Run(mixer, milliseconds: 50); } Console.WriteLine(" the same tone standing still while the view pans past it:"); Voice standing = mixer.Play(passing, new Vector(0, 0)); for (int step = 0; step <= 58; ++step) { mixer.View = new View(new Vector(-29 + step, 0), 10); Run(mixer, milliseconds: 50); } mixer.Stop(standing); Run(mixer, milliseconds: 300); } static void LoopDemo(XAudio2Module module) { Console.WriteLine("Loop: a 1.5 s rising sweep that then repeats 0.3 s to 0.9 s of itself, stopped at 4 s."); Mixer mixer = new Mixer(module, new Xoshiro256PlusPlus(seed: 5)); Bus effects = mixer.AddBus("Effects"); SoundId looping = mixer.Define(new SoundDefinition( module.LoadSound(RisingSweepOfFloats(1.5)), effects, loop: new Loop(TimeSpan.FromSeconds(0.3), TimeSpan.FromSeconds(0.9)))); Voice voice = mixer.Play(looping); Run(mixer, milliseconds: 4000); Console.WriteLine($" still playing after the sweep's own 1.5 s: {mixer.IsPlaying(voice)}"); mixer.Stop(voice); Run(mixer, milliseconds: 300); } static byte[] WavFileOf(DecodedSound sound) { int bytesPerSample = DecodedSound.GetBytesPerSample(sound.Format); int blockAlign = sound.ChannelCount * bytesPerSample; byte[] samples = new byte[sound.Samples.Length]; sound.Samples.CopyTo(samples, 0); using (MemoryStream file = new MemoryStream()) using (BinaryWriter writer = new BinaryWriter(file)) { writer.Write(System.Text.Encoding.ASCII.GetBytes("RIFF")); writer.Write(36 + samples.Length); writer.Write(System.Text.Encoding.ASCII.GetBytes("WAVEfmt ")); writer.Write(16); writer.Write((short)(sound.Format == SampleFormat.Float32 ? 3 : 1)); writer.Write((short)sound.ChannelCount); writer.Write(sound.SampleRate); writer.Write(sound.SampleRate * blockAlign); writer.Write((short)blockAlign); writer.Write((short)(bytesPerSample * 8)); writer.Write(System.Text.Encoding.ASCII.GetBytes("data")); writer.Write(samples.Length); writer.Write(samples); return file.ToArray(); } } static void StreamDemo(XAudio2Module module) { Console.WriteLine("Stream: a 1.5 s rising sweep decoded as it plays, then the same sweep repeating " + "0.3 s to 0.9 s of itself until 4 s."); byte[] file = WavFileOf(RisingSweepOfFloats(1.5)); StreamedSound sound = (StreamedSound)module.LoadStream(new ByteChunks(new ByteView(file))).Result.Value; PumpUntilFinished(module, module.Play(sound, volume: 1f)); Voice looping = module.Play( sound, volume: 1f, loop: new Loop(TimeSpan.FromSeconds(0.3), TimeSpan.FromSeconds(0.9))); Pump(module, milliseconds: 4000); Console.WriteLine($" still playing after the sweep's own 1.5 s: {module.IsPlaying(looping)}"); module.Stop(looping); Pump(module, milliseconds: 300); sound.Dispose(); } static SoundId DefineStreamedTrack(XAudio2Module module, Mixer mixer, Bus music, DecodedSound track) { StreamedSound sound = (StreamedSound)module.LoadStream( new ByteChunks(new ByteView(WavFileOf(track)))).Result.Value; return mixer.Define(new SoundDefinition(sound, music, loop: Loop.Whole, fadeInSeconds: 1.5f, fadeOutSeconds: 1.5f)); } static void CrossfadeDemo(XAudio2Module module) { Console.WriteLine("Crossfade: two streamed tracks, a chord at 220 and 277 Hz then one at 330 and 415 Hz, " + "crossfading over 1.5 s every 3 s."); Mixer mixer = new Mixer(module, new Xoshiro256PlusPlus(seed: 6)); Bus music = mixer.AddBus("Music"); SoundId first = DefineStreamedTrack(module, mixer, music, Tone(220, 277, seconds: 2, SampleRate, channelCount: 2)); SoundId second = DefineStreamedTrack(module, mixer, music, Tone(330, 415, seconds: 2, SampleRate, channelCount: 2)); Voice current = mixer.Play(first); for (int round = 0; round < 3; ++round) { Run(mixer, milliseconds: 3000); mixer.Stop(current); current = mixer.Play(round % 2 == 0 ? second : first); Console.WriteLine($" crossfading, playing {mixer.ActiveCount}, fading {mixer.FadingCount}"); } Run(mixer, milliseconds: 3000); mixer.Stop(current); Run(mixer, milliseconds: 1800); } static void PauseDemo(XAudio2Module module) { Console.WriteLine("Pause: 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."); Mixer mixer = new Mixer(module, new Xoshiro256PlusPlus(seed: 7)); Bus music = mixer.AddBus("Music"); Bus effects = mixer.AddBus("Effects"); SoundId sweep = mixer.Define(new SoundDefinition( module.LoadSound(RisingSweepOfFloats(2)), music, loop: Loop.Whole)); SoundId blip = mixer.Define(new SoundDefinition( module.LoadSound(Tone(660, 660, seconds: 0.2, SampleRate, channelCount: 1)), effects)); Voice voice = mixer.Play(sweep); Run(mixer, milliseconds: 1000); music.IsPaused = true; for (int i = 0; i < 3; ++i) { Run(mixer, milliseconds: 500); mixer.Play(blip); } music.IsPaused = false; Run(mixer, milliseconds: 1500); mixer.Stop(voice); Run(mixer, milliseconds: 300); } static void BackgroundDemo(XAudio2Module module) { Console.WriteLine("Background: 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."); Mixer mixer = new Mixer(module, new Xoshiro256PlusPlus(seed: 8)); Bus music = mixer.AddBus("Music"); SoundId sweep = mixer.Define(new SoundDefinition( module.LoadSound(RisingSweepOfFloats(2)), music, loop: Loop.Whole)); Voice voice = mixer.Play(sweep); Run(mixer, milliseconds: 1000); foreach (BackgroundPolicy policy in new[] { BackgroundPolicy.Mute, BackgroundPolicy.Duck, BackgroundPolicy.Pause }) { Console.WriteLine($" background, {policy}"); mixer.BackgroundPolicy = policy; mixer.IsInBackground = true; Run(mixer, milliseconds: 1500); mixer.IsInBackground = false; Console.WriteLine(" foreground"); Run(mixer, milliseconds: 1000); } mixer.Stop(voice); Run(mixer, milliseconds: 300); } // A whole number of cycles in a second, so the loop joins without a click. static DecodedSound HumOfFloats(double hertz) { float[] samples = new float[SampleRate]; for (int frame = 0; frame < samples.Length; ++frame) { samples[frame] = 0.3f * (float)Math.Sin(2 * Math.PI * hertz * frame / SampleRate); } byte[] bytes = new byte[samples.Length * 4]; Buffer.BlockCopy(samples, 0, bytes, 0, bytes.Length); return new DecodedSound(new ByteView(bytes), SampleRate, channelCount: 1, SampleFormat.Float32); } static void FactoryDemo(XAudio2Module module) { Console.WriteLine("Factories: three 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 reporting every frame while it is on."); Mixer mixer = new Mixer(module, new Xoshiro256PlusPlus(seed: 9)); Bus effects = mixer.AddBus("Effects"); mixer.View = new View(Vector.Zero, 10); SoundId hum = mixer.Define(new SoundDefinition(module.LoadSound(HumOfFloats(110)), effects, 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(); double last = 0; 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; Console.WriteLine($" {on} factories on, {mixer.ActiveCount} voice playing"); } mixer.Update((float)(now - last)); last = now; Thread.Sleep(16); } } static void LimiterDemo(XAudio2Module module) { Console.WriteLine("Limiter: 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(Tone(220 * (i + 1), 220 * (i + 1), seconds: 1.2, SampleRate, channelCount: 1)); } foreach (bool limited in new[] { true, false, true }) { module.LimiterEnabled = limited; Console.WriteLine($" {(limited ? "limited" : "unlimited")}"); foreach (Sound tone in tones) { module.Play(tone, volume: 1f); } Pump(module, milliseconds: 1800); } module.LimiterEnabled = before; } static int Main(string[] args) { XAudio2Module module = new XAudio2Module(); if (!module.IsSupported) { Console.WriteLine("XAudio2 or an audio device isn't available on this machine."); return 1; } module.Load(); try { PrintPerformance(module, "loaded"); PlayFiles(module, args); PlayKinds(module); PlayAtOtherRates(module); FadeOut(module); PanDemo(module); PitchDemo(module); Burst(module); MixerDemo(module); StealDemo(module); VariationDemo(module); PlacementDemo(module); LoopDemo(module); StreamDemo(module); CrossfadeDemo(module); PauseDemo(module); BackgroundDemo(module); FactoryDemo(module); LimiterDemo(module); } finally { module.Unload(); } Console.WriteLine("Done."); return 0; } } }