161 lines
5.6 KiB
C#
161 lines
5.6 KiB
C#
|
|
using Godot;
|
||
|
|
using System;
|
||
|
|
|
||
|
|
namespace Chessistics.Scripts.Presentation;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Procedural sound effects via synthesized waveforms.
|
||
|
|
/// No external audio files needed — everything is generated in code.
|
||
|
|
/// </summary>
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>Simple ADSR-ish envelope: quick attack, sustain, smooth release.</summary>
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
}
|