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>
102 lines
3.2 KiB
Python
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())
|