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())
|