halkyon/apps/meditate/tool/bells.py

102 lines
3.2 KiB
Python
Raw Permalink Normal View History

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-03 09:25:30 +01:00
#!/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
refactor!: packages, paths and app IDs under Halkyon Days' names (ADR 0045, step 3) Learn's folder is apps/halkyon, Todo's apps/myrmex; every package of ours takes the prefix halkyone_ (halkyone_halkyon_*, halkyone_myrmex_*, halkyone_train_*, halkyone_meditate_*, halkyone_app, halkyone_base_* and the other shared ones; halkyon_oidc_* keeps its name, D11). Halkyon and Myrmex take the app IDs one.halky.halkyon and one.halky.myrmex (D10); Train and Meditate keep theirs, while every app's Android namespace, Kotlin package and method channels move to one.halky. The snaps are halkyon and myrmex (D17), each built in a clone with its recipe in place of snap/, as Train's and Todo's were. Serverpod finds a database's migrations by its module's name, which changed with the packages': - each server's new, empty migration ("rename") records itself under the new names and deletes the rows under the old ones; - deploy/redeploy.sh copies each database's record to the new name before the migrations (carry_module), so the renamed servers find their databases migrated; a copy, never a move, so a deploy failing before the migration commits leaves the old rows in place; - Train and Meditate keep their devices' databases: halkyone_app's carryMigrationRecord copies their record before the sync engine opens them (openPersonDatabase); - the client migrations' generated moduleName getters, one line each, say the new names: Serverpod's client asserts in debug builds that every migration names the module, and create-migration never writes a migration's files again. Train's WorkManager work is enqueued under new names, the former ones cancelled: the work kept names the workers' former classes. The release tool: an app's tags of its earlier names count as its history (legacyTagPrefixes), the shared packages' former paths count for its version, and every snap is built in a clone (atRoot gone). The databases, Docker images and Compose project keep their names until step 5. BREAKING CHANGE: Halkyon (Learn) and Myrmex (Todo) are new apps to Android and the Snap Store: installs of app.halkyon.learn and app.halkyon.todo don't update to them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-12 23:21:45 +01:00
halkyone_meditate_flutter, mono 16-bit PCM at 22,050 Hz. The output is the
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-03 09:25:30 +01:00
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
refactor!: packages, paths and app IDs under Halkyon Days' names (ADR 0045, step 3) Learn's folder is apps/halkyon, Todo's apps/myrmex; every package of ours takes the prefix halkyone_ (halkyone_halkyon_*, halkyone_myrmex_*, halkyone_train_*, halkyone_meditate_*, halkyone_app, halkyone_base_* and the other shared ones; halkyon_oidc_* keeps its name, D11). Halkyon and Myrmex take the app IDs one.halky.halkyon and one.halky.myrmex (D10); Train and Meditate keep theirs, while every app's Android namespace, Kotlin package and method channels move to one.halky. The snaps are halkyon and myrmex (D17), each built in a clone with its recipe in place of snap/, as Train's and Todo's were. Serverpod finds a database's migrations by its module's name, which changed with the packages': - each server's new, empty migration ("rename") records itself under the new names and deletes the rows under the old ones; - deploy/redeploy.sh copies each database's record to the new name before the migrations (carry_module), so the renamed servers find their databases migrated; a copy, never a move, so a deploy failing before the migration commits leaves the old rows in place; - Train and Meditate keep their devices' databases: halkyone_app's carryMigrationRecord copies their record before the sync engine opens them (openPersonDatabase); - the client migrations' generated moduleName getters, one line each, say the new names: Serverpod's client asserts in debug builds that every migration names the module, and create-migration never writes a migration's files again. Train's WorkManager work is enqueued under new names, the former ones cancelled: the work kept names the workers' former classes. The release tool: an app's tags of its earlier names count as its history (legacyTagPrefixes), the shared packages' former paths count for its version, and every snap is built in a clone (atRoot gone). The databases, Docker images and Compose project keep their names until step 5. BREAKING CHANGE: Halkyon (Learn) and Myrmex (Todo) are new apps to Android and the Snap Store: installs of app.halkyon.learn and app.halkyon.todo don't update to them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-12 23:21:45 +01:00
OUT = Path(__file__).resolve().parent.parent / "halkyone_meditate_flutter" / "assets" / "bells"
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-03 09:25:30 +01:00
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())