Files
peerspeak/assets/sounds/generate_chimes.py
T
molluskandClaude Opus 4.8 673b72d9ba feat: notification chimes for room join / peer join / peer leave (phase 1)
Adds audible notifications for the basic membership events: a bright ascending
triad when you join a room, a soft two-note rise when another participant
joins, and a two-note fall when one leaves. Hooked into the app's UiEvent
handling (RoomJoined / PeerJoined / PeerLeft) so each is played locally by
whoever observes the event.

The chimes are short mono 16-bit WAVs generated by a committed stdlib-only
Python script (assets/sounds/generate_chimes.py) and embedded in the binary
with include_bytes!, so a deployed single binary is self-contained. The new
notify module materializes each to a temp file once, then plays it
fire-and-forget via pw-play (PipeWire-native; falls back to paplay/aplay) on a
detached thread that waits on the child -- never blocks the UI, never leaves a
zombie, and silently no-ops if no player is available.

Phase 1 of a larger plan; always-on for now. Later phases: reconnect-attempt /
reconnected sounds, a settings enable/disable toggle, per-event custom sounds,
and extra events (self-leave, mute/unmute, connection-lost).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 00:47:38 -04:00

80 lines
2.5 KiB
Python

#!/usr/bin/env python3
"""Generate PeerSpeak's default notification chimes as small mono 16-bit WAVs.
Stdlib only (wave/struct/math) so there's no build-time dependency. Re-run to
regenerate the committed assets:
python3 assets/sounds/generate_chimes.py
Each chime is a short sequence of sine notes with a soft attack (no click) and
an exponential decay (a gentle bell-like tail), plus a quiet second harmonic so
it reads as a "chime" rather than a flat beep. Distinct melodic shapes make the
events recognizable without looking: ascending = arrival, descending = leaving.
"""
import math
import os
import struct
import wave
RATE = 48000 # match PeerSpeak's audio graph
AMPLITUDE = 0.38 # clearly audible, not harsh (fraction of full scale)
ATTACK_S = 0.006 # short fade-in to kill the start click
DECAY = 7.0 # exponential decay rate (higher = shorter tail)
# Note frequencies (Hz)
G4, C5, E5, G5 = 392.00, 523.25, 659.25, 783.99
def note(freq, dur):
"""One enveloped note: fundamental + soft 2nd harmonic, attack + decay."""
n = int(RATE * dur)
out = []
for i in range(n):
t = i / RATE
env = math.exp(-DECAY * t)
if t < ATTACK_S:
env *= t / ATTACK_S
sample = math.sin(2 * math.pi * freq * t) + 0.3 * math.sin(2 * math.pi * 2 * freq * t)
out.append(AMPLITUDE * env * sample / 1.3)
return out
def chime(notes):
"""Concatenate (freq, dur) notes into one sample buffer."""
buf = []
for freq, dur in notes:
buf.extend(note(freq, dur))
return buf
def write_wav(path, samples):
with wave.open(path, "w") as w:
w.setnchannels(1)
w.setsampwidth(2)
w.setframerate(RATE)
frames = b"".join(
struct.pack("<h", max(-32767, min(32767, int(s * 32767)))) for s in samples
)
w.writeframes(frames)
print(f"wrote {path} ({len(samples) / RATE * 1000:.0f} ms)")
CHIMES = {
# You joined a room: bright ascending triad — welcoming.
"self-join.wav": [(C5, 0.11), (E5, 0.11), (G5, 0.30)],
# Someone else joined: gentle two-note rise — softer than your own join.
"peer-join.wav": [(G4, 0.13), (C5, 0.28)],
# Someone left: two-note fall — the mirror of an arrival.
"peer-leave.wav": [(C5, 0.13), (G4, 0.28)],
}
def main():
here = os.path.dirname(os.path.abspath(__file__))
for name, notes in CHIMES.items():
write_wav(os.path.join(here, name), chime(notes))
if __name__ == "__main__":
main()