Audible chimes for reconnect/reconnected/reconnect-failed, self-leave,
peer join/leave, and mic/deafen toggle, completing the 4-phase
notification plan (phase 1 room/peer join+leave shipped in 673b72d).
- Per-event custom WAV overrides in Settings, with ~ expansion and live
file-found/not-found validation; persisted on settings exit. Each event
falls back to its embedded default chime when no custom path is set.
- Reconnect-attempt chime is edge-triggered: fires once per disconnect,
not once per redial attempt.
- Global "enable sound notifications" toggle.
90 lines
3.0 KiB
Python
90 lines
3.0 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)],
|
|
# Reconnecting to peer: soft single Bb4 note.
|
|
"reconnect-attempt.wav": [(466.16, 0.20)],
|
|
# Reconnected to peer: bright two-note rise (C5 -> G5).
|
|
"reconnected.wav": [(C5, 0.10), (G5, 0.25)],
|
|
# Left a room: descending triad.
|
|
"self-leave.wav": [(G5, 0.10), (E5, 0.10), (C5, 0.25)],
|
|
# Mute/unmute toggle: tiny clean blip.
|
|
"mic-toggle.wav": [(E5, 0.08)],
|
|
# Reconnect gave up: disappointing low two-note fall.
|
|
"reconnect-failed.wav": [(C5, 0.15), (349.23, 0.30)],
|
|
}
|
|
|
|
|
|
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()
|