namespace TravelEar.Core; /// /// The game's BitCrusher (AudioSystem) ported from its managed Burst fallback /// (Process$BurstManaged, docs/reference/big-walk-local-voice-wiring.md and the T2 notes in /// M3-PLAN.md): per hold of step = sampleRate / crushRate samples, the block's first sample /// is quantized to bitDepth bits and held, ramping linearly toward the next block's quantized /// first sample by the smooth fraction, then mixed dryWet with the input. Quantization: /// ampVal = 2^(bitDepth-1), crushScale = log(bitDepth+1) / log(25) / ampVal, /// q(x) = clamp(trunc(x * ampVal +- 0.5) * crushScale, -1, 1). The game runs it at 24 bits, /// where the quantization is inaudible and the sample-hold is the whole effect. Mono; stateless /// across frames, as the game's kernel is across DSP blocks. /// public sealed class BitCrusher { public int BitDepth { get; } public int CrushRateHz { get; private set; } public float DryWet { get; private set; } public float Smooth { get; private set; } private readonly float _ampVal; private readonly float _crushScale; public BitCrusher(int bitDepth, int crushRateHz, float dryWet, float smooth) { BitDepth = Math.Max(1, bitDepth); _ampVal = MathF.Pow(2f, BitDepth - 1f); _crushScale = MathF.Log(BitDepth + 1f) / MathF.Log(25f) / _ampVal; Set(crushRateHz, dryWet, smooth); } /// The per-frame parameters the game's VoicePlayer.Update drives. public void Set(int crushRateHz, float dryWet, float smooth) { CrushRateHz = Math.Max(1, crushRateHz); DryWet = Math.Clamp(dryWet, 0f, 1f); Smooth = Math.Clamp(smooth, 0f, 1f); } /// The game's hold length: max(1, outputSampleRate / crushRate) (integer division). public int StepFor(int sampleRate) => Math.Max(1, sampleRate / CrushRateHz); private float Quantize(float x) { var v = x * _ampVal; v = v >= 0f ? v + 0.5f : v - 0.5f; var q = (int)v * _crushScale; // truncation toward zero, as cvttss2si return q > 1f ? 1f : q < -1f ? -1f : q; } // [impl->REQ-RENDER-MEGAPHONE] /// Processes in place at ; no allocation. public void Process(Span mono, int sampleRate) { var n = mono.Length; if (n == 0) return; var step = StepFor(sampleRate); var wet = DryWet; var dry = 1f - DryWet; if (step == 1) { for (var i = 0; i < n; i++) mono[i] = Quantize(mono[i]) * wet + mono[i] * dry; return; } for (var start = 0; start < n; start += step) { var q0 = Quantize(mono[start]); var next = start + step; var slope = 0f; if (next < n && Smooth > 0f) slope = (Quantize(mono[next]) - q0) / step * Smooth; for (var k = 0; k < step; k++) { var idx = start + k; if (idx >= n) break; mono[idx] = (q0 + k * slope) * wet + mono[idx] * dry; } } } }