Files
CedricandClaude Opus 5.5 971ff64117 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>
2026-10-29 00:20:16 +01:00

62 lines
2.0 KiB
Python

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