The developer's request of 10 Nov 2026: completing a task should feel like something. `CompletionFeedback`, held by `TodoDevice` and handed to each person's `TodoPerson`, gives a light haptic tick on Android and iOS and, on desktops, a short quiet chime of our own: two soft bell tones a fifth apart, synthesized by `apps/todo/tool/completion_sound.dart` into `assets/sounds/task_done.wav`, so it needs no one's licence. A task's check and the selection's Complete call it once, however many tasks are done; an undo is silent. Settings' Tasks turns the chime off on a desktop, kept per device in `<app support>/completion_feedback.json`; a phone is offered no switch. A device that can't play loses only the sound. The player is `audioplayers`, as Learn's sounds: on Linux it links GStreamer, so Todo's snap gains the `audio-playback` plug and GStreamer's headers to build, as Learn's recipe has. Whats-New: Completing a task now gives a light tap on phones and a short chime on computers, which Settings › Tasks turns off. Whats-New-De: Eine erledigte Aufgabe gibt jetzt auf dem Handy ein leichtes Tippen und am Computer einen kurzen Klang, den du unter Einstellungen › Aufgaben ausschaltest. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
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]);
|
|
}
|