primocode 10.16.0 → 10.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. package/LICENSES/edit-ia-pro.txt +32 -0
  2. package/LICENSES/fontes-OFL.txt +476 -0
  3. package/LICENSES/hyperframes-Apache-2.0.txt +205 -0
  4. package/LICENSES/permanent-marker-Apache-2.0.txt +204 -0
  5. package/LICENSES/selfie-multiclass-Apache-2.0.txt +227 -0
  6. package/LICENSES/three-MIT.txt +23 -0
  7. package/LICENSES/video-use-MIT.txt +24 -0
  8. package/LICENSES/woven-sfx-CC0.txt +16 -0
  9. package/LICENSES/yunet-MIT.txt +21 -0
  10. package/README.md +2 -2
  11. package/bin/primocode.js +28 -8
  12. package/editor/LICENSE.md +11 -0
  13. package/editor/README.md +321 -0
  14. package/editor/SKILL.md +307 -0
  15. package/editor/THIRD_PARTY_NOTICES.md +228 -0
  16. package/editor/assets/baixar.json +591 -0
  17. package/editor/assets/fonts/anton/OFL.txt +93 -0
  18. package/editor/assets/fonts/caveat/OFL.txt +93 -0
  19. package/editor/assets/fonts/figtree/OFL.txt +93 -0
  20. package/editor/assets/fonts/fonts.json +10 -0
  21. package/editor/assets/fonts/montserrat/OFL.txt +93 -0
  22. package/editor/assets/fonts/permanentmarker/LICENSE.txt +202 -0
  23. package/editor/assets/models/SELFIE_MULTICLASS_LICENSE.txt +227 -0
  24. package/editor/assets/models/YUNET_LICENSE.txt +21 -0
  25. package/editor/assets/sfx/licencas/bass_drop.txt +32 -0
  26. package/editor/assets/sfx/licencas/cash.txt +76 -0
  27. package/editor/assets/sfx/licencas/charge.txt +31 -0
  28. package/editor/assets/sfx/licencas/click.txt +46 -0
  29. package/editor/assets/sfx/licencas/coin.txt +50 -0
  30. package/editor/assets/sfx/licencas/count.txt +47 -0
  31. package/editor/assets/sfx/licencas/data.txt +31 -0
  32. package/editor/assets/sfx/licencas/error.txt +32 -0
  33. package/editor/assets/sfx/licencas/flash.txt +31 -0
  34. package/editor/assets/sfx/licencas/gears.txt +82 -0
  35. package/editor/assets/sfx/licencas/keyboard.txt +48 -0
  36. package/editor/assets/sfx/licencas/metal_hit.txt +62 -0
  37. package/editor/assets/sfx/licencas/metal_riser.txt +61 -0
  38. package/editor/assets/sfx/licencas/mouse_click.txt +50 -0
  39. package/editor/assets/sfx/licencas/notification.txt +54 -0
  40. package/editor/assets/sfx/licencas/pop.txt +38 -0
  41. package/editor/assets/sfx/licencas/rewind.txt +32 -0
  42. package/editor/assets/sfx/licencas/rise.txt +46 -0
  43. package/editor/assets/sfx/licencas/sinteticos.txt +21 -0
  44. package/editor/assets/sfx/licencas/snap.txt +50 -0
  45. package/editor/assets/sfx/licencas/success.txt +40 -0
  46. package/editor/assets/sfx/licencas/synth_hit.txt +32 -0
  47. package/editor/assets/sfx/licencas/tick.txt +14 -0
  48. package/editor/assets/sfx/licencas/whoosh.txt +47 -0
  49. package/editor/assets/sfx/licencas/wind.txt +31 -0
  50. package/editor/assets/sfx/sfx.json +832 -0
  51. package/editor/assets/sfx/sfx_sinteticos.json +317 -0
  52. package/editor/assets/sfx/sfx_woven.json +387 -0
  53. package/editor/assets/sfx/woven/LICENCA.md +16 -0
  54. package/editor/fichas/briefing-do-video.md +45 -0
  55. package/editor/fichas/conferencia-da-amostra.md +56 -0
  56. package/editor/fichas/matriz-de-ganchos.csv +3 -0
  57. package/editor/fichas/padrao.json +16 -0
  58. package/editor/fichas/planilha-de-cenas.csv +4 -0
  59. package/editor/fichas/planilha-de-versoes.csv +4 -0
  60. package/editor/fichas/plano-em-blocos.md +32 -0
  61. package/editor/fichas/registro-de-materiais.json +27 -0
  62. package/editor/helpers/assets_ocr/ocr_vision.swift +41 -0
  63. package/editor/helpers/bordas_precisas.py +272 -0
  64. package/editor/helpers/camera_viva.py +426 -0
  65. package/editor/helpers/captions.py +601 -0
  66. package/editor/helpers/common.py +308 -0
  67. package/editor/helpers/compose_final.py +3555 -0
  68. package/editor/helpers/conferir_corte.py +234 -0
  69. package/editor/helpers/conferir_dados.py +405 -0
  70. package/editor/helpers/contact_sheet.py +282 -0
  71. package/editor/helpers/corte_por_onda.py +433 -0
  72. package/editor/helpers/corte_seco.py +595 -0
  73. package/editor/helpers/denoise.py +971 -0
  74. package/editor/helpers/efeitos.py +408 -0
  75. package/editor/helpers/efeitos_gerar.py +159 -0
  76. package/editor/helpers/elevenlabs_api.py +442 -0
  77. package/editor/helpers/face_layout.py +570 -0
  78. package/editor/helpers/fetch_images.py +423 -0
  79. package/editor/helpers/ganho_por_trecho.py +214 -0
  80. package/editor/helpers/gen_image.py +312 -0
  81. package/editor/helpers/gen_music.py +316 -0
  82. package/editor/helpers/gen_sfx.py +148 -0
  83. package/editor/helpers/grade.py +307 -0
  84. package/editor/helpers/icloud.py +119 -0
  85. package/editor/helpers/make_sfx.py +940 -0
  86. package/editor/helpers/matte.py +1342 -0
  87. package/editor/helpers/medir_referencia.py +2232 -0
  88. package/editor/helpers/mixar_trilha.py +354 -0
  89. package/editor/helpers/montar_corte.py +295 -0
  90. package/editor/helpers/motion/__init__.py +13 -0
  91. package/editor/helpers/motion/engine.py +1915 -0
  92. package/editor/helpers/motion/registry.py +510 -0
  93. package/editor/helpers/motion/render.py +665 -0
  94. package/editor/helpers/motion/templates/barra_progresso.py +370 -0
  95. package/editor/helpers/motion/templates/carimbo.py +132 -0
  96. package/editor/helpers/motion/templates/checklist.py +271 -0
  97. package/editor/helpers/motion/templates/comentario_cta.py +380 -0
  98. package/editor/helpers/motion/templates/contador.py +222 -0
  99. package/editor/helpers/motion/templates/destaque.py +263 -0
  100. package/editor/helpers/motion/templates/etiqueta_preco.py +461 -0
  101. package/editor/helpers/motion/templates/grafico_barras.py +510 -0
  102. package/editor/helpers/motion/templates/grafico_linha.py +462 -0
  103. package/editor/helpers/motion/templates/notificacao.py +266 -0
  104. package/editor/helpers/motion/templates/palavra_impacto.py +410 -0
  105. package/editor/helpers/motion/templates/passos.py +321 -0
  106. package/editor/helpers/motion/templates/riscado.py +298 -0
  107. package/editor/helpers/motion/templates/selo_garantia.py +150 -0
  108. package/editor/helpers/motion_render.py +241 -0
  109. package/editor/helpers/motion_sugerir.py +961 -0
  110. package/editor/helpers/noise_check.py +134 -0
  111. package/editor/helpers/pack_transcripts.py +223 -0
  112. package/editor/helpers/palavras_na_onda.py +176 -0
  113. package/editor/helpers/plano_sfx.py +839 -0
  114. package/editor/helpers/portao.py +427 -0
  115. package/editor/helpers/preview_server.py +1177 -0
  116. package/editor/helpers/rastrear_rosto.py +298 -0
  117. package/editor/helpers/render_cut.py +1256 -0
  118. package/editor/helpers/respiros.py +89 -0
  119. package/editor/helpers/sincronizar_cameras.py +571 -0
  120. package/editor/helpers/speech_regions.py +423 -0
  121. package/editor/helpers/style_catalog.json +231 -0
  122. package/editor/helpers/style_samples.py +246 -0
  123. package/editor/helpers/tela_verde.py +2113 -0
  124. package/editor/helpers/text_render.py +2059 -0
  125. package/editor/helpers/texto_na_tela.py +366 -0
  126. package/editor/helpers/timeline_view.py +292 -0
  127. package/editor/helpers/tira_de_quadros.py +113 -0
  128. package/editor/helpers/transcribe.py +734 -0
  129. package/editor/helpers/transcribe_batch.py +129 -0
  130. package/editor/helpers/trilhas.py +1146 -0
  131. package/editor/helpers/trilhas_humores.json +66 -0
  132. package/editor/helpers/valores.py +187 -0
  133. package/editor/helpers/verify_cut.py +820 -0
  134. package/editor/helpers/voice_levels.py +233 -0
  135. package/editor/helpers/voz_check.py +386 -0
  136. package/editor/helpers/voz_limpa.py +81 -0
  137. package/editor/helpers/watch_preview.py +372 -0
  138. package/editor/modelo-projeto/LEIA.md +402 -0
  139. package/editor/modelo-projeto/assets/fontes/OFL-Caveat.txt +93 -0
  140. package/editor/modelo-projeto/assets/fontes/OFL-JetBrainsMono.txt +93 -0
  141. package/editor/modelo-projeto/assets/fontes/OFL-Montserrat.txt +93 -0
  142. package/editor/modelo-projeto/assets/fontes/larguras.json +1 -0
  143. package/editor/modelo-projeto/assets/three/BufferGeometryUtils.js +1438 -0
  144. package/editor/modelo-projeto/assets/three/GLTFLoader.js +4895 -0
  145. package/editor/modelo-projeto/assets/three/LICENSE +21 -0
  146. package/editor/modelo-projeto/assets/three/ORIGEM.txt +17 -0
  147. package/editor/modelo-projeto/assets/three/RoomEnvironment.js +185 -0
  148. package/editor/modelo-projeto/assets/three/three.core.min.js +6 -0
  149. package/editor/modelo-projeto/assets/three/three.module.min.js +6 -0
  150. package/editor/modelo-projeto/build.py +4828 -0
  151. package/editor/modelo-projeto/compositions/cenas/2d/k2d_antes_depois.html +199 -0
  152. package/editor/modelo-projeto/compositions/cenas/2d/k2d_arquivo.html +232 -0
  153. package/editor/modelo-projeto/compositions/cenas/2d/k2d_busca.html +315 -0
  154. package/editor/modelo-projeto/compositions/cenas/2d/k2d_comando.html +304 -0
  155. package/editor/modelo-projeto/compositions/cenas/2d/k2d_conversa.html +217 -0
  156. package/editor/modelo-projeto/compositions/cenas/2d/k2d_esquema.html +289 -0
  157. package/editor/modelo-projeto/compositions/cenas/2d/k2d_medidor.html +249 -0
  158. package/editor/modelo-projeto/compositions/cenas/2d/k2d_notas.html +233 -0
  159. package/editor/modelo-projeto/compositions/cenas/3d/k3d_caminho.html +197 -0
  160. package/editor/modelo-projeto/compositions/cenas/3d/k3d_explodida.html +152 -0
  161. package/editor/modelo-projeto/compositions/cenas/3d/k3d_tela.html +167 -0
  162. package/editor/modelo-projeto/compositions/cenas/3d/k3d_volume.html +124 -0
  163. package/editor/modelo-projeto/compositions/cenas/3d/motor-three.js +728 -0
  164. package/editor/modelo-projeto/compositions/cenas/LEIA.md +177 -0
  165. package/editor/modelo-projeto/ferramentas/medir_fontes.py +65 -0
  166. package/editor/modelo-projeto/ferramentas/pessoa_recortada.py +176 -0
  167. package/editor/modelo-projeto/ferramentas/rastrear_maos.py +189 -0
  168. package/editor/modelo-projeto/hyperframes.json +12 -0
  169. package/editor/modelo-projeto/package.json +11 -0
  170. package/editor/modelo-projeto/projeto.json +16 -0
  171. package/editor/preview/app.css +1009 -0
  172. package/editor/preview/app.js +2122 -0
  173. package/editor/preview/index.html +307 -0
  174. package/editor/pyproject.toml +23 -0
  175. package/editor/references/3d.md +236 -0
  176. package/editor/references/animacao-catalogo.md +672 -0
  177. package/editor/references/animacao.md +498 -0
  178. package/editor/references/comandos.md +92 -0
  179. package/editor/references/corte-seco-como-funciona.md +50 -0
  180. package/editor/references/corte-seco.md +88 -0
  181. package/editor/references/corte.md +290 -0
  182. package/editor/references/direcao-por-blocos.md +218 -0
  183. package/editor/references/enquadramento.md +654 -0
  184. package/editor/references/estilos-prontos.md +355 -0
  185. package/editor/references/estudio.md +100 -0
  186. package/editor/references/exemplo-eventos.json +67 -0
  187. package/editor/references/lettering.md +252 -0
  188. package/editor/references/modelos.md +494 -0
  189. package/editor/references/phase2.md +1007 -0
  190. package/editor/references/referencia-aplicar.md +203 -0
  191. package/editor/references/referencia-medir.md +375 -0
  192. package/editor/references/revisao.md +335 -0
  193. package/editor/references/som-e-trilha.md +146 -0
  194. package/editor/references/tabela-de-sons.md +130 -0
  195. package/editor/references/variacoes.md +234 -0
  196. package/editor/uv.lock +1141 -0
  197. package/lib/act.js +36 -8
  198. package/lib/catalogo.js +4 -0
  199. package/lib/chamada-torta.js +81 -0
  200. package/lib/editor-pro.js +786 -0
  201. package/lib/entrega.js +58 -0
  202. package/lib/estudio/conferencia-visual.js +76 -0
  203. package/lib/estudio/diretor.js +6 -1
  204. package/lib/estudio/exportar.js +3 -1
  205. package/lib/estudio/gosto.js +45 -4
  206. package/lib/estudio/index.js +8 -5
  207. package/lib/estudio/kit-ae/base.jsx +119 -13
  208. package/lib/estudio/kit-ae/cenas.jsx +47 -15
  209. package/lib/estudio/kit-ae/guarda.jsx +274 -0
  210. package/lib/estudio/kit-ae/primitivas.jsx +180 -24
  211. package/lib/estudio/maquina-diretor.js +61 -7
  212. package/lib/estudio/maquina-foto.js +177 -0
  213. package/lib/estudio/maquina-guarda.js +207 -0
  214. package/lib/estudio/maquina-layout.js +22 -1
  215. package/lib/estudio/maquina.js +60 -11
  216. package/lib/estudio/referencias.js +1 -1
  217. package/lib/estudio/visao/visao.swift +97 -0
  218. package/lib/estudio/visao.js +60 -0
  219. package/lib/local/cerebro.json +192 -6
  220. package/lib/local/motor.js +5 -2
  221. package/lib/mcp.js +105 -7
  222. package/lib/nuvem.js +11 -1
  223. package/lib/outra-pasta.js +1 -1
  224. package/lib/passagem.js +50 -0
  225. package/lib/perigo.js +29 -12
  226. package/lib/protecao.js +134 -0
  227. package/lib/sandbox.js +11 -4
  228. package/lib/titulo-conversa.js +46 -0
  229. package/lib/tools.js +3 -0
  230. package/lib/ui.js +1 -1
  231. package/package.json +3 -2
  232. package/skills/editar-meu-video/SKILL.md +6 -2
  233. package/skills/editar-video/SKILL.md +113 -0
  234. package/studio/pessoa.py +52 -0
  235. package/voz/primo_voz/app.py +43 -4
  236. package/voz/primo_voz/chat.html +17 -6
  237. package/voz/primo_voz/painel_dados.py +134 -0
@@ -0,0 +1,940 @@
1
+ """Sound effects for editor-video (Contract 8): the built-in effect registry and the procedural generator.
2
+
3
+ Built-in effects (the 24 names in NAMES: whoosh, pop, click, tick, rise, cash, coin, notification, success,
4
+ error, count, keyboard, snap, mouse_click, bass_drop, metal_hit, synth_hit, flash, charge, metal_riser, wind,
5
+ rewind, gears, data) are listed in assets/sfx/sfx.json: for each name the file (relative to assets/sfx), the
6
+ anchor (`anchor_s`, seconds from the file start to the moment the sound hits full strength, which
7
+ compose_final puts on the event frame), a Portuguese `label` and `aliases` (resolve_name: "grave" ->
8
+ bass_drop, "estalo de dedo" -> snap; accents, case, spaces and hyphens do not matter), where it came from
9
+ and its measured levels. An entry points either to a recording in assets/sfx/ (the `source` field says where
10
+ it came from) or to a procedural file in assets/sfx/sinteticos/.
11
+
12
+ The procedural generator writes assets/sfx/sinteticos/<name>.wav for SYNTH_NAMES with numpy only: 48 kHz,
13
+ 16-bit stereo, deterministic (fixed random seeds), so re-running gives the same bytes. The first seven peak at
14
+ about -3 dBFS with the attack at t=0; the eight sounds added on 16/09/2026 (KIT_LEVELS: error, bass_drop,
15
+ synth_hit, flash, charge, wind, rewind, data) come out already at their kit level (kit_gain_db: 200 ms target
16
+ at sfx.volume 1.0, never above -20 LUFS on the 3 s window) and are used as they are by sfx.json.
17
+ assets/sfx/sfx_sinteticos.json is the registry of the procedural set alone (`synthetic_registry()`).
18
+
19
+ `measure()` gives the numbers stored in sfx.json (numpy only): K-weighted (BS.1770) loudness maxima over
20
+ 200 ms, 400 ms (momentary) and 3 s (short-term) windows, the sample peak and the anchor (first moment the
21
+ 50 ms K-weighted power envelope comes within 2 dB of its maximum).
22
+
23
+ Usage:
24
+ uv run python helpers/make_sfx.py [--out-dir assets/sfx/sinteticos] [--only whoosh,pop] [--peak-db -3]
25
+ uv run python helpers/make_sfx.py --measure [arquivo.wav ...] (sem arquivo: os efeitos prontos)
26
+ uv run python helpers/make_sfx.py --synthetic-registry (grava assets/sfx/sfx_sinteticos.json)
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import argparse
32
+ import json
33
+ import math
34
+ import re
35
+ import subprocess
36
+ import sys
37
+ import unicodedata
38
+ import wave
39
+ from pathlib import Path
40
+
41
+ import numpy as np
42
+
43
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
44
+ from common import SFX_DIR # noqa: E402
45
+
46
+ SR = 48000
47
+ PEAK_DB = -3.0
48
+ # every built-in effect name, in the order of assets/sfx/sfx.json (the first seven are the 15/09/2026 set)
49
+ NAMES = ("whoosh", "pop", "click", "tick", "rise", "cash", "coin",
50
+ "notification", "success", "error", "count", "keyboard", "snap", "mouse_click",
51
+ "bass_drop", "metal_hit", "synth_hit", "flash", "charge", "metal_riser", "wind", "rewind", "gears", "data")
52
+ # the procedural set: what this file generates into assets/sfx/sinteticos/
53
+ SYNTH_NAMES = ("whoosh", "pop", "click", "tick", "rise", "cash", "coin",
54
+ "error", "bass_drop", "synth_hit", "flash", "charge", "wind", "rewind", "data")
55
+ SYNTH_DIR = SFX_DIR / "sinteticos" # procedural files (make_sfx output)
56
+ MANIFEST = SFX_DIR / "sfx.json" # built-in name -> file, anchor_s, source, levels
57
+ SYNTH_REGISTRY = SFX_DIR / "sfx_sinteticos.json" # the same for the procedural set only (the package's sfx.json)
58
+ SYNTH_SOUNDS = { # PT-BR description of each procedural sound (duration added)
59
+ "whoosh": "vento rápido, sintético",
60
+ "pop": "bolha estourando, sintético",
61
+ "click": "clique curto, sintético",
62
+ "tick": "toque seco e curto de madeira",
63
+ "rise": "subida curta que cresce até o topo, sintética",
64
+ "cash": "caixa registradora: estalo mecânico e logo depois o sino, o ching",
65
+ "coin": "uma moeda tilintando, sintética",
66
+ "error": "buzina de erro: uma nota curta e outra mais grave e mais longa",
67
+ "bass_drop": "grave que cai: um tom de 150 Hz descendo até 26 Hz",
68
+ "synth_hit": "batida sintética: um zap descendo que termina num grave 808",
69
+ "flash": "flash: ar subindo, estouro brilhante com baque grave e um brilho que apaga",
70
+ "charge": "carga de energia: estalo elétrico e um apito que sobe e some",
71
+ "wind": "vento suave que passa da esquerda para a direita",
72
+ "rewind": "fita rebobinando: som embolado que acelera e para de repente",
73
+ "data": "dados de computador: bipes agudos e rápidos",
74
+ }
75
+ SFX_CALIBRATION = 1.4 # linear gain of an effect at sfx.volume 1.0 over a voice at the target loudness (compose_final)
76
+ ANCHOR_WINDOW_S = 0.05 # anchor: 50 ms K-weighted power envelope ...
77
+ ANCHOR_DROP_DB = 2.0 # ... first reaches 2 dB under its maximum
78
+ # Level rule of the kit, at sfx.volume 1.0 over a -14 LUFS voice: short UI accents -14 LUFS on the loudest 200 ms
79
+ # window, a soft whoosh -12, impacts -10, and never above -20 LUFS on the 3 s window (long sounds, like the risers,
80
+ # are set by that window alone: 6 dB under the voice).
81
+ LONG_CAP_LUFS = -20.0
82
+ KIT_PEAK_MAX_DB = -1.0
83
+ # the procedural sounds of 16/09/2026 come out of the generator already at their kit level (200 ms target or None)
84
+ KIT_LEVELS = {"error": -14.0, "bass_drop": -10.0, "synth_hit": -10.0, "flash": -10.0, "charge": None,
85
+ "wind": -12.0, "rewind": None, "data": None}
86
+
87
+
88
+ # ---------------------------------------------------------------- building blocks
89
+
90
+ def _t(dur: float) -> np.ndarray:
91
+ return np.arange(int(round(dur * SR))) / SR
92
+
93
+
94
+ def _attack_env(n: int, attack_s: float, decay_s: float, hold_s: float = 0.0) -> np.ndarray:
95
+ """Fast linear attack from 0, optional hold, exponential decay (time constant decay_s)."""
96
+ t = np.arange(n) / SR
97
+ env = np.ones(n)
98
+ a = max(1, int(attack_s * SR))
99
+ env[:a] = np.linspace(0.0, 1.0, a, endpoint=False) ** 0.7
100
+ tail = t - attack_s - hold_s
101
+ env = np.where(tail > 0, env * np.exp(-np.maximum(tail, 0) / max(decay_s, 1e-4)), env)
102
+ return env
103
+
104
+
105
+ def _fade_tail(x: np.ndarray, ms: float = 12.0) -> np.ndarray:
106
+ n = min(len(x), int(SR * ms / 1000))
107
+ if n > 1:
108
+ x = x.copy()
109
+ x[-n:] *= np.linspace(1.0, 0.0, n) ** 2
110
+ return x
111
+
112
+
113
+ def _svf_bandpass(x: np.ndarray, cutoff: np.ndarray, q: float) -> np.ndarray:
114
+ """Chamberlin state-variable band-pass with a per-sample cutoff (Hz)."""
115
+ y = np.zeros_like(x)
116
+ low = band = 0.0
117
+ damp = 1.0 / q
118
+ f = 2.0 * np.sin(np.pi * np.clip(cutoff, 20.0, SR / 6.5) / SR)
119
+ for i in range(len(x)):
120
+ high = x[i] - low - damp * band
121
+ band += f[i] * high
122
+ low += f[i] * band
123
+ y[i] = band
124
+ return y
125
+
126
+
127
+ def _one_pole_lowpass(x: np.ndarray, hz: float) -> np.ndarray:
128
+ a = math.exp(-2.0 * math.pi * hz / SR)
129
+ y = np.zeros_like(x)
130
+ acc = 0.0
131
+ for i in range(len(x)):
132
+ acc = (1 - a) * x[i] + a * acc
133
+ y[i] = acc
134
+ return y
135
+
136
+
137
+ def _highpass(x: np.ndarray, hz: float) -> np.ndarray:
138
+ return x - _one_pole_lowpass(x, hz)
139
+
140
+
141
+ def _normalize(stereo: np.ndarray, peak_db: float) -> np.ndarray:
142
+ peak = float(np.max(np.abs(stereo))) or 1.0
143
+ return stereo * (10 ** (peak_db / 20.0) / peak)
144
+
145
+
146
+ # ---------------------------------------------------------------- sounds
147
+
148
+ def whoosh() -> np.ndarray:
149
+ """Airy swish: noise through a band-pass sweeping down, quick attack, 0.5 s tail."""
150
+ rng = np.random.default_rng(1207)
151
+ dur = 0.55
152
+ n = int(dur * SR)
153
+ t = np.arange(n) / SR
154
+ p = t / dur
155
+ sweep = 5200.0 * (380.0 / 5200.0) ** (p ** 0.6)
156
+ chans = []
157
+ for side, detune in ((0, 1.0), (1, 1.07)):
158
+ noise = rng.standard_normal(n)
159
+ band = _svf_bandpass(noise, sweep * detune, q=1.6)
160
+ air = _highpass(rng.standard_normal(n), 3000.0) * 0.12
161
+ env = _attack_env(n, 0.006, 0.16, hold_s=0.02)
162
+ chans.append((band + air) * env)
163
+ x = np.stack(chans)
164
+ return np.stack([_fade_tail(c, 30) for c in x])
165
+
166
+
167
+ def pop() -> np.ndarray:
168
+ """Bubble pop: sine with a fast pitch drop plus a tiny click on top."""
169
+ rng = np.random.default_rng(88)
170
+ dur = 0.16
171
+ t = _t(dur)
172
+ n = len(t)
173
+ freq = 240.0 + 820.0 * np.exp(-t / 0.018)
174
+ phase = 2 * np.pi * np.cumsum(freq) / SR
175
+ body = np.sin(phase) * _attack_env(n, 0.0015, 0.045)
176
+ click = _highpass(rng.standard_normal(n), 2500.0) * _attack_env(n, 0.0005, 0.004) * 0.35
177
+ mono = _fade_tail(body + click, 20)
178
+ return np.stack([mono, mono])
179
+
180
+
181
+ def click() -> np.ndarray:
182
+ """Short UI click: bright filtered noise burst with a 2.9 kHz blip."""
183
+ rng = np.random.default_rng(5)
184
+ dur = 0.06
185
+ t = _t(dur)
186
+ n = len(t)
187
+ noise = _highpass(rng.standard_normal(n), 1800.0) * _attack_env(n, 0.0004, 0.006)
188
+ blip = np.sin(2 * np.pi * 2900.0 * t) * _attack_env(n, 0.0006, 0.012) * 0.6
189
+ low = np.sin(2 * np.pi * 180.0 * t) * _attack_env(n, 0.001, 0.01) * 0.35
190
+ mono = _fade_tail(noise + blip + low, 10)
191
+ return np.stack([mono, mono])
192
+
193
+
194
+ def tick() -> np.ndarray:
195
+ """Woody tick: two inharmonic partials with a very short decay."""
196
+ dur = 0.07
197
+ t = _t(dur)
198
+ n = len(t)
199
+ mono = (np.sin(2 * np.pi * 1750.0 * t) + 0.55 * np.sin(2 * np.pi * 3620.0 * t + 0.6)
200
+ + 0.25 * np.sin(2 * np.pi * 820.0 * t)) * _attack_env(n, 0.0008, 0.011)
201
+ mono = _fade_tail(mono, 10)
202
+ return np.stack([mono, mono])
203
+
204
+
205
+ def rise() -> np.ndarray:
206
+ """Upward swell: a pitched hit at t=0, then a tone and noise band climbing for 0.9 s."""
207
+ rng = np.random.default_rng(314)
208
+ dur = 0.9
209
+ t = _t(dur)
210
+ n = len(t)
211
+ p = t / dur
212
+ freq = 220.0 * (4.0 ** (p ** 1.4))
213
+ phase = 2 * np.pi * np.cumsum(freq) / SR
214
+ tone = (np.sin(phase) + 0.3 * np.sin(2 * phase)) * 0.5
215
+ swell = 0.35 + 0.65 * (np.sin(np.pi * np.clip(p / 0.8, 0, 1) / 2.0) ** 2)
216
+ swell *= np.where(p > 0.8, np.exp(-(p - 0.8) * 18.0), 1.0)
217
+ start = np.linspace(0.0, 1.0, int(0.003 * SR), endpoint=False)
218
+ swell[: len(start)] *= start
219
+ # the onset at t=0: a short pitched thump with a bright edge, as loud as the swell peak
220
+ hit_f = 170.0 + 330.0 * np.exp(-t / 0.03)
221
+ hit = np.sin(2 * np.pi * np.cumsum(hit_f) / SR) * _attack_env(n, 0.002, 0.07) * 1.25
222
+ chans = []
223
+ for side in (0, 1):
224
+ noise = _svf_bandpass(rng.standard_normal(n), 900.0 + 5200.0 * p ** 1.5, q=2.0) * 0.8
225
+ edge = _highpass(rng.standard_normal(n), 2200.0) * _attack_env(n, 0.001, 0.02) * 0.5
226
+ chans.append(_fade_tail((tone + noise) * swell + hit + edge, 25))
227
+ return np.stack(chans)
228
+
229
+
230
+ def _partial(t: np.ndarray, hz: float, amp: float, decay_s: float, phase: float = 0.0,
231
+ attack_s: float = 0.0012) -> np.ndarray:
232
+ """One struck metallic partial: sine with a short attack and an exponential decay."""
233
+ return amp * np.sin(2 * np.pi * hz * t + phase) * _attack_env(len(t), attack_s, decay_s)
234
+
235
+
236
+ def _at(x: np.ndarray, seconds: float, n: int) -> np.ndarray:
237
+ """Place a short sound x at `seconds` inside a silent buffer of n samples."""
238
+ out = np.zeros(n)
239
+ i = int(round(seconds * SR))
240
+ if i < n:
241
+ m = min(len(x), n - i)
242
+ out[i:i + m] = x[:m]
243
+ return out
244
+
245
+
246
+ def _clack(rng: np.random.Generator, center_hz: float, dur: float, decay_s: float,
247
+ body_hz: float, body_amp: float) -> np.ndarray:
248
+ """Mechanical clack: band-passed noise burst plus a short resonant knock."""
249
+ t = _t(dur)
250
+ n = len(t)
251
+ burst = _svf_bandpass(rng.standard_normal(n), np.full(n, center_hz), q=1.3) * _attack_env(n, 0.0003, decay_s)
252
+ grit = _highpass(rng.standard_normal(n), 3500.0) * _attack_env(n, 0.0002, decay_s * 0.4) * 0.5
253
+ knock = np.sin(2 * np.pi * body_hz * t) * _attack_env(n, 0.0005, decay_s * 2.2) * body_amp
254
+ return burst + grit + knock
255
+
256
+
257
+ CASH_CHING_AT = 0.085 # seconds from the "ka" clack to the bell strike
258
+
259
+
260
+ def cash() -> np.ndarray:
261
+ """Cash register ka-ching: drawer clack at t=0, bright bell 85 ms later, a light coin rattle."""
262
+ rng = np.random.default_rng(1971)
263
+ dur = 1.15
264
+ t = _t(dur)
265
+ n = len(t)
266
+
267
+ # "ka": two quick mechanical clacks (lever and drawer latch) over a low thump, done by ~60 ms
268
+ ka = _at(_clack(rng, 2300.0, 0.05, 0.006, 760.0, 0.35), 0.0, n)
269
+ ka += _at(_clack(rng, 1500.0, 0.035, 0.007, 520.0, 0.3) * 0.6, 0.024, n)
270
+ thump_t = _t(0.06)
271
+ thump_f = 105.0 + 120.0 * np.exp(-thump_t / 0.012)
272
+ thump = np.sin(2 * np.pi * np.cumsum(thump_f) / SR) * _attack_env(len(thump_t), 0.0008, 0.018)
273
+ ka += _at(_fade_tail(thump, 8), 0.0, n) * 0.6
274
+ # the cabinet keeps a faint ring into the bell, so the gap is not digital silence
275
+ ka += _at(np.sin(2 * np.pi * 610.0 * _t(0.12)) * _attack_env(int(0.12 * SR), 0.001, 0.03) * 0.05, 0.0, n)
276
+ ka = _fade_tail(ka[: int(0.12 * SR)], 20)
277
+ ka = np.concatenate([ka, np.zeros(n - len(ka))]) * 1.2
278
+
279
+ # "ching": inharmonic bell partials between 2 and 6 kHz; detuned pairs beat slowly, highs die first.
280
+ # Same pitches on both sides (a phone plays it in mono), only the phases differ.
281
+ bell_t = _t(dur - CASH_CHING_AT)
282
+ partials = ( # (Hz, amplitude, decay time constant in s)
283
+ (2217.0, 1.00, 0.62), (2221.5, 0.30, 0.58),
284
+ (3046.0, 0.70, 0.50),
285
+ (3981.0, 0.58, 0.42), (3987.0, 0.22, 0.40),
286
+ (4870.0, 0.44, 0.33),
287
+ (5723.0, 0.34, 0.26),
288
+ )
289
+ # coins settling in the drawer: a few tiny high clinks, quieter each time, alternating sides
290
+ clinks = []
291
+ when = CASH_CHING_AT + 0.17
292
+ for j in range(6):
293
+ when += float(rng.uniform(0.035, 0.07))
294
+ clinks.append((when, float(rng.uniform(5600.0, 7400.0)), 0.16 * (0.8 ** j)))
295
+ chans = []
296
+ for side in (0, 1):
297
+ bell = np.zeros(len(bell_t))
298
+ for k, (hz, amp, decay) in enumerate(partials):
299
+ bell += _partial(bell_t, hz, amp, decay, phase=0.7 * k + 0.5 * side * (k % 2))
300
+ strike = _highpass(rng.standard_normal(len(bell_t)), 4200.0) * _attack_env(len(bell_t), 0.0003, 0.0025) * 0.9
301
+ ching = _at(bell * 0.42 + strike, CASH_CHING_AT, n)
302
+ rattle = np.zeros(n)
303
+ ct = _t(0.05)
304
+ for j, (at_s, f1, amp) in enumerate(clinks):
305
+ clink = (_partial(ct, f1, 1.0, 0.012, attack_s=0.0004)
306
+ + _partial(ct, f1 * 1.47, 0.6, 0.008, attack_s=0.0004))
307
+ rattle += _at(clink * amp * (1.0 if (j + side) % 2 else 0.55), at_s, n)
308
+ chans.append(_fade_tail(ka + ching + rattle, 180))
309
+ return np.stack(chans)
310
+
311
+
312
+ def coin() -> np.ndarray:
313
+ """Single coin clink: a tiny strike and three high inharmonic partials, about 0.3 s."""
314
+ rng = np.random.default_rng(424)
315
+ dur = 0.32
316
+ t = _t(dur)
317
+ n = len(t)
318
+ chans = []
319
+ for side in (0, 1):
320
+ # same pitches on both sides (no beating when the phone plays it in mono), phases differ
321
+ ring = (_partial(t, 3890.0, 1.00, 0.110, phase=0.3 * side, attack_s=0.0005)
322
+ + _partial(t, 5930.0, 0.62, 0.070, phase=0.4 + 0.9 * side, attack_s=0.0005)
323
+ + _partial(t, 8610.0, 0.34, 0.040, phase=2.0 - 0.7 * side, attack_s=0.0005))
324
+ strike = _highpass(rng.standard_normal(n), 5000.0) * _attack_env(n, 0.0002, 0.0018) * 1.2
325
+ chans.append(_fade_tail(ring + strike, 60))
326
+ return np.stack(chans)
327
+
328
+
329
+ # ---------------------------------------------------------------- helpers for the 16/09/2026 sounds
330
+
331
+ def _pad_filter(x: np.ndarray, gain) -> np.ndarray:
332
+ """Zero-phase filter in the frequency domain (gain: freqs -> linear gain), padded with 0.25 s of silence."""
333
+ x = np.asarray(x, dtype=np.float64)
334
+ pad = int(0.25 * SR)
335
+ y = np.pad(x, [(0, 0)] * (x.ndim - 1) + [(pad, pad)])
336
+ n = y.shape[-1]
337
+ out = np.fft.irfft(np.fft.rfft(y, axis=-1) * gain(np.fft.rfftfreq(n, 1.0 / SR)), n=n, axis=-1)
338
+ return out[..., pad:pad + x.shape[-1]]
339
+
340
+
341
+ def _lp(x: np.ndarray, hz: float, order: int = 2) -> np.ndarray:
342
+ """Butterworth-shaped low-pass magnitude, zero phase."""
343
+ return _pad_filter(x, lambda f: 1.0 / np.sqrt(1.0 + (f / hz) ** (2 * order)))
344
+
345
+
346
+ def _hp(x: np.ndarray, hz: float, order: int = 2) -> np.ndarray:
347
+ """Butterworth-shaped high-pass magnitude, zero phase."""
348
+ return _pad_filter(x, lambda f: 1.0 / np.sqrt(1.0 + (hz / np.maximum(f, 1e-3)) ** (2 * order)))
349
+
350
+
351
+ def _bp(x: np.ndarray, lo: float, hi: float, order: int = 2) -> np.ndarray:
352
+ return _lp(_hp(x, lo, order), hi, order)
353
+
354
+
355
+ def _env_db(t: np.ndarray, points) -> np.ndarray:
356
+ """Piecewise-linear envelope in dB: points = ((seconds, dB), ...)."""
357
+ return 10 ** (np.interp(t, [p[0] for p in points], [p[1] for p in points]) / 20.0)
358
+
359
+
360
+ def _phase(freq: np.ndarray) -> np.ndarray:
361
+ return 2 * np.pi * np.cumsum(freq) / SR
362
+
363
+
364
+ def _one_pole_lowpass_var(x: np.ndarray, hz: np.ndarray) -> np.ndarray:
365
+ """One-pole low-pass with a per-sample cutoff (Hz)."""
366
+ a = np.exp(-2.0 * np.pi * np.clip(hz, 1.0, SR / 2.5) / SR)
367
+ y = np.zeros_like(x)
368
+ acc = 0.0
369
+ for i in range(len(x)):
370
+ acc = (1 - a[i]) * x[i] + a[i] * acc
371
+ y[i] = acc
372
+ return y
373
+
374
+
375
+ def _unit(x: np.ndarray) -> np.ndarray:
376
+ return x / (float(np.max(np.abs(x))) or 1.0)
377
+
378
+
379
+ def _fade_in(x: np.ndarray, ms: float) -> np.ndarray:
380
+ n = min(x.shape[-1], max(1, int(SR * ms / 1000)))
381
+ x = x.copy()
382
+ x[..., :n] *= np.linspace(0.0, 1.0, n)
383
+ return x
384
+
385
+
386
+ # ---------------------------------------------------------------- sounds added on 16/09/2026
387
+
388
+ ERROR_FIRST_GAIN, ERROR_BELL, ERROR_HP = 0.55, 1.0, 200.0
389
+
390
+
391
+ def error() -> np.ndarray:
392
+ """Denied buzz: a short note, then a longer one a fourth lower; odd harmonics pushed toward 400-900 Hz."""
393
+ dur = 0.66
394
+ t = _t(dur)
395
+
396
+ def note(f0: float, start: float, length: float, fall_db: float) -> np.ndarray:
397
+ tt = t - start
398
+ ph = 2 * np.pi * f0 * np.maximum(tt, 0.0)
399
+ wave = np.zeros_like(t)
400
+ for k in range(1, 60, 2): # band-limited square wave
401
+ if k * f0 < 9000.0:
402
+ wave += np.sin(k * ph) / k
403
+ env = (np.clip(tt / 0.006, 0, 1) * np.clip((length - tt) / 0.02, 0, 1)
404
+ * 10 ** (fall_db * np.clip(tt / length, 0, 1) / 20.0))
405
+ return np.where((tt >= 0) & (tt < length), wave * env, 0.0)
406
+
407
+ x = ERROR_FIRST_GAIN * note(139.0, 0.0, 0.15, -3.0) + note(104.0, 0.18, 0.46, -30.0)
408
+
409
+ def shape(f):
410
+ bell = 1.0 + ERROR_BELL * np.exp(-0.5 * (np.log2(np.maximum(f, 1.0) / 560.0) / 0.5) ** 2)
411
+ return bell / np.sqrt(1.0 + (ERROR_HP / np.maximum(f, 1e-3)) ** 4) / np.sqrt(1.0 + (f / 2600.0) ** 4)
412
+
413
+ mono = _fade_tail(_pad_filter(x, shape), 15)
414
+ return np.stack([mono, mono])
415
+
416
+
417
+ def bass_drop() -> np.ndarray:
418
+ """Sub drop: a sine gliding from 150 Hz to 26 Hz with light saturation (heard on phones) and a soft click."""
419
+ rng = np.random.default_rng(2604)
420
+ dur = 1.05
421
+ t = _t(dur)
422
+ freq = 26.0 + (150.0 - 26.0) * np.exp(-t / 0.075)
423
+ body = np.tanh(2.2 * np.sin(_phase(freq))) / np.tanh(2.2)
424
+ body *= _env_db(t, ((0, -60), (0.003, -3), (0.05, 0), (0.12, -5), (0.3, -7), (0.5, -14), (0.65, -15),
425
+ (0.85, -21), (1.0, -27), (1.05, -40)))
426
+ n = int(0.015 * SR)
427
+ click = _bp(rng.standard_normal(n), 400.0, 1200.0) * np.exp(-np.arange(n) / (0.004 * SR))
428
+ body[:n] += _unit(click) * 10 ** (-30 / 20)
429
+ mono = _fade_tail(_fade_in(body, 0.5), 60)
430
+ return np.stack([mono, mono])
431
+
432
+
433
+ def synth_hit() -> np.ndarray:
434
+ """Synth hit: a sawtooth zap sliding down for 0.33 s straight into a saturated 808 slide (108 -> 32 Hz)."""
435
+ rng = np.random.default_rng(808)
436
+ dur, zap_len = 1.5, 0.33
437
+ x = np.zeros((2, int(dur * SR)))
438
+ nz = int(zap_len * SR)
439
+ tz = np.arange(nz) / SR
440
+ fz = 480.0 + (850.0 - 480.0) * np.exp(-tz / 0.1)
441
+ ph = _phase(fz)
442
+ saw = np.zeros(nz)
443
+ for k in range(1, 16):
444
+ saw += np.where(k * fz < 7000.0, 1.0 / k, 0.0) * np.sin(k * ph)
445
+ saw = _unit(_lp(saw, 3000.0)) * _env_db(tz, ((0, -34), (zap_len * 0.8, -6), (zap_len - 0.01, 0), (zap_len, -40)))
446
+ saw *= 10 ** (-6 / 20)
447
+ x[0, :nz] += saw
448
+ x[1, :nz] += np.roll(saw, int(0.0007 * SR))
449
+ tb = np.arange(x.shape[1] - nz) / SR
450
+ fb = 32.0 + (108.0 - 32.0) * np.exp(-tb / 0.8)
451
+ body = np.tanh(2.2 * np.sin(_phase(fb))) / np.tanh(2.2)
452
+ body *= _env_db(tb, ((0, -60), (0.004, 0), (0.55, -3), (0.8, -18), (1.05, -45), (dur, -60)))
453
+ n = int(0.010 * SR)
454
+ click = _bp(rng.standard_normal(n), 1500.0, 6000.0) * np.exp(-np.arange(n) / (0.002 * SR))
455
+ body[:n] += _unit(click) * 10 ** (-18 / 20)
456
+ x[:, nz:] += body
457
+ return np.stack([_fade_tail(_fade_in(c, 0.5), 50) for c in x])
458
+
459
+
460
+ FLASH_HIT_AT = 0.28 # seconds from the start of the flash sound to the burst
461
+ FLASH_DRIVE = 2.5
462
+ FLASH_MIX_DB = (-5.0, -7.0, -2.0, -4.0, -10.0) # thump, swell, burst, shimmer, low tail
463
+
464
+
465
+ def flash() -> np.ndarray:
466
+ """Flash: a noise swell into a bright burst with a low thump, a shimmer that darkens and a short low tail."""
467
+ rng = np.random.default_rng(1826)
468
+ dur, pre = 1.4, FLASH_HIT_AT
469
+ t = _t(dur)
470
+ n = len(t)
471
+ th = t - pre
472
+ after = th >= 0
473
+ thp = np.maximum(th, 0.0)
474
+ f = 52.0 + 45.0 * np.exp(-thp / 0.06)
475
+ thump = np.tanh(1.5 * np.sin(_phase(f))) / np.tanh(1.5)
476
+ thump = np.where(after, thump * _env_db(th, ((0, -60), (0.004, 0), (0.05, -5), (0.11, -16), (0.16, -24),
477
+ (0.4, -34), (0.6, -60), (dur, -60))), 0.0)
478
+ swell, burst, shimmer, tail = (np.zeros((2, n)) for _ in range(4))
479
+ sweep = 350.0 * (3200.0 / 350.0) ** np.clip(t / pre, 0, 1)
480
+ swell_env = _env_db(t, ((0, -50), (pre * 0.5, -30), (pre * 0.9, -6), (pre, 0), (pre + 0.03, -70), (dur, -70)))
481
+ tail_env = np.where(after, _env_db(th, ((0, -70), (0.06, -10), (0.14, 0), (0.35, -8), (0.65, -18),
482
+ (dur - pre, -40))), 0.0)
483
+ common = rng.standard_normal((4, n))
484
+
485
+ def noise() -> np.ndarray: # partly shared between the sides, so the flash stays full in mono
486
+ return 0.77 * common[k] + 0.64 * rng.standard_normal(n)
487
+
488
+ for side in (0, 1):
489
+ k = 0
490
+ swell[side] = _svf_bandpass(noise(), sweep, q=1.2) * swell_env
491
+ k = 1
492
+ burst[side] = (_bp(noise(), 500.0, 14000.0)
493
+ * np.where(after, np.exp(-thp / 0.035) * np.clip(th / 0.002, 0, 1), 0.0))
494
+ gate = np.zeros(n)
495
+ for at in pre + np.sort(rng.exponential(0.22, 80)):
496
+ i = int(at * SR)
497
+ if i < n:
498
+ m = min(n - i, int(0.05 * SR))
499
+ gate[i:i + m] += rng.uniform(0.3, 1.0) * np.exp(-np.arange(m) / (0.008 * SR))
500
+ k = 2
501
+ grains = _hp(noise(), 1500.0) * gate
502
+ shimmer[side] = (_one_pole_lowpass_var(grains, 2500.0 + 9500.0 * np.exp(-thp / 0.25))
503
+ * np.where(after, np.exp(-thp / 0.35), 0.0))
504
+ k = 3
505
+ tail[side] = _bp(noise(), 45.0, 220.0) * tail_env
506
+ w = FLASH_MIX_DB
507
+ x = (10 ** (w[0] / 20) * _unit(thump) + 10 ** (w[1] / 20) * _unit(swell) + 10 ** (w[2] / 20) * _unit(burst)
508
+ + 10 ** (w[3] / 20) * _unit(shimmer) + 10 ** (w[4] / 20) * _unit(tail))
509
+ x = np.tanh(FLASH_DRIVE * _unit(x)) / np.tanh(FLASH_DRIVE) # soft saturation: a denser, louder hit
510
+ return np.stack([_fade_tail(_fade_in(c, 0.5), 60) for c in x])
511
+
512
+
513
+ CHARGE_OVERTONES, CHARGE_SNAP_TAU, CHARGE_SNAP_GAIN = (0.8, 0.5), 0.08, 2.5
514
+
515
+
516
+ def charge() -> np.ndarray:
517
+ """Energy charge: an electric snap, then a whine that climbs from 3.8 to 10 kHz with two overtones and fades."""
518
+ rng = np.random.default_rng(7)
519
+ dur = 2.4
520
+ t = _t(dur)
521
+ f0 = 3800.0 * (10000.0 / 3800.0) ** (t / dur)
522
+ ph = _phase(f0)
523
+ tone = np.zeros_like(t)
524
+ for k, amp in ((1, 1.0), (2, CHARGE_OVERTONES[0]), (3, CHARGE_OVERTONES[1])):
525
+ tone += amp * np.sin(k * ph) * np.clip((19000.0 - k * f0) / 2000.0, 0, 1)
526
+ tone *= np.exp(-t / 1.1) * (1 - np.exp(-t / 0.004))
527
+ burst = _bp(rng.standard_normal(len(t)), 800.0, 7000.0, 2) * np.exp(-t / CHARGE_SNAP_TAU)
528
+ mono = _fade_tail(0.55 * tone + CHARGE_SNAP_GAIN * burst, 30)
529
+ return np.stack([mono, mono])
530
+
531
+
532
+ WIND_PEAK_AT = 0.42 # seconds to the top of the wind swell
533
+ WIND_BODY, WIND_AIR = (30.0, 110.0), 0.08
534
+
535
+
536
+ def wind() -> np.ndarray:
537
+ """Soft wind pass: a deep noise swell with an airy top at its peak, moving from left to right."""
538
+ rng = np.random.default_rng(11)
539
+ dur, pk = 1.3, WIND_PEAK_AT
540
+ t = _t(dur)
541
+ n = len(t)
542
+ rise = np.clip(t / pk, 0, 1) ** 2.2
543
+ fall = np.where(t > pk, np.exp(-(t - pk) / 0.13) * 0.85 + 0.15 * np.exp(-(t - pk) / 0.5), 1.0)
544
+ env = rise * fall
545
+ noise = rng.standard_normal(n)
546
+ body = _bp(noise, WIND_BODY[0], WIND_BODY[1], 2)
547
+ cutoff = 300.0 + 5200.0 * np.exp(-((t - pk) / 0.12) ** 2)
548
+ air = _hp(_one_pole_lowpass_var(rng.standard_normal(n), cutoff), 200.0, 1)
549
+ mono = _hp(_unit(body) + WIND_AIR * _unit(air), 40.0, 4)
550
+ x = np.stack([mono, mono])
551
+ pan = np.clip((t - 0.1) / 0.7, 0, 1)
552
+ x[0] *= np.cos(pan * np.pi / 2) * 0.6 + 0.4
553
+ x[1] *= np.sin(pan * np.pi / 2) * 0.6 + 0.4
554
+ x *= env
555
+ return np.stack([_fade_tail(c, 50) for c in x])
556
+
557
+
558
+ def rewind() -> np.ndarray:
559
+ """Tape rewind: garbled audio that speeds up (pitch 80 -> 900 Hz) and gets louder, then a fast tape stop."""
560
+ rng = np.random.default_rng(1985)
561
+ dur, stop = 1.5, 0.22
562
+ body_len = dur - stop
563
+ t = _t(dur)
564
+ n = len(t)
565
+ u = np.clip(t / body_len, 0, 1)
566
+ fall = np.clip((t - body_len) / stop, 0, 1)
567
+ f0 = 80.0 * (900.0 / 80.0) ** (u ** 0.8) * (0.09 ** fall)
568
+ knots = rng.uniform(-1, 1, int(dur * 60) + 2)
569
+ wob = _unit(_lp(np.interp(t, np.linspace(0, dur, len(knots)), knots), 25.0))
570
+ ph = _phase(f0 * (1 + 0.35 * wob))
571
+ tone = np.zeros(n)
572
+ for h, amp in ((1, 1.0), (2, 0.55), (3, 0.35), (4, 0.22), (5, 0.14), (6, 0.09), (8, 0.05)):
573
+ tone += amp * np.sin(h * ph + rng.uniform(0, 2 * np.pi))
574
+ tone *= 0.55 + 0.45 * np.abs(np.sin(_phase(6 + 20 * u) + 0.6 * wob))
575
+ whirr = _svf_bandpass(rng.standard_normal(n), 400.0 + 3200.0 * u, q=0.9)
576
+ whirr = _hp(whirr * (0.7 + 0.3 * np.sin(_phase(4 + 14 * u))), 150.0)
577
+ sib = _bp(rng.standard_normal(n), 3000.0, 9000.0)
578
+ gate = (np.sin(_phase(8 + 14 * u) + 3 * wob) > 0.35).astype(float)
579
+ sib *= _one_pole_lowpass(gate, 60.0)
580
+
581
+ def rms(v):
582
+ return float(np.sqrt(np.mean(v ** 2))) or 1.0
583
+
584
+ x = tone / rms(tone) + 0.25 * whirr / rms(whirr) + 0.6 * sib / rms(sib)
585
+ x = _hp(_lp(x, 12000.0, 1), 70.0)
586
+ tk = t - (dur - stop * 0.55)
587
+ x += 0.35 * float(np.max(np.abs(x))) * np.where(tk > 0, np.sin(2 * np.pi * 70 * tk) * np.exp(-tk / 0.05), 0.0)
588
+ env = np.minimum(1, t / 0.12) ** 1.5 * 10 ** ((-9.0 + 9.0 * u ** 1.5) / 20.0)
589
+ env *= np.where(t > body_len, np.exp(-((t - body_len) / (stop * 0.45)) ** 2), 1.0)
590
+ mono = _fade_tail(x * env, 10)
591
+ return np.stack([mono, mono])
592
+
593
+
594
+ def _data_stream(dur: float, seed: int) -> np.ndarray:
595
+ rng = np.random.default_rng(seed)
596
+ n = int(dur * SR)
597
+ x = np.zeros((2, n))
598
+ freqs = np.array([8300.0, 10900.0, 10900.0, 11600.0, 12200.0, 12900.0, 14500.0, 14500.0])
599
+ pos = 0
600
+ while pos < n:
601
+ ln = int(rng.uniform(0.022, 0.055) * SR)
602
+ if rng.random() < 0.8:
603
+ f = float(rng.choice(freqs)) * rng.uniform(0.995, 1.005)
604
+ k = np.arange(min(ln, n - pos))
605
+ seg = np.sin(2 * np.pi * f * k / SR)
606
+ g = np.ones_like(seg)
607
+ r = min(48, len(g) // 2)
608
+ if r:
609
+ g[:r] = np.linspace(0, 1, r)
610
+ g[-r:] = np.linspace(1, 0, r)
611
+ p = rng.uniform(0.15, 0.85)
612
+ amp = rng.uniform(0.5, 1.0)
613
+ x[0, pos:pos + len(k)] += seg * g * amp * np.cos(p * np.pi / 2)
614
+ x[1, pos:pos + len(k)] += seg * g * amp * np.sin(p * np.pi / 2)
615
+ pos += ln + int(rng.uniform(0.0, 0.012) * SR)
616
+ return x
617
+
618
+
619
+ def data() -> np.ndarray:
620
+ """Computer data: two streams of short, random high beeps (8 to 14.5 kHz), one leaning to each side."""
621
+ a, b = _data_stream(2.0, 23), _data_stream(2.0, 24)
622
+ x = np.stack([a[0] + 0.25 * b[0], b[1] + 0.25 * a[1]])
623
+ return np.stack([_fade_tail(_fade_in(c, 10), 150) for c in x])
624
+
625
+
626
+ BUILDERS = {"whoosh": whoosh, "pop": pop, "click": click, "tick": tick, "rise": rise, "cash": cash, "coin": coin,
627
+ "error": error, "bass_drop": bass_drop, "synth_hit": synth_hit, "flash": flash, "charge": charge,
628
+ "wind": wind, "rewind": rewind, "data": data}
629
+
630
+
631
+ # ---------------------------------------------------------------- io
632
+
633
+ def write_wav(path: Path, stereo: np.ndarray) -> None:
634
+ data = np.clip(stereo, -1.0, 1.0)
635
+ pcm = (data.T * 32767.0).round().astype("<i2")
636
+ path.parent.mkdir(parents=True, exist_ok=True)
637
+ tmp = path.with_name(".tmp-" + path.name)
638
+ with wave.open(str(tmp), "wb") as w:
639
+ w.setnchannels(2)
640
+ w.setsampwidth(2)
641
+ w.setframerate(SR)
642
+ w.writeframes(pcm.tobytes())
643
+ tmp.replace(path)
644
+
645
+
646
+ def kit_gain_db(sound, target_200ms: float | None) -> tuple[float, str]:
647
+ """Gain (dB) that puts a sound at the kit level at sfx.volume 1.0, and the window that set it.
648
+
649
+ target_200ms: the LUFS wanted on the loudest 200 ms window (None: long sound, set by the 3 s window alone).
650
+ The 3 s window never goes above LONG_CAP_LUFS."""
651
+ m = measure(sound)
652
+ up = 20 * math.log10(SFX_CALIBRATION)
653
+ gain = LONG_CAP_LUFS - (m["short_term_max_lufs"] + up)
654
+ window = "short_term_max_lufs"
655
+ if target_200ms is not None:
656
+ g200 = target_200ms - (m["loudness_200ms_lufs"] + up)
657
+ if g200 <= gain:
658
+ gain, window = g200, "loudness_200ms_lufs"
659
+ return gain, window
660
+
661
+
662
+ def render(name: str, peak_db: float = PEAK_DB) -> np.ndarray:
663
+ """The procedural sound as written to disk: peak at peak_db, or at its kit level for the KIT_LEVELS names."""
664
+ raw = BUILDERS[name]()
665
+ if name in KIT_LEVELS: # DC out, and the first and last samples at zero
666
+ raw = raw - raw.mean(axis=1, keepdims=True)
667
+ raw = np.stack([_fade_tail(_fade_in(c, 1.0), 5) for c in raw])
668
+ stereo = _normalize(raw, PEAK_DB if name in KIT_LEVELS else peak_db)
669
+ if name in KIT_LEVELS:
670
+ gain, _ = kit_gain_db(stereo, KIT_LEVELS[name])
671
+ stereo = stereo * 10 ** (gain / 20.0)
672
+ peak = 20 * math.log10(float(np.max(np.abs(stereo))))
673
+ if peak > KIT_PEAK_MAX_DB + 1e-6:
674
+ raise ValueError(f"{name}: o pico ficaria em {peak:.2f} dBFS no nível do kit")
675
+ return stereo
676
+
677
+
678
+ def make(name: str, out_dir: Path, peak_db: float = PEAK_DB) -> dict:
679
+ """Write out_dir/<name>.wav. peak_db only applies to the first seven; the KIT_LEVELS ones come out at kit level."""
680
+ stereo = render(name, peak_db)
681
+ path = out_dir / f"{name}.wav"
682
+ write_wav(path, stereo)
683
+ peak = float(np.max(np.abs(stereo)))
684
+ first = int(np.argmax(np.max(np.abs(stereo), axis=0) >= 0.5 * peak))
685
+ return {"name": name, "file": str(path), "duration": stereo.shape[1] / SR,
686
+ "peak_db": 20 * math.log10(peak), "attack_ms": 1000.0 * first / SR}
687
+
688
+
689
+ def ensure_sfx(names=SYNTH_NAMES, out_dir: Path = SYNTH_DIR) -> dict[str, Path]:
690
+ """Create any missing procedural SFX file in out_dir. Returns name -> path."""
691
+ out = {}
692
+ for name in names:
693
+ if name not in BUILDERS:
694
+ continue
695
+ path = Path(out_dir) / f"{name}.wav"
696
+ if not path.exists():
697
+ make(name, Path(out_dir))
698
+ out[name] = path
699
+ return out
700
+
701
+
702
+ # ---------------------------------------------------------------- built-in registry
703
+
704
+ def load_manifest(path: Path = MANIFEST) -> dict:
705
+ """assets/sfx/sfx.json as a dict (empty when missing or unreadable)."""
706
+ try:
707
+ data = json.loads(Path(path).read_text(encoding="utf-8"))
708
+ except (OSError, ValueError):
709
+ return {}
710
+ return data if isinstance(data, dict) else {}
711
+
712
+
713
+ def name_key(text: str) -> str:
714
+ """How a name is compared: lower case, no accents, spaces and hyphens as '_' ("Estalo de Dedo" -> estalo_de_dedo)."""
715
+ text = unicodedata.normalize("NFKD", str(text).strip().lower())
716
+ text = "".join(ch for ch in text if not unicodedata.combining(ch))
717
+ return re.sub(r"[\s\-]+", "_", text)
718
+
719
+
720
+ def alias_index(manifest: dict) -> dict[str, str]:
721
+ """name_key(id, label or alias) -> id. An id always wins over another effect's label or alias."""
722
+ entries = {k: v for k, v in manifest.items() if isinstance(v, dict)}
723
+ out = {name_key(name): name for name in entries}
724
+ for name, entry in entries.items():
725
+ for alias in [entry.get("label")] + list(entry.get("aliases") or []):
726
+ if isinstance(alias, str) and alias.strip():
727
+ out.setdefault(name_key(alias), name)
728
+ return out
729
+
730
+
731
+ def resolve_name(name, manifest: dict | None = None) -> str | None:
732
+ """The sfx.json id a name points to (the id itself, its Portuguese label or an alias), or None."""
733
+ manifest = load_manifest() if manifest is None else manifest
734
+ if not isinstance(name, str) or not name.strip():
735
+ return None
736
+ if isinstance(manifest.get(name), dict):
737
+ return name
738
+ return alias_index(manifest).get(name_key(name))
739
+
740
+
741
+ def builtin_sfx(names, sfx_dir: Path = SFX_DIR) -> dict[str, dict]:
742
+ """name -> {"path": Path, "anchor_s": float, "id": str} for every built-in effect in names whose file is there.
743
+
744
+ A name can be the id ("bass_drop"), its Portuguese label or an alias ("grave"); "id" is the sfx.json entry
745
+ it resolved to. A procedural file (under sinteticos/) that is missing is generated on the spot. Names that
746
+ are not built-in effects, or whose recording is missing, are left out (the caller warns)."""
747
+ sfx_dir = Path(sfx_dir)
748
+ manifest = load_manifest(sfx_dir / "sfx.json")
749
+ synth = (sfx_dir / "sinteticos").resolve()
750
+ out: dict[str, dict] = {}
751
+ for name in names:
752
+ effect = resolve_name(name, manifest)
753
+ entry = manifest.get(effect) if effect else None
754
+ if not isinstance(entry, dict) or not isinstance(entry.get("file"), str):
755
+ continue
756
+ path = (sfx_dir / entry["file"]).resolve()
757
+ if not path.exists() and path.parent == synth and path.stem in BUILDERS:
758
+ make(path.stem, synth)
759
+ if not path.exists():
760
+ continue
761
+ try:
762
+ anchor = max(0.0, float(entry.get("anchor_s") or 0.0))
763
+ except (TypeError, ValueError):
764
+ anchor = 0.0
765
+ out[name] = {"path": path, "anchor_s": anchor, "id": effect}
766
+ return out
767
+
768
+
769
+ # ---------------------------------------------------------------- measurement
770
+
771
+ # BS.1770 K-weighting at 48 kHz: high shelf, then the RLB high-pass
772
+ _K_STAGES = (((1.53512485958697, -2.69169618940638, 1.19839281085285), (1.0, -1.69065929318241, 0.73248077421585)),
773
+ ((1.0, -2.0, 1.0), (1.0, -1.99004745483398, 0.99007225036621)))
774
+
775
+
776
+ def read_audio(path: Path) -> np.ndarray:
777
+ """(channels, samples) float in [-1, 1] at 48 kHz: 16-bit WAV read directly, anything else through ffmpeg."""
778
+ path = Path(path)
779
+ try:
780
+ with wave.open(str(path), "rb") as w:
781
+ if w.getsampwidth() == 2 and w.getframerate() == SR:
782
+ ch = w.getnchannels()
783
+ pcm = np.frombuffer(w.readframes(w.getnframes()), dtype="<i2").astype(np.float64) / 32768.0
784
+ return pcm.reshape(-1, ch).T
785
+ except (wave.Error, EOFError, OSError):
786
+ pass
787
+ proc = subprocess.run(["ffmpeg", "-hide_banner", "-nostdin", "-v", "error", "-i", str(path), "-vn", "-f", "f32le",
788
+ "-ac", "2", "-ar", str(SR), "-"], capture_output=True)
789
+ if proc.returncode != 0:
790
+ raise ValueError(f"não consegui ler o som: {path}")
791
+ return np.frombuffer(proc.stdout[: len(proc.stdout) // 8 * 8], dtype="<f4").reshape(-1, 2).T.astype(np.float64)
792
+
793
+
794
+ def k_weighted(x: np.ndarray) -> np.ndarray:
795
+ """K-weighting applied in the frequency domain (x: channels x samples, 48 kHz, already padded with silence)."""
796
+ n = x.shape[1]
797
+ zinv = np.exp(-2j * np.pi * np.fft.rfftfreq(n, 1.0 / SR) / SR)
798
+ h = np.ones_like(zinv)
799
+ for b, a in _K_STAGES:
800
+ h *= (b[0] + b[1] * zinv + b[2] * zinv * zinv) / (a[0] + a[1] * zinv + a[2] * zinv * zinv)
801
+ return np.fft.irfft(np.fft.rfft(x, axis=1) * h, n=n, axis=1)
802
+
803
+
804
+ def measure(sound) -> dict:
805
+ """Levels and anchor of a sound (a path or a channels x samples array at 48 kHz).
806
+
807
+ loudness_200ms_lufs / momentary_max_lufs / short_term_max_lufs: the loudest 200 ms, 400 ms and 3 s window
808
+ (K-weighted, channels summed, BS.1770 offset -0.691). anchor_s: the first moment the 50 ms K-weighted
809
+ power envelope (centered) is within 2 dB of its maximum, i.e. when the sound reaches full strength."""
810
+ x = read_audio(sound) if not isinstance(sound, np.ndarray) else np.atleast_2d(np.asarray(sound, dtype=np.float64))
811
+ n = x.shape[1]
812
+ pad = 3 * SR
813
+ y = k_weighted(np.concatenate([np.zeros((x.shape[0], pad)), x, np.zeros((x.shape[0], pad))], axis=1))
814
+ power = (y * y).sum(axis=0)
815
+ c = np.concatenate([[0.0], np.cumsum(power)])
816
+
817
+ def window_max(seconds: float) -> float:
818
+ w = int(round(seconds * SR))
819
+ return -0.691 + 10 * math.log10(max(float(((c[w:] - c[:-w]) / w).max()), 1e-20))
820
+
821
+ w = int(ANCHOR_WINDOW_S * SR)
822
+ lo = np.arange(pad, pad + n) - w // 2
823
+ env = 10 * np.log10(np.maximum((c[lo + w] - c[lo]) / w, 1e-20))
824
+ anchor = int(np.argmax(env >= env.max() - ANCHOR_DROP_DB))
825
+ peak = float(np.max(np.abs(x))) if n else 0.0
826
+ return {"duration_s": round(n / SR, 3), "channels": int(x.shape[0]), "anchor_s": round(anchor / SR, 3),
827
+ "peak_dbfs": round(20 * math.log10(max(peak, 1e-9)), 2),
828
+ "loudness_200ms_lufs": round(window_max(0.2), 2), "momentary_max_lufs": round(window_max(0.4), 2),
829
+ "short_term_max_lufs": round(window_max(3.0), 2)}
830
+
831
+
832
+ LEVEL_KEYS = ("loudness_200ms_lufs", "momentary_max_lufs", "short_term_max_lufs")
833
+
834
+
835
+ def synthetic_registry(sfx_dir: Path = SFX_DIR) -> dict:
836
+ """The sfx.json entries for the procedural set alone: every name -> sinteticos/<name>.wav, measured.
837
+
838
+ Stored as assets/sfx/sfx_sinteticos.json (--synthetic-registry). It is the alternative made by this
839
+ project's own code, shipped alongside the CC0 kit that sfx.json points at by default."""
840
+ up = 20 * math.log10(SFX_CALIBRATION)
841
+ out = {}
842
+ for name in SYNTH_NAMES:
843
+ path = ensure_sfx([name], Path(sfx_dir) / "sinteticos")[name]
844
+ m = measure(path)
845
+ anchor = m.pop("anchor_s")
846
+ dur = f"{m['duration_s']:.2f}".rstrip("0").rstrip(".").replace(".", ",")
847
+ out[name] = {"file": f"sinteticos/{name}.wav", "anchor_s": anchor, "sound": f"{SYNTH_SOUNDS[name]} ({dur} s)",
848
+ "source": "sintético, helpers/make_sfx.py", "gain_db": 0.0,
849
+ "level_rule": kit_level_rule(name), "measured": m,
850
+ "at_volume_1": {k: round(m[k] + up, 2) for k in LEVEL_KEYS}}
851
+ return out
852
+
853
+
854
+ def _lufs_text(value: float) -> str:
855
+ return f"{value:.1f}".replace(".", ",")
856
+
857
+
858
+ def kit_level_rule(name: str) -> str:
859
+ """PT-BR text of how the generator sets the level of a procedural sound (sfx.json `level_rule`)."""
860
+ if name not in KIT_LEVELS:
861
+ return "sem ajuste: pico em -3 dBFS, como sai do gerador"
862
+ target = KIT_LEVELS[name]
863
+ _, window = kit_gain_db(render(name), target)
864
+ cap = f"janela de 3 s em {_lufs_text(LONG_CAP_LUFS)} LUFS com volume 1.0"
865
+ if target is None:
866
+ return f"acertado pelo gerador: {cap} (som longo: 6 dB abaixo da voz de -14 LUFS)"
867
+ if window == "short_term_max_lufs":
868
+ return (f"acertado pelo gerador: {cap}, o teto dos sons longos (com os {_lufs_text(target)} LUFS da janela "
869
+ f"de 200 ms a janela de 3 s passaria do teto)")
870
+ return (f"acertado pelo gerador: janela de 200 ms em {_lufs_text(target)} LUFS com volume 1.0, sem passar de "
871
+ f"{_lufs_text(LONG_CAP_LUFS)} na janela de 3 s")
872
+
873
+
874
+ def _print_measure(label: str, m: dict) -> None:
875
+ up = 20 * math.log10(SFX_CALIBRATION)
876
+ print(f"{label}: {m['duration_s']:.2f}s ponto forte em {m['anchor_s']:.3f}s pico {m['peak_dbfs']:.1f} dBFS "
877
+ f"200 ms {m['loudness_200ms_lufs']:.1f} LUFS (volume 1.0: {m['loudness_200ms_lufs'] + up:.1f}) "
878
+ f"400 ms {m['momentary_max_lufs']:.1f} ({m['momentary_max_lufs'] + up:.1f}) "
879
+ f"3 s {m['short_term_max_lufs']:.1f} ({m['short_term_max_lufs'] + up:.1f})")
880
+
881
+
882
+ def main(argv: list[str] | None = None) -> int:
883
+ ap = argparse.ArgumentParser(description=f"Gera os efeitos sonoros sintéticos ({', '.join(SYNTH_NAMES)}) em WAV 48 kHz, "
884
+ "ou mede o volume e o ponto forte de efeitos (--measure).")
885
+ ap.add_argument("--out-dir", default=str(SYNTH_DIR), help="pasta de saída (padrão: assets/sfx/sinteticos da skill)")
886
+ ap.add_argument("--only", help="só estes, separados por vírgula (ex: whoosh,pop)")
887
+ ap.add_argument("--peak-db", type=float, default=PEAK_DB,
888
+ help="pico em dBFS (padrão -3; os sons de 16/09/2026 saem sempre no nível do kit)")
889
+ ap.add_argument("--measure", nargs="*", metavar="ARQUIVO",
890
+ help="mede volume (janelas de 200 ms, 400 ms e 3 s) e ponto forte; sem arquivo, mede os efeitos prontos")
891
+ ap.add_argument("--synthetic-registry", action="store_true",
892
+ help="mede os efeitos sintéticos e grava assets/sfx/sfx_sinteticos.json (o sfx.json do pacote de venda)")
893
+ args = ap.parse_args(argv)
894
+
895
+ if args.synthetic_registry:
896
+ text = json.dumps(synthetic_registry(), ensure_ascii=False, indent=2) + "\n"
897
+ SYNTH_REGISTRY.write_text(text, encoding="utf-8")
898
+ print(f"ok: {SYNTH_REGISTRY}")
899
+ return 0
900
+
901
+ if args.measure is not None:
902
+ if args.measure:
903
+ items = [(p, Path(p)) for p in args.measure]
904
+ else:
905
+ found = builtin_sfx(NAMES)
906
+ items = [(name, found[name]["path"]) for name in NAMES if name in found]
907
+ for label, path in items:
908
+ if not Path(path).exists():
909
+ print(f"erro: arquivo não encontrado: {path}", file=sys.stderr)
910
+ return 2
911
+ try:
912
+ _print_measure(str(label), measure(Path(path)))
913
+ except ValueError as e:
914
+ print(f"erro: {e}", file=sys.stderr)
915
+ return 2
916
+ return 0
917
+
918
+ names = [n.strip() for n in args.only.split(",")] if args.only else list(SYNTH_NAMES)
919
+ bad = [n for n in names if n not in BUILDERS]
920
+ if bad:
921
+ print(f"erro: efeito desconhecido: {', '.join(bad)}. Opções: {', '.join(SYNTH_NAMES)}", file=sys.stderr)
922
+ return 2
923
+ if not -20.0 <= args.peak_db <= 0.0:
924
+ print("erro: --peak-db precisa ficar entre -20 e 0", file=sys.stderr)
925
+ return 2
926
+ out_dir = Path(args.out_dir).resolve()
927
+ try:
928
+ out_dir.mkdir(parents=True, exist_ok=True)
929
+ except OSError as e:
930
+ print(f"erro: não consegui criar a pasta {out_dir}: {e}", file=sys.stderr)
931
+ return 2
932
+ for name in names:
933
+ info = make(name, out_dir, args.peak_db)
934
+ print(f"ok: {name}.wav {info['duration']:.2f}s pico {info['peak_db']:.1f} dBFS ataque em {info['attack_ms']:.1f} ms")
935
+ print(f"pasta: {out_dir}")
936
+ return 0
937
+
938
+
939
+ if __name__ == "__main__":
940
+ sys.exit(main())