claude-code-arcane 2.1.0 → 2.2.0
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.
- package/CHANGELOG.md +7 -0
- package/README.md +5 -4
- package/agents/audio/audio-director.md +90 -0
- package/agents/audio/audio-programmer.md +97 -0
- package/agents/audio/audio-qa.md +95 -0
- package/agents/audio/composer.md +88 -0
- package/agents/audio/technical-sound-designer.md +90 -0
- package/agents/audio/voice-director.md +96 -0
- package/docs/SKILLS-CATALOG.md +22 -2
- package/docs/USER-GUIDE.md +2 -2
- package/package.json +1 -1
- package/profiles/audio.yaml +45 -0
- package/rules/gamedev/audio-code.md +45 -0
- package/skills/adaptive-music/SKILL.md +171 -0
- package/skills/adaptive-music/references/anti-patterns.md +53 -0
- package/skills/audio-audit/SKILL.md +164 -0
- package/skills/audio-audit/references/anti-patterns.md +65 -0
- package/skills/audio-audit/scripts/check_assets.py +208 -0
- package/skills/audio-bible/SKILL.md +119 -0
- package/skills/audio-bible/references/anti-patterns.md +39 -0
- package/skills/audio-mix/SKILL.md +151 -0
- package/skills/audio-mix/references/anti-patterns.md +65 -0
- package/skills/audio-spec/SKILL.md +107 -0
- package/skills/audio-spec/references/anti-patterns.md +45 -0
- package/skills/middleware-integration/SKILL.md +186 -0
- package/skills/middleware-integration/references/anti-patterns.md +69 -0
- package/skills/midi-compose/SKILL.md +136 -0
- package/skills/midi-compose/references/smf-format.md +129 -0
- package/skills/midi-compose/scripts/compose_midi.py +326 -0
- package/skills/midi-compose/templates/example-composition.json +69 -0
- package/skills/music-composition/SKILL.md +106 -0
- package/skills/music-composition/references/form.md +57 -0
- package/skills/music-composition/references/game-music.md +82 -0
- package/skills/music-composition/references/harmony.md +82 -0
- package/skills/music-composition/references/melody.md +48 -0
- package/skills/music-composition/references/orchestration.md +69 -0
- package/skills/music-composition/references/rhythm-groove.md +49 -0
- package/skills/procedural-audio/SKILL.md +157 -0
- package/skills/procedural-audio/references/anti-patterns.md +45 -0
- package/skills/sfx-design/SKILL.md +130 -0
- package/skills/sfx-design/references/anti-patterns.md +53 -0
- package/skills/spatial-audio/SKILL.md +139 -0
- package/skills/spatial-audio/references/anti-patterns.md +53 -0
- package/skills/ui-sound-design/SKILL.md +133 -0
- package/skills/ui-sound-design/references/anti-patterns.md +57 -0
- package/skills/voice-pipeline/SKILL.md +150 -0
- package/skills/voice-pipeline/references/anti-patterns.md +69 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: audio-spec
|
|
3
|
+
description: "SFX spec sheets y audio event lists: trigger, prioridad, concurrencia, cooldown, variaciones, propiedades espaciales. Usar para: especificar sonidos, event list por sistema, brief para sound designer."
|
|
4
|
+
category: "audio"
|
|
5
|
+
argument-hint: "[system-name | all | events]"
|
|
6
|
+
user-invocable: true
|
|
7
|
+
allowed-tools: Read, Glob, Grep, Write, Edit
|
|
8
|
+
---
|
|
9
|
+
# audio-spec — SFX Specs & Event Lists
|
|
10
|
+
|
|
11
|
+
Convierte dirección sonora en especificaciones que un sound designer externo puede producir sin más briefing, y en event lists que un programador puede implementar sin adivinar.
|
|
12
|
+
|
|
13
|
+
Dos entregables distintos y complementarios:
|
|
14
|
+
- **Spec sheet** — describe *el sonido*: cómo suena, cuánto dura, cuántas variantes
|
|
15
|
+
- **Event list** — describe *el contrato*: qué lo dispara, con qué prioridad, cuántas instancias, qué parámetros recibe
|
|
16
|
+
|
|
17
|
+
## Cuándo usar
|
|
18
|
+
|
|
19
|
+
- Después de `/audio-bible` (la dirección tiene que existir primero)
|
|
20
|
+
- Cuando vas a tercerizar producción de SFX y necesitás un brief cerrado
|
|
21
|
+
- Cuando el código necesita saber qué eventos postear antes de que existan los assets
|
|
22
|
+
- Antes de `/middleware-integration` — la event list es su input
|
|
23
|
+
|
|
24
|
+
## Modos
|
|
25
|
+
|
|
26
|
+
| Modo | Qué hace |
|
|
27
|
+
|---|---|
|
|
28
|
+
| `system-name` | Especifica un sistema concreto: spec sheets + event list de, por ejemplo, `combat` |
|
|
29
|
+
| `all` | Barre todos los sistemas del proyecto, uno por vez |
|
|
30
|
+
| `events` | Solo la event list, sin spec sheets. Para cuando el código necesita el contrato antes de que existan los assets |
|
|
31
|
+
|
|
32
|
+
## Input
|
|
33
|
+
|
|
34
|
+
1. Leer audio bible (paleta, frequency allocation, mix hierarchy, standards). Si no existe, avisar y ofrecer `/audio-bible` primero
|
|
35
|
+
2. Identificar el sistema a especificar (combate, locomoción, UI, ambiente, props...) o `all`
|
|
36
|
+
3. Leer el código/GDD del sistema para extraer los momentos que piden sonido
|
|
37
|
+
4. Preguntar al usuario: nivel de detalle (spec completo vs event list sola) y si hay assets existentes que reusar
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Spec sheet — campos obligatorios
|
|
42
|
+
|
|
43
|
+
Cada sonido lleva estos campos. Un campo vacío es una decisión que alguien va a tomar por vos:
|
|
44
|
+
|
|
45
|
+
| Campo | Qué define | Ejemplo |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| **ID** | Nombre canónico, matchea naming del bible | `sfx_weapon_shotgun_fire` |
|
|
48
|
+
| **Descripción** | Qué es, en una línea funcional | Disparo de escopeta de doble caño, seco, sin cola de reverb |
|
|
49
|
+
| **Referencias** | 1-3 sonidos concretos con qué tomar de cada uno | — |
|
|
50
|
+
| **Carácter frecuencial** | Dónde vive, contra la allocation del bible | Sub-punch 50-70 Hz + crack 3-5 kHz, vacío en mids |
|
|
51
|
+
| **Duración** | Rango, no valor único | 180-260 ms |
|
|
52
|
+
| **Envolvente** | Attack / body / tail explícitos | Attack <5 ms, body 60 ms, tail 120 ms |
|
|
53
|
+
| **Rango de volumen** | Nivel relativo a la categoría | −6 dB a −3 dB del bus SFX |
|
|
54
|
+
| **Propiedades espaciales** | 2D/3D, curva de atenuación, min/max distance | 3D, 2-40 m, log |
|
|
55
|
+
| **Variaciones** | Cuántas variantes y qué randomización | 5 variantes, pitch ±3%, volumen ±1.5 dB |
|
|
56
|
+
| **Prioridad** | Qué se cae primero bajo presión de voces | Alta (feedback de acción del jugador) |
|
|
57
|
+
|
|
58
|
+
## Event list — campos obligatorios
|
|
59
|
+
|
|
60
|
+
| Campo | Qué define |
|
|
61
|
+
|---|---|
|
|
62
|
+
| **Evento** | Nombre que postea el código |
|
|
63
|
+
| **Trigger** | Condición exacta de gameplay que lo dispara |
|
|
64
|
+
| **Sonidos** | Qué IDs de spec resuelve (uno o un container) |
|
|
65
|
+
| **Concurrencia** | Máximo de instancias simultáneas + qué hacer al exceder (drop oldest / drop newest / reject) |
|
|
66
|
+
| **Cooldown** | Mínimo entre disparos, para eventos que pueden spamearse |
|
|
67
|
+
| **Parámetros** | RTPCs que recibe, con rango normalizado |
|
|
68
|
+
| **Stop condition** | Cómo termina: one-shot, loop con stop explícito, o fade |
|
|
69
|
+
|
|
70
|
+
**Concurrencia y cooldown no son opcionales.** Son la diferencia entre un sistema que sobrevive 40 enemigos en pantalla y uno que satura el voice budget. Todo evento que puede dispararse más de una vez por frame necesita ambos.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Proceso
|
|
75
|
+
|
|
76
|
+
1. **Barrer el sistema** — listar cada momento que pide sonido, incluyendo los negativos (dónde el silencio es la decisión)
|
|
77
|
+
2. **Agrupar** — sonidos que comparten carácter van juntos; identificar qué puede ser variación de un mismo asset en vez de asset nuevo
|
|
78
|
+
3. **Draftar** specs y event list del grupo
|
|
79
|
+
4. **Presentar** al usuario para review
|
|
80
|
+
5. **Escribir** — preguntar "¿Escribo el spec de `<sistema>` a `<path>`?" antes de usar Write
|
|
81
|
+
|
|
82
|
+
No batchear sistemas: cerrar uno antes de abrir el siguiente.
|
|
83
|
+
|
|
84
|
+
### Reducción de assets
|
|
85
|
+
|
|
86
|
+
Antes de especificar N sonidos nuevos, preguntar si el caso se resuelve con variación o con síntesis. El caso canónico: 20 samples × 6 superficies × 3 intensidades = 360 archivos (~180 MB) vs footsteps procedurales (~50 KB) con variación infinita. Si el sistema es de alta frecuencia y muchas dimensiones, derivar a `/procedural-audio` antes de escribir el spec.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Verdict
|
|
91
|
+
|
|
92
|
+
Por sistema especificado:
|
|
93
|
+
|
|
94
|
+
- **READY** — todo sonido tiene los 10 campos, todo evento tiene concurrencia y cooldown. Un externo puede producir y un programador puede implementar
|
|
95
|
+
- **CONCERNS** — hay campos vacíos o eventos sin límites; listar cuáles
|
|
96
|
+
- **BLOCKED** — falta el audio bible, o el sistema de gameplay no está definido lo suficiente para saber qué momentos existen
|
|
97
|
+
|
|
98
|
+
## Anti-patterns
|
|
99
|
+
|
|
100
|
+
> → Read references/anti-patterns.md
|
|
101
|
+
|
|
102
|
+
## Next steps
|
|
103
|
+
|
|
104
|
+
- `/middleware-integration` para implementar la event list en Wwise/FMOD
|
|
105
|
+
- `/procedural-audio` para los sistemas de alta frecuencia que no conviene samplear
|
|
106
|
+
- `/sfx-design` para profundizar el diseño de los sonidos hero
|
|
107
|
+
- `/audio-audit` para verificar los assets entregados contra el spec
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Audio Spec — Anti-patterns
|
|
2
|
+
|
|
3
|
+
## Spec sin números
|
|
4
|
+
|
|
5
|
+
"Impacto potente y satisfactorio" no es producible. Duración, envolvente, bandas de frecuencia y nivel relativo sí. Si el campo no tiene número o rango, no está especificado.
|
|
6
|
+
|
|
7
|
+
## Duración como valor único
|
|
8
|
+
|
|
9
|
+
"200 ms" fuerza al sound designer a mentir o a pedir aprobación. Un rango (180-260 ms) le da lugar a que el sonido termine donde tiene que terminar.
|
|
10
|
+
|
|
11
|
+
## Eventos sin concurrencia
|
|
12
|
+
|
|
13
|
+
El spec dice "sonido de casquillo al caer". Llegan 40 enemigos, 300 casquillos, el voice budget explota y el mix desaparece. Todo evento que puede dispararse más de una vez por frame necesita máximo de instancias y política de descarte.
|
|
14
|
+
|
|
15
|
+
## Cooldown ausente en eventos de UI
|
|
16
|
+
|
|
17
|
+
Navegación rápida en un menú dispara el mismo tick 15 veces en 200 ms. Suena como un glitch. Cooldown mínimo, siempre.
|
|
18
|
+
|
|
19
|
+
## Una variante
|
|
20
|
+
|
|
21
|
+
Un sonido sin variantes se vuelve audible como *sonido* en vez de como evento en unos 20 minutos de juego. Si el evento es frecuente, mínimo 4-5 variantes o randomización de pitch, y preferentemente ambas.
|
|
22
|
+
|
|
23
|
+
## Pitch randomization exagerada
|
|
24
|
+
|
|
25
|
+
±3% es variación. ±20% cambia el objeto: el arma parece otra arma. Rango chico y round-robin le gana a rango grande.
|
|
26
|
+
|
|
27
|
+
## Specs 3D sin curva
|
|
28
|
+
|
|
29
|
+
Marcar "3D" y no definir min/max distance ni forma de la curva deja la atenuación en el default del middleware, que casi nunca es el correcto. El sonido se oye a 200 m o desaparece a 3 m.
|
|
30
|
+
|
|
31
|
+
## Especificar antes de decidir procedural
|
|
32
|
+
|
|
33
|
+
Escribir 360 spec sheets de footsteps es trabajo perdido si la respuesta correcta era síntesis. Evaluar el eje frecuencia × dimensiones antes de especificar.
|
|
34
|
+
|
|
35
|
+
## Event list sin stop condition
|
|
36
|
+
|
|
37
|
+
Los loops son la fuente número uno de audio colgado. Todo sonido que loopea necesita decir explícitamente quién lo para y qué pasa si el objeto dueño se destruye primero.
|
|
38
|
+
|
|
39
|
+
## Prioridades todas iguales
|
|
40
|
+
|
|
41
|
+
Si todo es prioridad alta, el voice limiter descarta al azar y se cae el feedback de la acción del jugador antes que un ambiente lejano. Las prioridades solo sirven si están diferenciadas.
|
|
42
|
+
|
|
43
|
+
## IDs que no siguen el naming del bible
|
|
44
|
+
|
|
45
|
+
`shotgun2_FINAL_v3` rompe búsqueda, batch processing y automatización de banks. El ID sale del pattern del bible, sin excepciones.
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: middleware-integration
|
|
3
|
+
description: "Integracion de middleware de audio (Wwise/FMOD): containers, eventos, Switches/States/RTPCs, atenuaciones, soundbanks, streaming vs memoria, y el contrato con el codigo de gameplay. Usar para: Wwise, FMOD, event authoring, RTPC, soundbanks, arquitectura de audio."
|
|
4
|
+
category: "audio"
|
|
5
|
+
argument-hint: "[structure | events | rtpc | banks | contract]"
|
|
6
|
+
user-invocable: true
|
|
7
|
+
allowed-tools: Read, Glob, Grep, Write, Edit
|
|
8
|
+
---
|
|
9
|
+
# middleware-integration — Wwise / FMOD
|
|
10
|
+
|
|
11
|
+
Diseña la estructura del proyecto de middleware y el contrato entre el audio y el código de gameplay.
|
|
12
|
+
|
|
13
|
+
Conceptos agnósticos donde se puede; nomenclatura de Wwise como referencia principal, con el equivalente FMOD anotado, porque los dos modelan lo mismo con nombres distintos.
|
|
14
|
+
|
|
15
|
+
## Cuándo usar
|
|
16
|
+
|
|
17
|
+
- Estructurar un proyecto de Wwise/FMOD desde cero
|
|
18
|
+
- Definir el contrato de eventos que va a postear el código
|
|
19
|
+
- Diseñar la organización de soundbanks
|
|
20
|
+
- El proyecto de middleware creció desordenado y hay que reorganizarlo
|
|
21
|
+
- Decidir qué va en memoria y qué va en streaming
|
|
22
|
+
|
|
23
|
+
## Input
|
|
24
|
+
|
|
25
|
+
1. Leer `/audio-spec` (event lists), `/audio-mix` (buses) y `/adaptive-music` (matriz de transiciones) si existen
|
|
26
|
+
2. Determinar el middleware y su versión
|
|
27
|
+
3. Preguntar al usuario: plataformas target, budget de memoria de audio, y si el proyecto ya existe o se arranca limpio
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Equivalencias Wwise ↔ FMOD
|
|
32
|
+
|
|
33
|
+
| Concepto | Wwise | FMOD Studio |
|
|
34
|
+
|---|---|---|
|
|
35
|
+
| Unidad que dispara el código | Event | Event |
|
|
36
|
+
| Contenedor de variación | Random / Sequence Container | Multi-instrument, Scatterer |
|
|
37
|
+
| Selección por estado discreto | Switch Container | Parameter (labeled) |
|
|
38
|
+
| Mezcla continua entre variantes | Blend Container | Parameter con automatización |
|
|
39
|
+
| Parámetro continuo | RTPC (Game Parameter) | Parameter (continuous) |
|
|
40
|
+
| Estado global | State | Global parameter / Snapshot |
|
|
41
|
+
| Agrupación de mezcla | Actor-Mixer / Bus | Group bus |
|
|
42
|
+
| Snapshot de mezcla | State con propiedades de bus | Snapshot |
|
|
43
|
+
| Curva de distancia | Attenuation ShareSet | 3D panner + curvas |
|
|
44
|
+
| Empaquetado | SoundBank | Bank |
|
|
45
|
+
| Música interactiva | Music Switch / Playlist Container | Event con transition regions |
|
|
46
|
+
|
|
47
|
+
## Estructura del proyecto
|
|
48
|
+
|
|
49
|
+
Organizar por **sistema de gameplay**, no por tipo de sonido. `Weapons/Shotgun/` es navegable; `Impacts/` con todo mezclado no.
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
Actor-Mixer Hierarchy
|
|
53
|
+
├── Player (locomoción, armas, voz del jugador)
|
|
54
|
+
├── Enemies (por arquetipo)
|
|
55
|
+
├── World (props, entorno, destructibles)
|
|
56
|
+
├── Ambience (por zona)
|
|
57
|
+
└── UI
|
|
58
|
+
|
|
59
|
+
Interactive Music
|
|
60
|
+
└── (por estado: Explore, Combat, Victory...)
|
|
61
|
+
|
|
62
|
+
Events
|
|
63
|
+
└── (espeja la jerarquía de Actor-Mixer)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Reglas que evitan el desorden:**
|
|
67
|
+
- Un evento por acción de gameplay, no por sonido. `Play_Weapon_Fire` con un Switch de tipo de arma le gana a un evento por arma
|
|
68
|
+
- Los ShareSets (atenuación, conversión, efectos) se definen una vez y se reusan. Un ShareSet por objeto es la causa número uno de proyectos inmanejables
|
|
69
|
+
- Work units separados por sistema: es lo que permite que dos personas trabajen sin conflictos de merge
|
|
70
|
+
|
|
71
|
+
## Containers
|
|
72
|
+
|
|
73
|
+
| Container | Cuándo | Cuidado |
|
|
74
|
+
|---|---|---|
|
|
75
|
+
| **Random** | Variación de un mismo sonido | Usar *shuffle* (round-robin), no random puro: random repite |
|
|
76
|
+
| **Sequence** | Sonidos que tienen orden (carga → disparo) | Definir qué pasa al interrumpir |
|
|
77
|
+
| **Switch** | Selección por estado discreto (superficie, tipo de arma) | Definir el default: si el Switch no está seteado, el evento no suena |
|
|
78
|
+
| **Blend** | Mezcla continua por parámetro (RPM de motor) | Cuidar el solapamiento de las curvas o hay huecos |
|
|
79
|
+
|
|
80
|
+
## Switches, States y RTPCs
|
|
81
|
+
|
|
82
|
+
Tres cosas distintas, y confundirlas es un error de arquitectura frecuente:
|
|
83
|
+
|
|
84
|
+
| | Alcance | Tipo | Ejemplo |
|
|
85
|
+
|---|---|---|---|
|
|
86
|
+
| **Switch** | Por objeto de juego | Discreto | Superficie bajo *este* personaje |
|
|
87
|
+
| **State** | Global | Discreto | El juego está en pausa |
|
|
88
|
+
| **RTPC** | Global o por objeto | Continuo | Salud del jugador, intensidad de combate |
|
|
89
|
+
|
|
90
|
+
**Regla:** si es una propiedad de un objeto específico, es Switch. Si es una condición del juego entero, es State. Si es un número que varía suave, es RTPC.
|
|
91
|
+
|
|
92
|
+
Usar un State para algo que es por objeto es el bug que produce "todos los personajes caminan sobre la superficie del jugador".
|
|
93
|
+
|
|
94
|
+
**Contrato de RTPCs:** todo RTPC necesita rango declarado, curva, y valor por default. El código **normaliza y clampea antes de enviarlo** — el middleware no debería recibir nunca un valor fuera de rango.
|
|
95
|
+
|
|
96
|
+
## Soundbanks
|
|
97
|
+
|
|
98
|
+
Estrategia por defecto que funciona en la mayoría de los proyectos:
|
|
99
|
+
|
|
100
|
+
| Bank | Contenido | Cuándo carga |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| `Init` | Estructura del proyecto, buses | Al arrancar, siempre |
|
|
103
|
+
| `Global` | UI, voz del jugador, música persistente | Al arrancar, siempre |
|
|
104
|
+
| `Level_XX` | Ambiente y props del nivel | Al cargar el nivel |
|
|
105
|
+
| `Character_XX` | Por personaje/enemigo | Con el spawn del personaje |
|
|
106
|
+
|
|
107
|
+
**Streaming vs memoria:**
|
|
108
|
+
|
|
109
|
+
| Va en memoria | Va en streaming |
|
|
110
|
+
|---|---|
|
|
111
|
+
| Todo lo corto y frecuente (SFX, UI, footsteps) | Música |
|
|
112
|
+
| Cualquier cosa que necesite latencia cero | Diálogo largo |
|
|
113
|
+
| | Ambientes largos |
|
|
114
|
+
|
|
115
|
+
El streaming ahorra memoria y cuesta latencia y accesos a disco. Un SFX en streaming llega tarde; la música en memoria se come el budget.
|
|
116
|
+
|
|
117
|
+
**Prefetch** para lo streameado que necesita arrancar rápido: los primeros ms van en memoria y el resto se stremea.
|
|
118
|
+
|
|
119
|
+
## Contrato con el código
|
|
120
|
+
|
|
121
|
+
Lo que el código tiene que poder hacer, y nada más:
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
PostEvent(eventName, gameObject) → dispara
|
|
125
|
+
StopEvent(playingId, fadeMs) → detiene lo que loopea
|
|
126
|
+
SetSwitch(group, value, gameObject) → propiedad de objeto
|
|
127
|
+
SetState(group, value) → condición global
|
|
128
|
+
SetRTPC(param, value01, gameObject?) → parámetro, normalizado
|
|
129
|
+
RegisterGameObject / Unregister → ciclo de vida
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**Reglas del contrato** (las mismas que enforcea `rules/gamedev/audio-code.md`):
|
|
133
|
+
|
|
134
|
+
- Gameplay **nunca** referencia assets, solo nombres de evento
|
|
135
|
+
- Todo loop tiene un stop explícito, y se detiene al destruirse el objeto dueño
|
|
136
|
+
- Los RTPCs llegan normalizados y clampeados
|
|
137
|
+
- Un evento que falla (bank no cargado) **no rompe el frame**: degrada en silencio
|
|
138
|
+
- Registrar y des-registrar game objects es obligatorio — objetos filtrados son un leak clásico
|
|
139
|
+
|
|
140
|
+
## Profiling
|
|
141
|
+
|
|
142
|
+
El middleware trae el profiler, y es la única fuente de verdad sobre lo que pasa en runtime:
|
|
143
|
+
|
|
144
|
+
- **Voces activas** vs el límite configurado, y qué se está descartando
|
|
145
|
+
- **CPU de audio** contra el 5-10% del frame
|
|
146
|
+
- **Memoria** por bank y total
|
|
147
|
+
- **Valores de RTPC en vivo** — sirve para descubrir que el código manda un rango equivocado
|
|
148
|
+
- **Árbol de contribución de voz** — por qué un sonido se oye más o menos de lo esperado
|
|
149
|
+
- Capturas exportables para análisis offline
|
|
150
|
+
|
|
151
|
+
Para profilear objetos de juego hay que tener la captura activa en la vista de Game Object antes de disparar los eventos, o no se registra nada.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Proceso
|
|
156
|
+
|
|
157
|
+
1. **Definir la jerarquía** por sistema de gameplay, con work units separados
|
|
158
|
+
2. **Crear los ShareSets** de atenuación y conversión antes de los objetos, para que se reusen
|
|
159
|
+
3. **Autorear los eventos** desde la event list de `/audio-spec`, uno por acción
|
|
160
|
+
4. **Definir Switch/State/RTPC** con su alcance correcto, rango y default
|
|
161
|
+
5. **Diseñar los buses** desde `/audio-mix`
|
|
162
|
+
6. **Definir los banks** y qué va en streaming
|
|
163
|
+
7. **Documentar el contrato** para el equipo de código
|
|
164
|
+
8. **Escribir** — preguntar "¿Escribo la arquitectura a `<path>`?" antes de usar Write
|
|
165
|
+
|
|
166
|
+
### Nota sobre automatización
|
|
167
|
+
|
|
168
|
+
Wwise expone WAAPI y FMOD tiene su API de scripting, así que la autoría en batch es automatizable. Hay servidores MCP de terceros que exponen WAAPI a un agente, pero al momento de escribir esto el más completo se declara **EXPERIMENTAL y explícitamente no recomendado para proyectos de producción**. Si querés explorarlo, hacelo en un proyecto de prueba, nunca sobre el proyecto real.
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Verdict
|
|
173
|
+
|
|
174
|
+
- **READY** — jerarquía por sistema, ShareSets reusados, cada parámetro con el alcance correcto, banks definidos, contrato documentado
|
|
175
|
+
- **CONCERNS** — la estructura existe pero hay ShareSets duplicados, parámetros con alcance equivocado (State donde va Switch), o banks sin estrategia de carga
|
|
176
|
+
- **BLOCKED** — falta la event list de `/audio-spec` o la estructura de buses de `/audio-mix`
|
|
177
|
+
|
|
178
|
+
## Anti-patterns
|
|
179
|
+
|
|
180
|
+
> → Read references/anti-patterns.md
|
|
181
|
+
|
|
182
|
+
## Next steps
|
|
183
|
+
|
|
184
|
+
- `/audio-audit` para profilear voces, CPU y memoria en runtime
|
|
185
|
+
- `/adaptive-music` si falta la matriz de transiciones de música
|
|
186
|
+
- `/audio-spec` si la event list no existe todavía
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Middleware Integration — Anti-patterns
|
|
2
|
+
|
|
3
|
+
## Un evento por sonido
|
|
4
|
+
|
|
5
|
+
500 eventos donde alcanzaban 40 con Switches. El código termina conociendo el nombre de cada asset, que es exactamente lo que el middleware venía a evitar. Un evento por *acción de gameplay*.
|
|
6
|
+
|
|
7
|
+
## ShareSet por objeto
|
|
8
|
+
|
|
9
|
+
Cada sonido con su propia atenuación y su propia conversión. Cambiar el rango de audición del juego pasa de ser un cambio a ser 300. Definir los ShareSets primero y reusarlos.
|
|
10
|
+
|
|
11
|
+
## State donde va Switch
|
|
12
|
+
|
|
13
|
+
Usar un State global para la superficie bajo el personaje produce el bug clásico: todos los personajes caminan sobre la superficie del jugador. Propiedad de objeto = Switch.
|
|
14
|
+
|
|
15
|
+
## Switch sin default
|
|
16
|
+
|
|
17
|
+
Si el Switch no está seteado cuando llega el evento, el sonido no suena, y el bug es invisible hasta que alguien nota el silencio. Todo Switch group con default.
|
|
18
|
+
|
|
19
|
+
## Random puro en vez de shuffle
|
|
20
|
+
|
|
21
|
+
Random repite: con 4 variantes hay 25% de escuchar la misma dos veces seguidas. Shuffle/round-robin excluyendo la última.
|
|
22
|
+
|
|
23
|
+
## RTPC sin clamp del lado del código
|
|
24
|
+
|
|
25
|
+
Un valor de salud que llega en 150 cuando el RTPC espera 0-100 deja el parámetro pegado al extremo. Normalizar y clampear en el boundary de gameplay.
|
|
26
|
+
|
|
27
|
+
## Loops sin stop explícito
|
|
28
|
+
|
|
29
|
+
La fuente número uno de audio colgado. Todo sonido que loopea necesita un stop, y tiene que detenerse cuando el objeto dueño se destruye.
|
|
30
|
+
|
|
31
|
+
## Game objects sin des-registrar
|
|
32
|
+
|
|
33
|
+
Cada enemigo muerto deja su game object registrado. A las dos horas de juego hay miles. Leak clásico, y arruina el profiling.
|
|
34
|
+
|
|
35
|
+
## Todo en un bank
|
|
36
|
+
|
|
37
|
+
Un bank monolítico se carga entero aunque el nivel use el 10%. Y cualquier cambio obliga a redistribuirlo completo. Init + Global + por nivel + por personaje.
|
|
38
|
+
|
|
39
|
+
## SFX en streaming
|
|
40
|
+
|
|
41
|
+
Un sonido corto y frecuente en streaming llega tarde y genera accesos a disco. Streaming es para música, diálogo largo y ambientes.
|
|
42
|
+
|
|
43
|
+
## Música en memoria
|
|
44
|
+
|
|
45
|
+
Se come el budget de audio entero. Streaming con prefetch.
|
|
46
|
+
|
|
47
|
+
## Fallo de evento que rompe el frame
|
|
48
|
+
|
|
49
|
+
Un bank no cargado tira una excepción que llega a gameplay. El audio degrada en silencio, siempre.
|
|
50
|
+
|
|
51
|
+
## Jerarquía por tipo de sonido
|
|
52
|
+
|
|
53
|
+
`Impacts/` con todos los impactos del juego mezclados no se navega ni se mezcla. Organizar por sistema de gameplay.
|
|
54
|
+
|
|
55
|
+
## Un solo work unit
|
|
56
|
+
|
|
57
|
+
Dos personas trabajando producen conflictos de merge irresolubles en un archivo binario o XML gigante. Work units por sistema.
|
|
58
|
+
|
|
59
|
+
## Confiar en el editor en vez del profiler
|
|
60
|
+
|
|
61
|
+
Disparar eventos a mano desde el editor no dice nada sobre voces activas, CPU o memoria en gameplay real. El profiler es la única fuente de verdad.
|
|
62
|
+
|
|
63
|
+
## Profilear game objects sin activar la captura
|
|
64
|
+
|
|
65
|
+
Sin la captura activa en la vista de Game Object antes de disparar los eventos, no se registra nada y parece que el sistema no funciona.
|
|
66
|
+
|
|
67
|
+
## Automatizar sobre el proyecto de producción
|
|
68
|
+
|
|
69
|
+
Las herramientas de autoría en batch vía WAAPI son potentes y las de terceros están mayormente en estado experimental. Probar en un proyecto descartable; el proyecto real no es el lugar.
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: midi-compose
|
|
3
|
+
description: "Genera archivos MIDI (.mid) desde un spec JSON con scripts Python stdlib-only. Sin dependencias: escribe el Standard MIDI File binario directo. Usar para: exportar una composicion a MIDI, bocetar stems, generar loops o capas para middleware."
|
|
4
|
+
category: "audio"
|
|
5
|
+
argument-hint: "[spec-path | new | validate]"
|
|
6
|
+
user-invocable: true
|
|
7
|
+
allowed-tools: Read, Glob, Grep, Write, Edit, Bash
|
|
8
|
+
---
|
|
9
|
+
# midi-compose — Composición a MIDI
|
|
10
|
+
|
|
11
|
+
Baja decisiones musicales a un `.mid` real. Las decisiones las toma `/music-composition`; esta skill las escribe.
|
|
12
|
+
|
|
13
|
+
**Sin dependencias.** El Standard MIDI File es un formato binario documentado y `scripts/compose_midi.py` lo emite con la stdlib de Python. No hay que instalar `midiutil`, `music21` ni nada. Corre en la máquina del usuario tal como se copia.
|
|
14
|
+
|
|
15
|
+
## Alcance
|
|
16
|
+
|
|
17
|
+
**Sí:** escribir `.mid` multipista con tempo, compás, tonalidad, programas General MIDI, velocities y percusión.
|
|
18
|
+
|
|
19
|
+
**No:** renderizar a audio (requiere FluidSynth + soundfonts, dependencias de sistema fuera de scope), tomar decisiones musicales (→ `/music-composition`), notación.
|
|
20
|
+
|
|
21
|
+
## Cuándo usar
|
|
22
|
+
|
|
23
|
+
- Ya hay material decidido (progresión, melodía, groove) y hace falta el archivo
|
|
24
|
+
- Bocetar stems para un sistema de capas de `/adaptive-music`
|
|
25
|
+
- Generar variantes de un tema por transformación (aumentación, cambio de modo)
|
|
26
|
+
- Entregar MIDI a un compositor o importarlo a un DAW/middleware
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Modos
|
|
31
|
+
|
|
32
|
+
| Modo | Qué hace |
|
|
33
|
+
|---|---|
|
|
34
|
+
| `spec-path` | Genera el `.mid` desde un spec JSON existente |
|
|
35
|
+
| `new` | Arma el spec desde cero a partir de material musical, y después genera |
|
|
36
|
+
| `validate` | Solo valida el spec y reporta, sin escribir nada |
|
|
37
|
+
|
|
38
|
+
## Workflow
|
|
39
|
+
|
|
40
|
+
### 1. Armar el spec
|
|
41
|
+
|
|
42
|
+
Un objeto JSON. Todos los tiempos están en **beats** (negras), no en ticks — el script convierte.
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"name": "Explore Calm",
|
|
47
|
+
"tempo": 84,
|
|
48
|
+
"ppq": 480,
|
|
49
|
+
"time_signature": [4, 4],
|
|
50
|
+
"key": "D",
|
|
51
|
+
"mode": "minor",
|
|
52
|
+
"tracks": [
|
|
53
|
+
{
|
|
54
|
+
"name": "Bed (strings)",
|
|
55
|
+
"channel": 0,
|
|
56
|
+
"program": 48,
|
|
57
|
+
"notes": [
|
|
58
|
+
{ "pitch": "D3", "start": 0, "duration": 4, "velocity": 62 }
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Hay un ejemplo completo de tres pistas en `templates/example-composition.json`.
|
|
66
|
+
|
|
67
|
+
**Campos del spec**
|
|
68
|
+
|
|
69
|
+
| Campo | Default | Notas |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| `name` | `"untitled"` | Va al meta de nombre de secuencia |
|
|
72
|
+
| `tempo` | `120` | BPM, 1-600 |
|
|
73
|
+
| `ppq` | `480` | Ticks por negra, 24-32767 |
|
|
74
|
+
| `time_signature` | `[4, 4]` | Denominador potencia de 2 |
|
|
75
|
+
| `key` / `mode` | `"C"` / `"major"` | Solo para el meta de tonalidad; no transpone |
|
|
76
|
+
| `tracks[].channel` | índice del track | 0-15, único por track |
|
|
77
|
+
| `tracks[].program` | `0` | Programa General MIDI 0-127 |
|
|
78
|
+
| `tracks[].drums` | `false` | Declaralo en el track del canal 9 |
|
|
79
|
+
| `notes[].pitch` | — | `60`, `"C4"`, `"F#3"`, `"Bb5"`. C4 = 60 |
|
|
80
|
+
| `notes[].start` | `0` | En beats desde el inicio |
|
|
81
|
+
| `notes[].duration` | `1` | En beats, > 0 |
|
|
82
|
+
| `notes[].velocity` | `90` | 1-127 |
|
|
83
|
+
|
|
84
|
+
### 2. Validar antes de escribir
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
python skills/midi-compose/scripts/compose_midi.py spec.json --validate
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Salida `[PASS]` con el resumen, más `[WARN]` por cada problema no fatal. Un `[FAIL]` nombra el track y la nota exactos.
|
|
91
|
+
|
|
92
|
+
### 3. Generar
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
python skills/midi-compose/scripts/compose_midi.py spec.json -o salida.mid
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Antes de escribir el `.mid` o el spec a disco, preguntar al usuario "¿Escribo esto a `<path>`?" — sobre todo si el archivo ya existe.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Límites de MIDI que importan
|
|
103
|
+
|
|
104
|
+
- **16 canales.** El canal 9 (10 en base 1) está reservado a percusión por General MIDI, así que quedan **15 pistas melódicas**. Si necesitás más, son varios archivos.
|
|
105
|
+
- **Un programa por canal a la vez.** Cambiar de instrumento a mitad de pista pide otro canal.
|
|
106
|
+
- **Velocity no es volumen.** Es intensidad de ataque; el sampler decide qué hacer con ella.
|
|
107
|
+
- **MIDI no lleva timbre.** El programa GM es una sugerencia; cómo suena depende del soundfont o del instrumento del DAW.
|
|
108
|
+
- **`pitch` en el canal de percusión es el instrumento**, no una altura: 36 = bombo, 38 = redoblante, 42 = hi-hat cerrado, 46 = hi-hat abierto.
|
|
109
|
+
|
|
110
|
+
## Feel y microtiming
|
|
111
|
+
|
|
112
|
+
El spec acepta `start` fraccional, así que el feel se escribe directo: para atrasar un backbeat 15 ms a 84 BPM, `15 / (60000 / 84 / 1) ≈ 0.021` beats. Un desvío de 10-30 ms es feel; arriba de ~50 ms se percibe como error. Ver `references/smf-format.md` para la relación tick/ms.
|
|
113
|
+
|
|
114
|
+
## Formato
|
|
115
|
+
|
|
116
|
+
> → Read references/smf-format.md para la estructura binaria del SMF, el encoding VLQ y la tabla de programas GM más usados
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Verdict
|
|
121
|
+
|
|
122
|
+
- **COMPLETE** — el `.mid` se escribió y la verificación de estructura pasa
|
|
123
|
+
- **CONCERNS** — se escribió pero hay `[WARN]`: contenido melódico en el canal de percusión, o pistas cerca del límite de 15
|
|
124
|
+
- **FAIL** — el spec no valida; el mensaje nombra el track y la nota
|
|
125
|
+
|
|
126
|
+
Verificación rápida de que el archivo es un SMF:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
python -c "print(open('salida.mid','rb').read(4) == b'MThd')" # True
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Next steps
|
|
133
|
+
|
|
134
|
+
- `/music-composition` si el material musical todavía no está decidido
|
|
135
|
+
- `/adaptive-music` para armar el sistema de capas que consume estos stems
|
|
136
|
+
- `/middleware-integration` para importar los segmentos a Wwise/FMOD
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Standard MIDI File — formato binario
|
|
2
|
+
|
|
3
|
+
Referencia de lo que emite `scripts/compose_midi.py`. Útil para debuggear un `.mid` a mano o extender el script.
|
|
4
|
+
|
|
5
|
+
## Estructura general
|
|
6
|
+
|
|
7
|
+
Un SMF es una secuencia de chunks. Cada chunk: 4 bytes de tipo + 4 bytes de longitud (big-endian) + cuerpo.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
MThd <len=6> <format:2> <ntrks:2> <division:2>
|
|
11
|
+
MTrk <len> <eventos...>
|
|
12
|
+
MTrk <len> <eventos...>
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
**Formatos:**
|
|
16
|
+
|
|
17
|
+
| Formato | Qué es |
|
|
18
|
+
|---|---|
|
|
19
|
+
| 0 | Un solo track con todo mezclado |
|
|
20
|
+
| 1 | Múltiples tracks simultáneos; el track 0 es el conductor (tempo/compás) |
|
|
21
|
+
| 2 | Tracks independientes, secuenciales. Casi no se usa |
|
|
22
|
+
|
|
23
|
+
El script emite **formato 1**: track 0 con tempo, compás y tonalidad sin notas, y un `MTrk` por pista musical. Es lo que esperan los DAWs.
|
|
24
|
+
|
|
25
|
+
**División** — con el bit alto en 0, es *ticks por negra* (PPQ). 480 es el valor estándar de la industria y divide bien tresillos (160) y semicorcheas (120).
|
|
26
|
+
|
|
27
|
+
## Delta-times y VLQ
|
|
28
|
+
|
|
29
|
+
Cada evento arranca con un delta-time: cuántos ticks pasaron desde el evento anterior *en ese track*. Se codifica como Variable-Length Quantity: 7 bits de datos por byte, MSB = 1 en todos los bytes menos el último.
|
|
30
|
+
|
|
31
|
+
| Valor | Bytes |
|
|
32
|
+
|---|---|
|
|
33
|
+
| 0 | `00` |
|
|
34
|
+
| 127 | `7F` |
|
|
35
|
+
| 128 | `81 00` |
|
|
36
|
+
| 480 | `83 60` |
|
|
37
|
+
| 8192 | `C0 00` |
|
|
38
|
+
|
|
39
|
+
Un delta mal codificado desplaza todo lo que sigue: es la causa número uno de un `.mid` que "suena raro a partir de la mitad".
|
|
40
|
+
|
|
41
|
+
## Eventos de canal
|
|
42
|
+
|
|
43
|
+
`n` es el canal, 0-15.
|
|
44
|
+
|
|
45
|
+
| Evento | Bytes | Notas |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| Note Off | `8n kk vv` | `kk` nota, `vv` velocity de release |
|
|
48
|
+
| Note On | `9n kk vv` | `vv = 0` equivale a Note Off |
|
|
49
|
+
| Control Change | `Bn cc vv` | cc 7 = volumen, 10 = pan, 11 = expresión |
|
|
50
|
+
| Program Change | `Cn pp` | Instrumento GM |
|
|
51
|
+
| Pitch Bend | `En ll mm` | 14 bits, centro en `00 40` |
|
|
52
|
+
|
|
53
|
+
**Orden en el mismo tick:** el Note Off tiene que ir antes del Note On cuando se reataca la misma nota, o el sintetizador corta la nota nueva. El script ordena por `(tick, 0=off, 1=on)` justamente por esto.
|
|
54
|
+
|
|
55
|
+
## Meta eventos
|
|
56
|
+
|
|
57
|
+
Todos arrancan con `FF`, después tipo, después longitud en VLQ.
|
|
58
|
+
|
|
59
|
+
| Meta | Bytes | Contenido |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| Nombre de secuencia/track | `FF 03 <len> <texto>` | |
|
|
62
|
+
| Tempo | `FF 51 03 tttttt` | Microsegundos por negra = `60000000 / BPM` |
|
|
63
|
+
| Compás | `FF 58 04 nn dd cc bb` | `dd` = log2(denominador); `cc` = 24; `bb` = 8 |
|
|
64
|
+
| Tonalidad | `FF 59 02 sf mi` | `sf` sostenidos (+) o bemoles (−), signed; `mi` 0 mayor / 1 menor |
|
|
65
|
+
| End of Track | `FF 2F 00` | **Obligatorio** al final de cada `MTrk` |
|
|
66
|
+
|
|
67
|
+
Sin `End of Track` muchos lectores rechazan el archivo entero.
|
|
68
|
+
|
|
69
|
+
## Conversión de tiempo
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
ticks = beats × PPQ
|
|
73
|
+
ms_por_tick = 60000 / (BPM × PPQ)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
A 120 BPM con PPQ 480: 1 tick = 1.041 ms. A 84 BPM: 1 tick = 1.488 ms.
|
|
77
|
+
|
|
78
|
+
Para escribir un desvío de feel en beats: `beats = ms × BPM / 60000`.
|
|
79
|
+
|
|
80
|
+
## Programas General MIDI usados seguido
|
|
81
|
+
|
|
82
|
+
| # | Instrumento | # | Instrumento |
|
|
83
|
+
|---|---|---|---|
|
|
84
|
+
| 0 | Acoustic Grand Piano | 48 | String Ensemble 1 |
|
|
85
|
+
| 4 | Electric Piano 1 | 49 | String Ensemble 2 |
|
|
86
|
+
| 11 | Vibraphone | 52 | Choir Aahs |
|
|
87
|
+
| 19 | Church Organ | 56 | Trumpet |
|
|
88
|
+
| 24 | Nylon Guitar | 57 | Trombone |
|
|
89
|
+
| 25 | Steel Guitar | 60 | French Horn |
|
|
90
|
+
| 32 | Acoustic Bass | 68 | Oboe |
|
|
91
|
+
| 33 | Finger Bass | 71 | Clarinet |
|
|
92
|
+
| 40 | Violin | 73 | Flute |
|
|
93
|
+
| 42 | Cello | 89 | Pad 2 (warm) |
|
|
94
|
+
| 46 | Orchestral Harp | 94 | Pad 7 (halo) |
|
|
95
|
+
|
|
96
|
+
## Percusión (canal 9)
|
|
97
|
+
|
|
98
|
+
El `pitch` es el instrumento, no la altura.
|
|
99
|
+
|
|
100
|
+
| # | Instrumento | # | Instrumento |
|
|
101
|
+
|---|---|---|---|
|
|
102
|
+
| 35 | Acoustic Bass Drum | 44 | Pedal Hi-Hat |
|
|
103
|
+
| 36 | Bass Drum 1 | 46 | Open Hi-Hat |
|
|
104
|
+
| 38 | Acoustic Snare | 49 | Crash Cymbal 1 |
|
|
105
|
+
| 39 | Hand Clap | 51 | Ride Cymbal 1 |
|
|
106
|
+
| 40 | Electric Snare | 54 | Tambourine |
|
|
107
|
+
| 41 | Low Floor Tom | 56 | Cowbell |
|
|
108
|
+
| 42 | Closed Hi-Hat | 76 | Hi Wood Block |
|
|
109
|
+
|
|
110
|
+
## Verificar un .mid a mano
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
python - <<'EOF'
|
|
114
|
+
import struct
|
|
115
|
+
data = open("salida.mid", "rb").read()
|
|
116
|
+
assert data[:4] == b"MThd"
|
|
117
|
+
length, fmt, ntrks, div = struct.unpack(">IHHH", data[4:14])
|
|
118
|
+
print("format", fmt, "| tracks", ntrks, "| ppq", div)
|
|
119
|
+
pos, seen = 14, 0
|
|
120
|
+
while pos < len(data):
|
|
121
|
+
name, size = data[pos:pos+4], struct.unpack(">I", data[pos+4:pos+8])[0]
|
|
122
|
+
body = data[pos+8:pos+8+size]
|
|
123
|
+
assert name == b"MTrk" and body[-3:] == b"\xff\x2f\x00", f"track {seen} malformado"
|
|
124
|
+
pos += 8 + size
|
|
125
|
+
seen += 1
|
|
126
|
+
assert seen == ntrks and pos == len(data)
|
|
127
|
+
print("SMF valido")
|
|
128
|
+
EOF
|
|
129
|
+
```
|