feat(sounds): replace call and stream effects
OpenSSF Scorecard / Scorecard analysis (push) Waiting to run
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Waiting to run
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
The originals were all 1.14s — long enough to be intrusive for events that fire whenever anyone joins, leaves, or watches a stream. Synthesised rather than sourced: no third-party file, so no licensing question in a public repository. The parameters are measured, not guessed — envelope, spectral peaks and decay taken from two reference sounds the instance owner supplied. The timbre is fundamental plus octave at near-equal strength (1.00 / 0.85 / 0.10 / 0.02) decaying to 1/e in 0.19s, with no reverb. Call join rises C4→G4, leave falls D4→G3, matching the references' intervals and their 100ms spacing. The stream pair reuses those resolution notes as single tones at ~55% the volume: they fire far more often during a broadcast, so they have to sit under the call sounds rather than beside them. Stream start is the one event that happens once per broadcast, so it can be a chord — with a low thump and a short air layer that both die inside 150ms, adding weight without length. Stream end mirrors it descending, quieter, and without the air, since brightness reads as arrival. Generators and the measured parameters are kept in tools/sfx so these can be retuned without redoing the analysis. Total size drops from 150KB to 41KB.
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
import math, random, wave, struct
|
||||
SR=48000; random.seed(11)
|
||||
HARM=(1.00, 0.85, 0.10, 0.02)
|
||||
|
||||
def note(f, dur, tau, amp=1.0):
|
||||
n=int(SR*dur); o=[]
|
||||
for i in range(n):
|
||||
t=i/SR
|
||||
s=sum(a*math.sin(2*math.pi*f*(k+1)*t) for k,a in enumerate(HARM))
|
||||
o.append(s*amp*min(1.0,t/0.004)*math.exp(-t/tau))
|
||||
return o
|
||||
|
||||
def thump(f, dur, amp):
|
||||
"""Instrumento 2: golpe grave com queda de tom. Da impacto no ataque e
|
||||
morre em 90ms — engorda sem alongar."""
|
||||
n=int(SR*dur); o=[]; ph=0.0
|
||||
for i in range(n):
|
||||
t=i/SR
|
||||
ph += 2*math.pi*(f*(1+1.2*math.exp(-t/0.02)))/SR
|
||||
o.append(math.sin(ph)*amp*math.exp(-t/(dur*0.30)))
|
||||
return o
|
||||
|
||||
def air(dur, amp, cutoff=5000):
|
||||
"""Instrumento 3: sopro curto de ruido filtrado. Brilho no inicio, sem cauda."""
|
||||
n=int(SR*dur)
|
||||
y=0.0; a=1-math.exp(-2*math.pi*cutoff/SR); o=[]
|
||||
for i in range(n):
|
||||
t=i/SR
|
||||
y += a*(random.uniform(-1,1)-y)
|
||||
o.append(y*amp*min(1.0,t/0.008)*math.exp(-t/(dur*0.22)))
|
||||
return o
|
||||
|
||||
def mix(layers,total):
|
||||
b=[0.0]*total
|
||||
for off,s in layers:
|
||||
for i,v in enumerate(s):
|
||||
if off+i<total: b[off+i]+=v
|
||||
return b
|
||||
|
||||
def peak_rms(x, win=int(SR*0.01)):
|
||||
return max(math.sqrt(sum(v*v for v in x[i:i+win])/win) for i in range(0,len(x)-win,win))
|
||||
|
||||
def write(name, buf, target):
|
||||
g=target/max(1e-9,peak_rms(buf))
|
||||
pk=max(abs(s*g) for s in buf)
|
||||
if pk>0.97: g*=0.97/pk
|
||||
with wave.open(name,'w') as w:
|
||||
w.setnchannels(1); w.setsampwidth(2); w.setframerate(SR)
|
||||
w.writeframes(b''.join(struct.pack('<h',int(max(-1,min(1,s*g))*32767)) for s in buf))
|
||||
print(f"{name}: pico RMS {peak_rms([s*g for s in buf]):.3f}, {len(buf)/SR:.2f}s")
|
||||
|
||||
C3,G3,C4,E4,G4,C5 = 130.81,196.00,261.63,329.63,392.00,523.25
|
||||
step=int(SR*0.033) # arpejo bem rapido: acorde montado em 100ms
|
||||
|
||||
# Decaimentos ~metade dos anteriores (0.20 vs 0.38). Total 0.62s vs 1.08s.
|
||||
write('stream_started.wav', mix([
|
||||
(0, thump(85, 0.14, 0.55)), # impacto
|
||||
(0, air(0.13, 0.16)), # brilho
|
||||
(0, note(C3, 0.60, 0.22, 0.80)),
|
||||
(0, note(G3, 0.58, 0.20, 0.45)),
|
||||
(0, note(C4, 0.56, 0.20)),
|
||||
(step, note(E4, 0.54, 0.19)),
|
||||
(2*step, note(G4, 0.52, 0.18)),
|
||||
(3*step, note(C5, 0.50, 0.16, 0.70)),
|
||||
], int(SR*0.62)), 0.42)
|
||||
Reference in New Issue
Block a user