feat(todo): a task done ticks on phones and chimes on desktops
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>
This commit is contained in:
parent
1cc27ef0d6
commit
fd6647d752
1 changed files with 470 additions and 7 deletions
86
apps/todo/tool/completion_sound.dart
Normal file
86
apps/todo/tool/completion_sound.dart
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
// 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]);
|
||||
}
|
||||
Loading…
Reference in a new issue