ad6689b42f
Pure-Dart WAV synthesizer (tool/gen_sfx.dart) generates place/clear/ combo/win/lose effects; AudioService player pool fires on placement, line clears, combo streaks, and phase transitions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
93 lines
3.1 KiB
Dart
93 lines
3.1 KiB
Dart
// 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<Float64List> parts) {
|
|
final total = parts.fold<int>(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)');
|
|
}
|