86 lines
3.1 KiB
Dart
86 lines
3.1 KiB
Dart
|
|
// Writes Todo's chime of a task done (decided by the developer, 10 Nov
|
||
|
|
// 2026), played on desktops: two soft bell tones a fifth apart, A5 then E6,
|
||
|
|
// each fading away, quiet, a quarter of a second in all. Synthesized here, so
|
||
|
|
// it is ours and needs no licence of anyone's; run from apps/todo:
|
||
|
|
//
|
||
|
|
// dart run tool/completion_sound.dart
|
||
|
|
//
|
||
|
|
// It writes halkyon_todo_flutter/assets/sounds/task_done.wav, a mono 16-bit
|
||
|
|
// WAV at 44.1 kHz; the same every run.
|
||
|
|
import 'dart:io';
|
||
|
|
import 'dart:math' as math;
|
||
|
|
import 'dart:typed_data';
|
||
|
|
|
||
|
|
const _rate = 44100;
|
||
|
|
|
||
|
|
/// The loudest the chime gets, of full scale.
|
||
|
|
const _peak = 0.3;
|
||
|
|
|
||
|
|
/// Each tone: when it starts and how long it rings, in seconds.
|
||
|
|
const _tones = [
|
||
|
|
(hertz: 880.0, start: 0.0, length: 0.16),
|
||
|
|
(hertz: 1318.51, start: 0.07, length: 0.18),
|
||
|
|
];
|
||
|
|
|
||
|
|
void main() {
|
||
|
|
final length = _tones
|
||
|
|
.map((tone) => tone.start + tone.length)
|
||
|
|
.reduce(math.max);
|
||
|
|
final count = (length * _rate).ceil();
|
||
|
|
final samples = Float64List(count);
|
||
|
|
for (final tone in _tones) {
|
||
|
|
final first = (tone.start * _rate).round();
|
||
|
|
final last = math.min(count, first + (tone.length * _rate).round());
|
||
|
|
for (var index = first; index < last; index++) {
|
||
|
|
final t = (index - first) / _rate;
|
||
|
|
// A few milliseconds rising, so it doesn't click, then fading as a
|
||
|
|
// bell does; a quiet octave above for its shimmer.
|
||
|
|
final attack = math.min(1, t / 0.004);
|
||
|
|
final fade = math.exp(-t * 28);
|
||
|
|
final end = math.min(1, (last - index) / (_rate * 0.01));
|
||
|
|
final wave =
|
||
|
|
math.sin(2 * math.pi * tone.hertz * t) +
|
||
|
|
0.2 * math.sin(4 * math.pi * tone.hertz * t);
|
||
|
|
samples[index] += wave * attack * fade * end;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
final loudest = samples.map((sample) => sample.abs()).reduce(math.max);
|
||
|
|
final pcm = ByteData(count * 2);
|
||
|
|
for (var index = 0; index < count; index++) {
|
||
|
|
pcm.setInt16(
|
||
|
|
index * 2,
|
||
|
|
(samples[index] / loudest * _peak * 32767).round(),
|
||
|
|
Endian.little,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
final file = File('halkyon_todo_flutter/assets/sounds/task_done.wav')
|
||
|
|
..createSync(recursive: true)
|
||
|
|
..writeAsBytesSync(_wav(pcm.buffer.asUint8List()));
|
||
|
|
stdout.writeln('Wrote ${file.path} (${file.lengthSync()} bytes).');
|
||
|
|
}
|
||
|
|
|
||
|
|
/// [pcm] in a WAV file's RIFF header: one channel, 16 bits at [_rate].
|
||
|
|
Uint8List _wav(Uint8List pcm) {
|
||
|
|
final header = ByteData(44);
|
||
|
|
void text(int offset, String value) {
|
||
|
|
for (var index = 0; index < value.length; index++) {
|
||
|
|
header.setUint8(offset + index, value.codeUnitAt(index));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
text(0, 'RIFF');
|
||
|
|
header.setUint32(4, 36 + pcm.length, Endian.little);
|
||
|
|
text(8, 'WAVE');
|
||
|
|
text(12, 'fmt ');
|
||
|
|
header
|
||
|
|
..setUint32(16, 16, Endian.little)
|
||
|
|
..setUint16(20, 1, Endian.little) // PCM
|
||
|
|
..setUint16(22, 1, Endian.little) // one channel
|
||
|
|
..setUint32(24, _rate, Endian.little)
|
||
|
|
..setUint32(28, _rate * 2, Endian.little) // bytes a second
|
||
|
|
..setUint16(32, 2, Endian.little) // bytes a frame
|
||
|
|
..setUint16(34, 16, Endian.little); // bits a sample
|
||
|
|
text(36, 'data');
|
||
|
|
header.setUint32(40, pcm.length, Endian.little);
|
||
|
|
return Uint8List.fromList([...header.buffer.asUint8List(), ...pcm]);
|
||
|
|
}
|