using Godot; using System; namespace Chessistics.Scripts.Presentation; /// /// Procedural sound effects via synthesized waveforms. /// No external audio files needed — everything is generated in code. /// public partial class SfxManager : Node { private const int SampleRate = 22050; private const float MasterVolume = 0.15f; public static SfxManager? Instance { get; private set; } public override void _Ready() { Instance = this; } // --- Public API --- public void PlayPlace() => PlayTone(523.25f, 0.08f, vol: 0.5f); // C5 short blip public void PlayProduce() => PlayTone(130.81f, 0.12f, vol: 0.3f, wave: Wave.Triangle); // C3 warm public void PlayTransfer() => PlayNoise(0.12f, vol: 0.15f); // filtered swoosh public void PlayDeliver() => PlayChord([523.25f, 659.25f], 0.18f, vol: 0.4f); // C5+E5 ding public void PlayMove() => PlayNoise(0.04f, vol: 0.08f); // tiny whoosh public void PlayDestroy() => PlaySweep(262f, 65f, 0.18f, vol: 0.4f); // descending crunch public void PlayVictory() => PlayArpeggio([262f, 330f, 392f, 523f], 0.12f, vol: 0.35f); // C-E-G-C arp public void PlayClick() => PlayTone(880f, 0.02f, vol: 0.2f); // tiny tick // --- Synthesis --- private enum Wave { Sine, Triangle, Square } private void PlayTone(float freq, float duration, float vol = 0.3f, Wave wave = Wave.Sine) { var samples = GenerateTone(freq, duration, vol, wave); PlaySamples(samples); } private void PlayNoise(float duration, float vol = 0.2f) { var count = (int)(SampleRate * duration); var samples = new float[count]; var rng = new Random(); for (int i = 0; i < count; i++) { float t = (float)i / count; float envelope = Envelope(t); samples[i] = (float)(rng.NextDouble() * 2 - 1) * vol * envelope * MasterVolume; } // Simple low-pass: average with previous sample for (int i = count - 1; i > 0; i--) samples[i] = (samples[i] + samples[i - 1]) * 0.5f; PlaySamples(samples); } private void PlayChord(float[] freqs, float duration, float vol = 0.3f) { var count = (int)(SampleRate * duration); var samples = new float[count]; foreach (var freq in freqs) { var tone = GenerateTone(freq, duration, vol / freqs.Length, Wave.Sine); for (int i = 0; i < count; i++) samples[i] += tone[i]; } PlaySamples(samples); } private void PlaySweep(float startFreq, float endFreq, float duration, float vol = 0.3f) { var count = (int)(SampleRate * duration); var samples = new float[count]; for (int i = 0; i < count; i++) { float t = (float)i / count; float freq = Mathf.Lerp(startFreq, endFreq, t); float envelope = Envelope(t); float phase = 2f * Mathf.Pi * freq * i / SampleRate; samples[i] = Mathf.Sin(phase) * vol * envelope * MasterVolume; } PlaySamples(samples); } private void PlayArpeggio(float[] notes, float noteLength, float vol = 0.3f) { float totalDuration = noteLength * notes.Length; var totalCount = (int)(SampleRate * totalDuration); var samples = new float[totalCount]; var noteCount = (int)(SampleRate * noteLength); for (int n = 0; n < notes.Length; n++) { int offset = n * noteCount; var tone = GenerateTone(notes[n], noteLength, vol, Wave.Sine); for (int i = 0; i < tone.Length && offset + i < totalCount; i++) samples[offset + i] += tone[i]; } PlaySamples(samples); } private float[] GenerateTone(float freq, float duration, float vol, Wave wave) { var count = (int)(SampleRate * duration); var samples = new float[count]; for (int i = 0; i < count; i++) { float t = (float)i / count; float phase = 2f * Mathf.Pi * freq * i / SampleRate; float value = wave switch { Wave.Triangle => 2f * Mathf.Abs(2f * ((freq * i / SampleRate) % 1f) - 1f) - 1f, Wave.Square => Mathf.Sin(phase) >= 0 ? 1f : -1f, _ => Mathf.Sin(phase) }; float envelope = Envelope(t); samples[i] = value * vol * envelope * MasterVolume; } return samples; } /// Simple ADSR-ish envelope: quick attack, sustain, smooth release. private static float Envelope(float t) { if (t < 0.05f) return t / 0.05f; // attack if (t < 0.3f) return 1f; // sustain return 1f - (t - 0.3f) / 0.7f; // release } private void PlaySamples(float[] samples) { var stream = new AudioStreamWav { Format = AudioStreamWav.FormatEnum.Format16Bits, MixRate = SampleRate, Stereo = false, Data = FloatsToWav16(samples) }; var player = new AudioStreamPlayer { Stream = stream, VolumeDb = -6f }; AddChild(player); player.Finished += () => player.QueueFree(); player.Play(); } private static byte[] FloatsToWav16(float[] samples) { var bytes = new byte[samples.Length * 2]; for (int i = 0; i < samples.Length; i++) { short val = (short)(Mathf.Clamp(samples[i], -1f, 1f) * 32767); bytes[i * 2] = (byte)(val & 0xFF); bytes[i * 2 + 1] = (byte)((val >> 8) & 0xFF); } return bytes; } }