// Synthesizes the game's SFX as 16-bit PCM WAVs so no licensed audio is // needed. Run after changing any envelope: // dart run tool/gen_sfx.dart import 'dart:io'; import 'dart:math' as math; import 'dart:typed_data'; const sampleRate = 44100; void main() { final outDir = Directory('assets/audio')..createSync(recursive: true); _write(outDir, 'place.wav', _tone([(180, 220)], 0.06, gain: 0.5)); _write(outDir, 'clear.wav', _tone([(440, 880)], 0.16, gain: 0.6)); _write( outDir, 'combo.wav', _concat([ _tone([(660, 660)], 0.07, gain: 0.55), _tone([(880, 880)], 0.07, gain: 0.55), _tone([(1175, 1175)], 0.1, gain: 0.6), ])); _write( outDir, 'win.wav', _concat([ _tone([(523, 523)], 0.12, gain: 0.55), _tone([(659, 659)], 0.12, gain: 0.55), _tone([(784, 784)], 0.12, gain: 0.55), _tone([(1047, 1047)], 0.3, gain: 0.6), ])); _write(outDir, 'lose.wav', _tone([(330, 150)], 0.45, gain: 0.5)); stdout.writeln('SFX written to ${outDir.path}'); } /// Sine sweep segments (startHz, endHz) with a soft attack/decay envelope. Float64List _tone(List<(double, double)> sweeps, double seconds, {double gain = 0.5}) { final n = (seconds * sampleRate).round(); final out = Float64List(n); var phase = 0.0; for (var i = 0; i < n; i++) { final t = i / n; final seg = sweeps[(t * sweeps.length).floor().clamp(0, sweeps.length - 1)]; final freq = seg.$1 + (seg.$2 - seg.$1) * t; phase += 2 * math.pi * freq / sampleRate; final attack = math.min(1.0, i / (0.005 * sampleRate)); final decay = math.min(1.0, (n - i) / (0.05 * sampleRate)); out[i] = math.sin(phase) * gain * attack * decay; } return out; } Float64List _concat(List parts) { final total = parts.fold(0, (sum, p) => sum + p.length); final out = Float64List(total); var offset = 0; for (final p in parts) { out.setAll(offset, p); offset += p.length; } return out; } void _write(Directory dir, String name, Float64List samples) { final pcm = Int16List(samples.length); for (var i = 0; i < samples.length; i++) { pcm[i] = (samples[i].clamp(-1.0, 1.0) * 32767).round(); } final dataSize = pcm.length * 2; final header = ByteData(44) ..setUint32(0, 0x52494646, Endian.big) // RIFF ..setUint32(4, 36 + dataSize, Endian.little) ..setUint32(8, 0x57415645, Endian.big) // WAVE ..setUint32(12, 0x666d7420, Endian.big) // fmt ..setUint32(16, 16, Endian.little) ..setUint16(20, 1, Endian.little) // PCM ..setUint16(22, 1, Endian.little) // mono ..setUint32(24, sampleRate, Endian.little) ..setUint32(28, sampleRate * 2, Endian.little) ..setUint16(32, 2, Endian.little) ..setUint16(34, 16, Endian.little) ..setUint32(36, 0x64617461, Endian.big) // data ..setUint32(40, dataSize, Endian.little); final file = File('${dir.path}/$name'); final bytes = BytesBuilder() ..add(header.buffer.asUint8List()) ..add(pcm.buffer.asUint8List()); file.writeAsBytesSync(bytes.toBytes()); stdout.writeln(' $name (${(dataSize / 1024).toStringAsFixed(1)} KB)'); }