using System; using TravelEar.Core; using Xunit; namespace TravelEar.Tests; /// The red bells' super-wet bloom and the chorus in it (catalogue rows 9-10, section 5.5). public class SpeechlessBloomTests { private const int Rate = 48_000; private static float[] Sine(float hz, int samples, float amplitude = 0.5f) { var block = new float[samples]; for (var i = 0; i < samples; i++) block[i] = amplitude * MathF.Sin(2f * MathF.PI * hz * i / Rate); return block; } private static float Energy(ReadOnlySpan block) { var sum = 0.0; for (var i = 0; i < block.Length; i++) sum += block[i] * (double)block[i]; return (float)sum; } // [unit->REQ-MIXER-RESYNTH] [Fact] public void Chorus_dry_only_is_a_passthrough_and_wet_taps_add_delayed_copies() { var dry = new Chorus(Rate, 1f, 0f, 0f, 0f, 100f, 0.5f, 0.5f); var block = Sine(440f, 4800); var copy = (float[])block.Clone(); dry.Process(block); for (var i = 0; i < block.Length; i++) Assert.Equal(copy[i], block[i], 5); var chorus = Chorus.SuperWet(Rate); var impulse = new float[Rate / 4]; impulse[0] = 1f; chorus.Process(impulse); Assert.Equal(1f, impulse[0], 4); // the dry copy var tail = 0f; for (var i = 1; i < impulse.Length; i++) tail += MathF.Abs(impulse[i]); Assert.InRange(tail, 1.4f, 1.6f); // wet 0.75 + 0.5 + 0.25, spread by the interpolation } // [unit->REQ-MIXER-RESYNTH] [Fact] public void Closed_return_renders_nothing_and_does_no_work() { var bloom = new SpeechlessBloom(Rate); var voice = Sine(440f, 2880); var output = new float[2880]; Assert.False(bloom.Process(voice, output, 0f, 1f, SpeechlessBloom.FloorDb)); Assert.Equal(0f, Energy(output)); Assert.False(bloom.Active); } // [unit->REQ-MIXER-RESYNTH] [Fact] public void Open_return_blooms_the_voice_and_rings_out_after_it_closes() { var bloom = new SpeechlessBloom(Rate); var voice = Sine(440f, 2880); var output = new float[2880]; var opened = 0f; for (var i = 0; i < 20; i++) { Assert.True(bloom.Process(voice, output, 0f, 0.8f, -8.7f)); // sp 0.75 opened += Energy(output); } Assert.True(opened > 0f); Assert.True(bloom.Active); Assert.Equal(0.8f, bloom.Pitch, 3); var silence = new float[2880]; Assert.True(bloom.Process(silence, output, 0f, 1f, SpeechlessBloom.FloorDb)); Assert.True(Energy(output) > 0f, "the 6.8 s tail keeps ringing after the return closes"); Assert.True(bloom.Active); for (var i = 0; i < (int)(SpeechlessBloom.TailSeconds * Rate / 2880) + 2; i++) bloom.Process(silence, output, 0f, 1f, SpeechlessBloom.FloorDb); Assert.False(bloom.Active); Assert.False(bloom.Process(silence, output, 0f, 1f, SpeechlessBloom.FloorDb)); } // [unit->REQ-MIXER-RESYNTH] [Fact] public void Return_level_follows_the_game_dB_and_the_bus_gain() { var quiet = new SpeechlessBloom(Rate); var loud = new SpeechlessBloom(Rate); var voice = Sine(440f, 2880); var a = new float[2880]; var b = new float[2880]; var ea = 0f; var eb = 0f; for (var i = 0; i < 10; i++) { quiet.Process(voice, a, 0f, 1f, -34.1f); // sp 0.25 loud.Process(voice, b, 0f, 1f, 0f); // sp 1 ea += Energy(a); eb += Energy(b); } Assert.InRange(10f * MathF.Log10(eb / ea), 33f, 35.5f); Assert.Equal(MixerStageModel.Gain(-34.1f), quiet.ReturnGain, 4); } }