halkyon/apps/meditate/tool/bells.py
Tizian 「ティツィアーン」 cb026dce8d
feat(meditate): the app: Today, Explore, Profile, the sitting and its bells
halkyon_meditate_flutter on halkyon_app and halkyon_ui (ADR 0038): the
first start, sign-in and account as every Halkyon app's, a bar below
medium windows and a rail with the icon from medium on, fern washes,
Settings in cards (Sync, the operator's, Sitting, Practice, the
account, What's new, About, the version), English and German.

- Today: a greeting by the hour, Sit now at 5, 10, 20 minutes and the
  timer's own length, the practice of the day, the week with the days
  in a row, favourites, recently sat (D9).
- Explore: six practices of Halkyon's own, written for the app, found
  by a search without case or accents, by goal and by kind; each page
  with how it goes, its lengths, its heart and Start (D4, D10).
- The sitting, over the tabs: its time from the clock, a ring counting
  down, the cue of the moment, Pause and End, the bells at the start,
  between and at the end, the screen kept on; ended early, kept from a
  minute on where the person says so (D5, D6).
- Profile: every Halkyon app's, then the person's practice: minutes,
  sittings, days, the week and every sitting kept, to delete (D8).
- Settings and sittings sealed with the person's key and synced (D7).

The bells are synthesised by apps/meditate/tool/bells.py, the icon is
Lucide's sprout from cobalt through pacific into fern
(tool/app_icons.py). Releases and the deploy are a later step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-04 09:31:56 +01:00

102 lines
3.2 KiB
Python

#!/usr/bin/env python3
"""Writes Halkyon Meditate's bells (ADR 0038, D5) into the app's assets.
Each bell is synthesised here from sine partials and decays, with nothing
recorded or downloaded, so the sounds are the project's own and under its
licence. Python's standard library alone; run from apps/meditate:
python3 -I tool/bells.py
It writes assets/bells/bowl.wav, small_bell.wav and wood_block.wav into
halkyon_meditate_flutter, mono 16-bit PCM at 22,050 Hz. The output is the
same on every run (the wooden block's noise has a fixed seed).
"""
import math
import random
import struct
import wave
from pathlib import Path
RATE = 22050
OUT = Path(__file__).resolve().parent.parent / "halkyon_meditate_flutter" / "assets" / "bells"
def partials(seconds, parts, attack=0.008):
"""Sums sine partials (frequency in Hz, amplitude, decay time in s)."""
samples = []
for n in range(int(seconds * RATE)):
t = n / RATE
rise = min(1.0, t / attack)
value = sum(amp * math.exp(-t / decay) * math.sin(2 * math.pi * freq * t) for freq, amp, decay in parts)
samples.append(rise * value)
return samples
def bowl():
"""A singing bowl: a low hum with inharmonic partials, two of them close
together so that it beats slowly, dying away over several seconds."""
base = 196.0
return partials(
9.0,
[
(base, 1.0, 3.2),
(base * 1.007, 0.6, 3.0),
(base * 2.71, 0.45, 2.0),
(base * 2.73, 0.3, 1.8),
(base * 5.13, 0.18, 1.1),
(base * 8.27, 0.08, 0.6),
],
attack=0.02,
)
def small_bell():
"""A small bell: brighter, a bell's minor-third partial, shorter."""
base = 784.0
return partials(
4.0,
[
(base * 0.5, 0.35, 1.6),
(base, 1.0, 1.3),
(base * 1.19, 0.45, 1.0),
(base * 1.5, 0.35, 0.8),
(base * 2.0, 0.3, 0.6),
(base * 2.51, 0.12, 0.35),
],
attack=0.003,
)
def wood_block():
"""A wooden block: a hollow knock, a short tone and a click of noise."""
noise = random.Random(38)
samples = []
for n in range(int(0.45 * RATE)):
t = n / RATE
tone = math.exp(-t / 0.045) * (math.sin(2 * math.pi * 1180 * t) + 0.5 * math.sin(2 * math.pi * 2410 * t))
click = math.exp(-t / 0.004) * (noise.random() * 2 - 1)
samples.append(min(1.0, t / 0.001) * (tone + 0.6 * click))
return samples
def write(name, samples, peak=0.7):
"""Writes [samples] normalised to [peak], faded out over the last 50 ms."""
top = max(abs(sample) for sample in samples) or 1.0
fade = int(0.05 * RATE)
frames = bytearray()
for n, sample in enumerate(samples):
tail = min(1.0, (len(samples) - n) / fade)
frames += struct.pack("<h", round(sample / top * peak * tail * 32767))
OUT.mkdir(parents=True, exist_ok=True)
with wave.open(str(OUT / name), "wb") as file:
file.setnchannels(1)
file.setsampwidth(2)
file.setframerate(RATE)
file.writeframes(bytes(frames))
if __name__ == "__main__":
write("bowl.wav", bowl())
write("small_bell.wav", small_bell())
write("wood_block.wav", wood_block())