Stage 4: guided Player for both Modes and the Session summary
- buildSegments lays a Plan out as holds with Transitions (get ready, switch sides, next round, next exercise) or sets with count-up Rests; Supersets alternate members and rest per round unless asked otherwise - SessionRunner drives segments against an injected clock: auto-advance with a chime, pause, skip, back, rep sets confirmed with logged reps and kg, timed sets started on demand, early end - Player screen: countdown ring, accent pulse in the last 3 s, Flip Animation, next-up cards, Rest Screen, mute toggle, screen kept on - Sessions table (schema v2 migration) keeps a Plan snapshot and Set Logs; nothing is saved when no hold or set was done - Summary with time, exercises or sets logged, and every Set Log - Synthesised quiet cue sounds (tools/sounds), status bar per Mode Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
bbdca95541
commit
971ff64117
28 files changed
+3465
-84
No files matched your search
@@ -0,0 +1,61 @@
|
||||
"""Synthesises the Player's two cue sounds into assets/sounds/ (standard library only).
|
||||
|
||||
- stretching_cue.wav: a soft singing bowl, slow attack, long gentle decay.
|
||||
- workout_cue.wav: a short, muted wooden "tok".
|
||||
|
||||
Both are kept quiet (peak about -12 dBFS) so they sit under the user's music.
|
||||
"""
|
||||
|
||||
import math
|
||||
import random
|
||||
import struct
|
||||
import wave
|
||||
from pathlib import Path
|
||||
|
||||
RATE = 22050
|
||||
|
||||
|
||||
def write(path: Path, samples: list[float]) -> None:
|
||||
peak = max(abs(s) for s in samples) or 1.0
|
||||
scale = 0.25 / peak # about -12 dBFS
|
||||
with wave.open(str(path), "wb") as out:
|
||||
out.setnchannels(1)
|
||||
out.setsampwidth(2)
|
||||
out.setframerate(RATE)
|
||||
out.writeframes(b"".join(struct.pack("<h", int(s * scale * 32767)) for s in samples))
|
||||
|
||||
|
||||
def singing_bowl(seconds: float = 2.6) -> list[float]:
|
||||
# Inharmonic partials of a bowl, each fading at its own pace, with a slight beat.
|
||||
partials = [(392.0, 1.0, 1.4), (393.2, 0.6, 1.5), (1062.0, 0.35, 0.8), (2010.0, 0.12, 0.45)]
|
||||
samples = []
|
||||
for n in range(int(seconds * RATE)):
|
||||
t = n / RATE
|
||||
attack = min(1.0, t / 0.04)
|
||||
value = sum(a * math.exp(-t / decay) * math.sin(2 * math.pi * f * t) for f, a, decay in partials)
|
||||
samples.append(attack * value)
|
||||
return samples
|
||||
|
||||
|
||||
def wood_tok(seconds: float = 0.12) -> list[float]:
|
||||
rng = random.Random(7)
|
||||
samples = []
|
||||
for n in range(int(seconds * RATE)):
|
||||
t = n / RATE
|
||||
body = math.exp(-t / 0.018) * (math.sin(2 * math.pi * 820 * t) + 0.4 * math.sin(2 * math.pi * 1640 * t))
|
||||
click = math.exp(-t / 0.002) * (rng.random() * 2 - 1) * 0.3
|
||||
samples.append(body + click)
|
||||
return samples
|
||||
|
||||
|
||||
def main() -> None:
|
||||
out = Path(__file__).resolve().parents[2] / "assets" / "sounds"
|
||||
out.mkdir(parents=True, exist_ok=True)
|
||||
write(out / "stretching_cue.wav", singing_bowl())
|
||||
write(out / "workout_cue.wav", wood_tok())
|
||||
for f in sorted(out.glob("*.wav")):
|
||||
print(f"{f.name}: {f.stat().st_size / 1000:.0f} kB")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user