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:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,32 @@
|
||||
# Geradores dos efeitos sonoros
|
||||
|
||||
Os `.ogg` em `packages/web/public/sounds/` para call e transmissão são
|
||||
sintetizados por estes scripts — nenhum arquivo de terceiros, nenhuma questão
|
||||
de licença num repositório público.
|
||||
|
||||
Os parâmetros não foram escolhidos a gosto: saíram da **medição** de dois sons
|
||||
de referência fornecidos pelo dono da instância (análise de envelope, picos
|
||||
espectrais e tempo de decaimento). O timbre resultante é fundamental + oitava
|
||||
quase na mesma intensidade, com pouco acima disso:
|
||||
|
||||
| Harmônico | Proporção medida |
|
||||
|---|---|
|
||||
| 1x (fundamental) | 1.00 |
|
||||
| 2x (oitava) | 0.85 |
|
||||
| 3x | 0.10 |
|
||||
| 4x | 0.02 |
|
||||
|
||||
Decaimento a 1/e: ~0,19 s. Sem reverberação.
|
||||
|
||||
| Script | Gera | Notas |
|
||||
|---|---|---|
|
||||
| `build.py` | `user_join`, `user_leave` | C4→G4 subindo / D4→G3 descendo |
|
||||
| `stream.py` | `stream_user_joined`, `stream_user_left` | nota única (G4 / G3), ~55% do volume |
|
||||
| `started4.py` | `stream_started` | acorde de dó + golpe grave + sopro de ar |
|
||||
| `ended.py` | `stream_ended` | arpejo descendo, resolve na raiz grave |
|
||||
|
||||
Para regerar: `python3 <script>.py` produz `.wav`, depois converter com
|
||||
`ffmpeg -i X.wav -c:a libvorbis -q:a 5 -ar 48000 X.ogg`.
|
||||
|
||||
Ajustes comuns: `HARM` muda o timbre, `tau` a duração, `step` a velocidade do
|
||||
arpejo, e o alvo em `write()` o volume (pico RMS).
|
||||
@@ -0,0 +1,47 @@
|
||||
import math, wave, struct
|
||||
SR=48000
|
||||
# Proporcoes medidas nas referencias: fundamental, oitava, 3o, 4o
|
||||
HARM=(1.00, 0.85, 0.10, 0.02)
|
||||
TAU=0.19 # decaimento a 1/e medido
|
||||
ATTACK=0.004
|
||||
|
||||
def note(f, dur):
|
||||
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*min(1.0,t/ATTACK)*math.exp(-t/TAU))
|
||||
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)):
|
||||
best=0.0
|
||||
for i in range(0,len(x)-win,win):
|
||||
seg=x[i:i+win]
|
||||
best=max(best, math.sqrt(sum(v*v for v in seg)/len(seg)))
|
||||
return best
|
||||
|
||||
def write(name, buf, target_rms):
|
||||
cur=peak_rms(buf)
|
||||
g=target_rms/max(1e-9,cur)
|
||||
# nao deixa estourar mesmo casando o volume da referencia
|
||||
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")
|
||||
|
||||
C4,D4,G3,G4 = 261.63, 293.66, 196.00, 392.00
|
||||
GAP=int(SR*0.10) # 100ms entre as notas, medido
|
||||
|
||||
# join: quinta subindo — mesmas notas da referencia
|
||||
write('user_join.wav', mix([(0,note(C4,0.55)),(GAP,note(G4,0.55))], int(SR*0.62)), 0.365)
|
||||
# leave: quinta descendo
|
||||
write('user_leave.wav', mix([(0,note(D4,0.55)),(GAP,note(G3,0.55))], int(SR*0.62)), 0.440)
|
||||
@@ -0,0 +1,53 @@
|
||||
import math, wave, struct
|
||||
SR=48000
|
||||
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):
|
||||
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 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,C4,E4,G4,C5 = 130.81,261.63,329.63,392.00,523.25
|
||||
step=int(SR*0.033)
|
||||
|
||||
# Espelho do inicio: o arpejo DESCE e resolve na raiz grave, que entra por
|
||||
# ultimo. Sem o sopro de ar — brilho sugere chegada, nao encerramento.
|
||||
# Mais curto e mais baixo: encerrar merece menos destaque que comecar.
|
||||
write('stream_ended.wav', mix([
|
||||
(0, note(C5, 0.42, 0.15, 0.70)),
|
||||
(step, note(G4, 0.44, 0.16)),
|
||||
(2*step, note(E4, 0.46, 0.17)),
|
||||
(3*step, note(C4, 0.48, 0.19)),
|
||||
(3*step, note(C3, 0.50, 0.21, 0.80)),
|
||||
(3*step, thump(85, 0.13, 0.40)),
|
||||
], int(SR*0.55)), 0.34)
|
||||
@@ -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)
|
||||
@@ -0,0 +1,31 @@
|
||||
import math, wave, struct
|
||||
SR=48000
|
||||
HARM=(1.00, 0.85, 0.10, 0.02) # mesmo timbre medido nas referencias
|
||||
ATTACK=0.004
|
||||
|
||||
def note(f, dur, tau):
|
||||
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*min(1.0,t/ATTACK)*math.exp(-t/tau))
|
||||
return o
|
||||
|
||||
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")
|
||||
|
||||
G3, G4 = 196.00, 392.00
|
||||
# Decaimento mais curto (0.13 vs 0.19) e volume ~55% do som de call:
|
||||
# entrar/sair da tela dispara muitas vezes durante uma transmissao.
|
||||
write('stream_user_joined.wav', note(G4, 0.42, 0.13), 0.20)
|
||||
write('stream_user_left.wav', note(G3, 0.42, 0.13), 0.20)
|
||||
Reference in New Issue
Block a user