claude-code-arcane 2.1.0 → 2.3.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 +29 -0
- package/README.md +57 -55
- 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/agents/career/career-strategist.md +88 -0
- package/agents/career/cv-reviewer.md +81 -0
- package/agents/career/hiring-manager.md +68 -0
- package/agents/career/mock-interviewer.md +76 -0
- package/agents/freelance/client-screener.md +84 -0
- package/agents/freelance/discovery-call.md +65 -0
- package/agents/freelance/pipeline-strategist.md +99 -0
- package/agents/freelance/proposal-reviewer.md +82 -0
- package/dist/cli.js +52 -13
- package/docs/RELEASE-SETUP.md +1 -1
- package/docs/SKILLS-CATALOG.md +47 -3
- package/docs/USER-GUIDE.md +77 -72
- package/docs/agent-hierarchy.md +21 -1
- package/docs/presentations/build_arcane_deck.py +2 -2
- package/docs/presentations/build_arcane_deck_12.py +2 -2
- package/package.json +2 -2
- package/profiles/audio.yaml +45 -0
- package/profiles/freelance.yaml +43 -0
- package/profiles/job-hunt.yaml +5 -2
- package/rules/drafter-reviewer.md +16 -3
- package/rules/freelance-guardrails.md +53 -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/career-registry/SKILL.md +95 -0
- package/skills/career-registry/scripts/career_registry.py +488 -0
- package/skills/client-screen/SKILL.md +83 -0
- package/skills/client-screen/references/red-flags.md +95 -0
- package/skills/cover-letter/SKILL.md +4 -3
- package/skills/cv-tailor/SKILL.md +11 -2
- package/skills/freelance-deliver/SKILL.md +90 -0
- package/skills/freelance-hunt/SKILL.md +126 -0
- package/skills/freelance-hunt/references/templates/Connects.md +39 -0
- package/skills/freelance-hunt/references/templates/Contrato.md +60 -0
- package/skills/freelance-hunt/references/templates/Propuesta.md +66 -0
- package/skills/freelance-kickoff/SKILL.md +94 -0
- package/skills/freelance-pipeline/SKILL.md +109 -0
- package/skills/freelance-pipeline/scripts/import_history.py +425 -0
- package/skills/freelance-profile/SKILL.md +93 -0
- package/skills/freelance-proposal/SKILL.md +112 -0
- package/skills/freelance-scan/SKILL.md +196 -0
- package/skills/freelance-scan/references/dedup.md +35 -0
- package/skills/freelance-scan/references/platforms.md +167 -0
- package/skills/freelance-scan/scripts/freelance_search.py +1077 -0
- package/skills/interview-prep/SKILL.md +11 -3
- package/skills/job-aplicar/SKILL.md +5 -1
- package/skills/job-hunt/SKILL.md +17 -0
- package/skills/job-outcome/SKILL.md +15 -1
- package/skills/job-upskill/SKILL.md +10 -2
- 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,326 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Escribe un Standard MIDI File (formato 1) desde un spec JSON.
|
|
3
|
+
|
|
4
|
+
Solo stdlib: el SMF es un formato binario documentado y no hace falta
|
|
5
|
+
ninguna dependencia para emitirlo. Ver references/smf-format.md.
|
|
6
|
+
|
|
7
|
+
Uso:
|
|
8
|
+
python compose_midi.py spec.json -o salida.mid
|
|
9
|
+
python compose_midi.py spec.json --validate # solo valida, no escribe
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import json
|
|
16
|
+
import struct
|
|
17
|
+
import sys
|
|
18
|
+
|
|
19
|
+
# ---------------------------------------------------------------- constantes
|
|
20
|
+
|
|
21
|
+
DRUM_CHANNEL = 9 # canal 10 en base 1; reservado a percusion por GM
|
|
22
|
+
MAX_CHANNELS = 16 # el limite duro: 15 tracks melodicos + percusion
|
|
23
|
+
DEFAULT_PPQ = 480
|
|
24
|
+
|
|
25
|
+
NOTE_OFFSETS = {
|
|
26
|
+
"C": 0, "D": 2, "E": 4, "F": 5, "G": 7, "A": 9, "B": 11,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
# sf para el meta de tonalidad: sostenidos positivos, bemoles negativos
|
|
30
|
+
KEY_SIGNATURES = {
|
|
31
|
+
"C": 0, "G": 1, "D": 2, "A": 3, "E": 4, "B": 5, "F#": 6, "C#": 7,
|
|
32
|
+
"F": -1, "Bb": -2, "Eb": -3, "Ab": -4, "Db": -5, "Gb": -6, "Cb": -7,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class SpecError(ValueError):
|
|
37
|
+
"""El spec JSON es invalido. El mensaje explica que arreglar."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# ------------------------------------------------------------------ helpers
|
|
41
|
+
|
|
42
|
+
def vlq(value: int) -> bytes:
|
|
43
|
+
"""Variable-length quantity: el encoding de delta-times del SMF.
|
|
44
|
+
|
|
45
|
+
Siete bits por byte, MSB=1 en todos menos el ultimo.
|
|
46
|
+
"""
|
|
47
|
+
if value < 0:
|
|
48
|
+
raise SpecError(f"delta-time negativo: {value}")
|
|
49
|
+
out = [value & 0x7F]
|
|
50
|
+
value >>= 7
|
|
51
|
+
while value:
|
|
52
|
+
out.append((value & 0x7F) | 0x80)
|
|
53
|
+
value >>= 7
|
|
54
|
+
return bytes(reversed(out))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def parse_pitch(pitch: object) -> int:
|
|
58
|
+
"""Acepta 60, "C4", "F#3", "Bb5". Devuelve numero de nota MIDI (C4 = 60)."""
|
|
59
|
+
if isinstance(pitch, bool):
|
|
60
|
+
raise SpecError(f"pitch invalido: {pitch!r}")
|
|
61
|
+
if isinstance(pitch, int):
|
|
62
|
+
if not 0 <= pitch <= 127:
|
|
63
|
+
raise SpecError(f"pitch fuera de rango 0-127: {pitch}")
|
|
64
|
+
return pitch
|
|
65
|
+
if not isinstance(pitch, str) or not pitch:
|
|
66
|
+
raise SpecError(f"pitch invalido: {pitch!r}")
|
|
67
|
+
|
|
68
|
+
text = pitch.strip()
|
|
69
|
+
letter = text[0].upper()
|
|
70
|
+
if letter not in NOTE_OFFSETS:
|
|
71
|
+
raise SpecError(f"nota desconocida en pitch {pitch!r}")
|
|
72
|
+
|
|
73
|
+
index = 1
|
|
74
|
+
accidental = 0
|
|
75
|
+
while index < len(text) and text[index] in "#b":
|
|
76
|
+
accidental += 1 if text[index] == "#" else -1
|
|
77
|
+
index += 1
|
|
78
|
+
|
|
79
|
+
octave_text = text[index:]
|
|
80
|
+
if not octave_text.lstrip("-").isdigit():
|
|
81
|
+
raise SpecError(f"falta la octava en pitch {pitch!r} (ej: C4, F#3, Bb5)")
|
|
82
|
+
|
|
83
|
+
# C4 = 60 => la octava -1 arranca en 0
|
|
84
|
+
value = (int(octave_text) + 1) * 12 + NOTE_OFFSETS[letter] + accidental
|
|
85
|
+
if not 0 <= value <= 127:
|
|
86
|
+
raise SpecError(f"pitch {pitch!r} cae fuera del rango MIDI 0-127")
|
|
87
|
+
return value
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def meta(kind: int, payload: bytes) -> bytes:
|
|
91
|
+
return b"\xff" + bytes([kind]) + vlq(len(payload)) + payload
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def text_meta(kind: int, text: str) -> bytes:
|
|
95
|
+
return meta(kind, text.encode("utf-8", errors="replace"))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# ------------------------------------------------------------------ validate
|
|
99
|
+
|
|
100
|
+
def validate(spec: dict) -> dict:
|
|
101
|
+
"""Normaliza y valida el spec. Devuelve el spec normalizado."""
|
|
102
|
+
if not isinstance(spec, dict):
|
|
103
|
+
raise SpecError("el spec raiz tiene que ser un objeto JSON")
|
|
104
|
+
|
|
105
|
+
tempo = spec.get("tempo", 120)
|
|
106
|
+
if not isinstance(tempo, (int, float)) or not 1 <= tempo <= 600:
|
|
107
|
+
raise SpecError(f"tempo tiene que ser un numero entre 1 y 600, no {tempo!r}")
|
|
108
|
+
|
|
109
|
+
ppq = spec.get("ppq", DEFAULT_PPQ)
|
|
110
|
+
if not isinstance(ppq, int) or not 24 <= ppq <= 32767:
|
|
111
|
+
raise SpecError(f"ppq tiene que ser un entero entre 24 y 32767, no {ppq!r}")
|
|
112
|
+
|
|
113
|
+
signature = spec.get("time_signature", [4, 4])
|
|
114
|
+
if (not isinstance(signature, (list, tuple)) or len(signature) != 2
|
|
115
|
+
or not all(isinstance(n, int) for n in signature)):
|
|
116
|
+
raise SpecError('time_signature tiene que ser [numerador, denominador], ej [4, 4]')
|
|
117
|
+
numerator, denominator = signature
|
|
118
|
+
if not 1 <= numerator <= 255:
|
|
119
|
+
raise SpecError(f"numerador de compas invalido: {numerator}")
|
|
120
|
+
if denominator not in (1, 2, 4, 8, 16, 32, 64):
|
|
121
|
+
raise SpecError(f"denominador de compas invalido: {denominator}")
|
|
122
|
+
|
|
123
|
+
key = spec.get("key", "C")
|
|
124
|
+
mode = spec.get("mode", "major")
|
|
125
|
+
if key not in KEY_SIGNATURES:
|
|
126
|
+
raise SpecError(
|
|
127
|
+
f"tonalidad {key!r} desconocida. Validas: {', '.join(sorted(KEY_SIGNATURES))}"
|
|
128
|
+
)
|
|
129
|
+
if mode not in ("major", "minor"):
|
|
130
|
+
raise SpecError(f'mode tiene que ser "major" o "minor", no {mode!r}')
|
|
131
|
+
|
|
132
|
+
tracks = spec.get("tracks")
|
|
133
|
+
if not isinstance(tracks, list) or not tracks:
|
|
134
|
+
raise SpecError("el spec necesita al menos un track en 'tracks'")
|
|
135
|
+
|
|
136
|
+
melodic = 0
|
|
137
|
+
used_channels: set[int] = set()
|
|
138
|
+
normalized_tracks = []
|
|
139
|
+
warnings: list[str] = []
|
|
140
|
+
|
|
141
|
+
for position, track in enumerate(tracks):
|
|
142
|
+
label = track.get("name") if isinstance(track, dict) else None
|
|
143
|
+
where = f"track {position}" + (f" ({label})" if label else "")
|
|
144
|
+
|
|
145
|
+
if not isinstance(track, dict):
|
|
146
|
+
raise SpecError(f"{where}: cada track tiene que ser un objeto")
|
|
147
|
+
|
|
148
|
+
channel = track.get("channel", DRUM_CHANNEL if track.get("drums") else position)
|
|
149
|
+
if not isinstance(channel, int) or not 0 <= channel < MAX_CHANNELS:
|
|
150
|
+
raise SpecError(f"{where}: channel tiene que estar entre 0 y 15, no {channel!r}")
|
|
151
|
+
if channel in used_channels:
|
|
152
|
+
raise SpecError(f"{where}: el canal {channel} ya lo usa otro track")
|
|
153
|
+
used_channels.add(channel)
|
|
154
|
+
|
|
155
|
+
if channel == DRUM_CHANNEL:
|
|
156
|
+
if not track.get("drums"):
|
|
157
|
+
warnings.append(
|
|
158
|
+
f"{where}: usa el canal {DRUM_CHANNEL}, reservado a percusion por GM. "
|
|
159
|
+
f'Si es intencional agregale "drums": true; si no, movelo a otro canal.'
|
|
160
|
+
)
|
|
161
|
+
else:
|
|
162
|
+
melodic += 1
|
|
163
|
+
|
|
164
|
+
program = track.get("program", 0)
|
|
165
|
+
if not isinstance(program, int) or not 0 <= program <= 127:
|
|
166
|
+
raise SpecError(f"{where}: program tiene que estar entre 0 y 127, no {program!r}")
|
|
167
|
+
|
|
168
|
+
notes = track.get("notes")
|
|
169
|
+
if not isinstance(notes, list):
|
|
170
|
+
raise SpecError(f"{where}: 'notes' tiene que ser una lista")
|
|
171
|
+
|
|
172
|
+
normalized_notes = []
|
|
173
|
+
for note_position, note in enumerate(notes):
|
|
174
|
+
note_where = f"{where}, nota {note_position}"
|
|
175
|
+
if not isinstance(note, dict):
|
|
176
|
+
raise SpecError(f"{note_where}: cada nota tiene que ser un objeto")
|
|
177
|
+
|
|
178
|
+
pitch = parse_pitch(note.get("pitch"))
|
|
179
|
+
|
|
180
|
+
start = note.get("start", 0)
|
|
181
|
+
if not isinstance(start, (int, float)) or start < 0:
|
|
182
|
+
raise SpecError(f"{note_where}: 'start' tiene que ser >= 0, no {start!r}")
|
|
183
|
+
|
|
184
|
+
duration = note.get("duration", 1)
|
|
185
|
+
if not isinstance(duration, (int, float)) or duration <= 0:
|
|
186
|
+
raise SpecError(f"{note_where}: 'duration' tiene que ser > 0, no {duration!r}")
|
|
187
|
+
|
|
188
|
+
velocity = note.get("velocity", 90)
|
|
189
|
+
if not isinstance(velocity, int) or not 1 <= velocity <= 127:
|
|
190
|
+
raise SpecError(
|
|
191
|
+
f"{note_where}: 'velocity' tiene que estar entre 1 y 127, no {velocity!r}"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
normalized_notes.append(
|
|
195
|
+
{"pitch": pitch, "start": float(start),
|
|
196
|
+
"duration": float(duration), "velocity": velocity}
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
normalized_tracks.append({
|
|
200
|
+
"name": track.get("name", f"Track {position}"),
|
|
201
|
+
"channel": channel,
|
|
202
|
+
"program": program,
|
|
203
|
+
"notes": normalized_notes,
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
"name": spec.get("name", "untitled"),
|
|
208
|
+
"tempo": float(tempo),
|
|
209
|
+
"ppq": ppq,
|
|
210
|
+
"time_signature": [numerator, denominator],
|
|
211
|
+
"key": key,
|
|
212
|
+
"mode": mode,
|
|
213
|
+
"tracks": normalized_tracks,
|
|
214
|
+
"melodic_tracks": melodic,
|
|
215
|
+
"warnings": warnings,
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# --------------------------------------------------------------------- build
|
|
220
|
+
|
|
221
|
+
def build_chunk(name: bytes, body: bytes) -> bytes:
|
|
222
|
+
return name + struct.pack(">I", len(body)) + body
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def conductor_track(spec: dict) -> bytes:
|
|
226
|
+
"""Track 0 del formato 1: tempo, compas, tonalidad. Sin notas."""
|
|
227
|
+
events = [b"\x00" + text_meta(0x03, spec["name"])]
|
|
228
|
+
|
|
229
|
+
microseconds = int(round(60_000_000 / spec["tempo"]))
|
|
230
|
+
events.append(b"\x00" + meta(0x51, struct.pack(">I", microseconds)[1:]))
|
|
231
|
+
|
|
232
|
+
numerator, denominator = spec["time_signature"]
|
|
233
|
+
events.append(
|
|
234
|
+
b"\x00" + meta(0x58, bytes([numerator, denominator.bit_length() - 1, 24, 8]))
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
sharps = KEY_SIGNATURES[spec["key"]]
|
|
238
|
+
events.append(
|
|
239
|
+
b"\x00" + meta(0x59, struct.pack(">bB", sharps, 1 if spec["mode"] == "minor" else 0))
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
events.append(b"\x00" + meta(0x2F, b""))
|
|
243
|
+
return build_chunk(b"MTrk", b"".join(events))
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def music_track(track: dict, ppq: int) -> bytes:
|
|
247
|
+
channel = track["channel"]
|
|
248
|
+
events = [
|
|
249
|
+
b"\x00" + text_meta(0x03, track["name"]),
|
|
250
|
+
b"\x00" + bytes([0xC0 | channel, track["program"]]),
|
|
251
|
+
]
|
|
252
|
+
|
|
253
|
+
# (tick, orden, bytes). orden 0 = note-off primero, para que una nota
|
|
254
|
+
# reatacada en el mismo tick no se corte a si misma.
|
|
255
|
+
timeline: list[tuple[int, int, bytes]] = []
|
|
256
|
+
for note in track["notes"]:
|
|
257
|
+
start = int(round(note["start"] * ppq))
|
|
258
|
+
end = start + max(1, int(round(note["duration"] * ppq)))
|
|
259
|
+
timeline.append((start, 1, bytes([0x90 | channel, note["pitch"], note["velocity"]])))
|
|
260
|
+
timeline.append((end, 0, bytes([0x80 | channel, note["pitch"], 0])))
|
|
261
|
+
|
|
262
|
+
timeline.sort(key=lambda item: (item[0], item[1]))
|
|
263
|
+
|
|
264
|
+
previous = 0
|
|
265
|
+
for tick, _order, payload in timeline:
|
|
266
|
+
events.append(vlq(tick - previous) + payload)
|
|
267
|
+
previous = tick
|
|
268
|
+
|
|
269
|
+
events.append(b"\x00" + meta(0x2F, b""))
|
|
270
|
+
return build_chunk(b"MTrk", b"".join(events))
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def build_midi(spec: dict) -> bytes:
|
|
274
|
+
ppq = spec["ppq"]
|
|
275
|
+
tracks = [conductor_track(spec)] + [music_track(t, ppq) for t in spec["tracks"]]
|
|
276
|
+
header = build_chunk(b"MThd", struct.pack(">HHH", 1, len(tracks), ppq))
|
|
277
|
+
return header + b"".join(tracks)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
# ----------------------------------------------------------------------- cli
|
|
281
|
+
|
|
282
|
+
def main(argv: list[str] | None = None) -> int:
|
|
283
|
+
parser = argparse.ArgumentParser(description="Escribe un .mid desde un spec JSON.")
|
|
284
|
+
parser.add_argument("spec", help="ruta al spec JSON")
|
|
285
|
+
parser.add_argument("-o", "--output", help="ruta del .mid de salida")
|
|
286
|
+
parser.add_argument("--validate", action="store_true",
|
|
287
|
+
help="solo valida el spec, no escribe nada")
|
|
288
|
+
args = parser.parse_args(argv)
|
|
289
|
+
|
|
290
|
+
try:
|
|
291
|
+
with open(args.spec, "r", encoding="utf-8") as handle:
|
|
292
|
+
raw = json.load(handle)
|
|
293
|
+
except FileNotFoundError:
|
|
294
|
+
print(f"[FAIL] no existe el spec: {args.spec}", file=sys.stderr)
|
|
295
|
+
return 1
|
|
296
|
+
except json.JSONDecodeError as error:
|
|
297
|
+
print(f"[FAIL] JSON invalido en {args.spec}: {error}", file=sys.stderr)
|
|
298
|
+
return 1
|
|
299
|
+
|
|
300
|
+
try:
|
|
301
|
+
spec = validate(raw)
|
|
302
|
+
except SpecError as error:
|
|
303
|
+
print(f"[FAIL] {error}", file=sys.stderr)
|
|
304
|
+
return 1
|
|
305
|
+
|
|
306
|
+
note_count = sum(len(t["notes"]) for t in spec["tracks"])
|
|
307
|
+
print(f"[PASS] spec valido: {len(spec['tracks'])} tracks "
|
|
308
|
+
f"({spec['melodic_tracks']} melodicos de 15 posibles), {note_count} notas, "
|
|
309
|
+
f"{spec['tempo']:g} BPM, {spec['key']} {spec['mode']}")
|
|
310
|
+
for warning in spec["warnings"]:
|
|
311
|
+
print(f"[WARN] {warning}")
|
|
312
|
+
|
|
313
|
+
if args.validate:
|
|
314
|
+
return 0
|
|
315
|
+
|
|
316
|
+
output = args.output or (args.spec.rsplit(".", 1)[0] + ".mid")
|
|
317
|
+
data = build_midi(spec)
|
|
318
|
+
with open(output, "wb") as handle:
|
|
319
|
+
handle.write(data)
|
|
320
|
+
|
|
321
|
+
print(f"[COMPLETE] {output} ({len(data)} bytes)")
|
|
322
|
+
return 0
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
if __name__ == "__main__":
|
|
326
|
+
sys.exit(main())
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Explore Calm — bed + pulse",
|
|
3
|
+
"tempo": 84,
|
|
4
|
+
"ppq": 480,
|
|
5
|
+
"time_signature": [4, 4],
|
|
6
|
+
"key": "D",
|
|
7
|
+
"mode": "minor",
|
|
8
|
+
"tracks": [
|
|
9
|
+
{
|
|
10
|
+
"name": "Bed (strings)",
|
|
11
|
+
"channel": 0,
|
|
12
|
+
"program": 48,
|
|
13
|
+
"notes": [
|
|
14
|
+
{ "pitch": "D3", "start": 0, "duration": 4, "velocity": 62 },
|
|
15
|
+
{ "pitch": "A3", "start": 0, "duration": 4, "velocity": 58 },
|
|
16
|
+
{ "pitch": "F4", "start": 0, "duration": 4, "velocity": 54 },
|
|
17
|
+
|
|
18
|
+
{ "pitch": "Bb2", "start": 4, "duration": 4, "velocity": 62 },
|
|
19
|
+
{ "pitch": "F3", "start": 4, "duration": 4, "velocity": 58 },
|
|
20
|
+
{ "pitch": "D4", "start": 4, "duration": 4, "velocity": 54 },
|
|
21
|
+
|
|
22
|
+
{ "pitch": "C3", "start": 8, "duration": 4, "velocity": 62 },
|
|
23
|
+
{ "pitch": "G3", "start": 8, "duration": 4, "velocity": 58 },
|
|
24
|
+
{ "pitch": "E4", "start": 8, "duration": 4, "velocity": 54 },
|
|
25
|
+
|
|
26
|
+
{ "pitch": "A2", "start": 12, "duration": 4, "velocity": 60 },
|
|
27
|
+
{ "pitch": "E3", "start": 12, "duration": 4, "velocity": 56 },
|
|
28
|
+
{ "pitch": "C4", "start": 12, "duration": 4, "velocity": 52 }
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"name": "Melody (clarinet)",
|
|
33
|
+
"channel": 1,
|
|
34
|
+
"program": 71,
|
|
35
|
+
"notes": [
|
|
36
|
+
{ "pitch": "A4", "start": 0.0, "duration": 2.0, "velocity": 78 },
|
|
37
|
+
{ "pitch": "F4", "start": 2.0, "duration": 1.0, "velocity": 72 },
|
|
38
|
+
{ "pitch": "E4", "start": 3.0, "duration": 1.0, "velocity": 72 },
|
|
39
|
+
{ "pitch": "D4", "start": 4.0, "duration": 3.0, "velocity": 80 },
|
|
40
|
+
{ "pitch": "F4", "start": 7.0, "duration": 1.0, "velocity": 70 },
|
|
41
|
+
{ "pitch": "G4", "start": 8.0, "duration": 2.0, "velocity": 76 },
|
|
42
|
+
{ "pitch": "E4", "start": 10.0, "duration": 2.0, "velocity": 72 },
|
|
43
|
+
{ "pitch": "D4", "start": 12.0, "duration": 4.0, "velocity": 74 }
|
|
44
|
+
]
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"name": "Pulse (percussion)",
|
|
48
|
+
"channel": 9,
|
|
49
|
+
"drums": true,
|
|
50
|
+
"program": 0,
|
|
51
|
+
"notes": [
|
|
52
|
+
{ "pitch": 42, "start": 0.0, "duration": 0.25, "velocity": 54 },
|
|
53
|
+
{ "pitch": 42, "start": 1.0, "duration": 0.25, "velocity": 44 },
|
|
54
|
+
{ "pitch": 42, "start": 2.0, "duration": 0.25, "velocity": 54 },
|
|
55
|
+
{ "pitch": 42, "start": 3.0, "duration": 0.25, "velocity": 44 },
|
|
56
|
+
{ "pitch": 42, "start": 4.0, "duration": 0.25, "velocity": 54 },
|
|
57
|
+
{ "pitch": 42, "start": 5.0, "duration": 0.25, "velocity": 44 },
|
|
58
|
+
{ "pitch": 42, "start": 6.0, "duration": 0.25, "velocity": 54 },
|
|
59
|
+
{ "pitch": 42, "start": 7.0, "duration": 0.25, "velocity": 44 },
|
|
60
|
+
{ "pitch": 36, "start": 8.0, "duration": 0.5, "velocity": 66 },
|
|
61
|
+
{ "pitch": 42, "start": 9.0, "duration": 0.25, "velocity": 44 },
|
|
62
|
+
{ "pitch": 42, "start": 10.0, "duration": 0.25, "velocity": 54 },
|
|
63
|
+
{ "pitch": 42, "start": 11.0, "duration": 0.25, "velocity": 44 },
|
|
64
|
+
{ "pitch": 36, "start": 12.0, "duration": 0.5, "velocity": 66 },
|
|
65
|
+
{ "pitch": 42, "start": 14.0, "duration": 0.25, "velocity": 50 }
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: music-composition
|
|
3
|
+
description: "Composicion musical: armonia, melodia, forma, groove, orquestacion. Traduce problemas creativos vagos en movimientos compositivos concretos con cifrados, grados de escala y voicings. Usar para: progresiones, melodias, modulacion, arreglo, analisis musical. NO genera audio ni MIDI ni opera DAW."
|
|
4
|
+
category: "audio"
|
|
5
|
+
argument-hint: "[harmony|melody|form|groove|orchestration|analyze]"
|
|
6
|
+
user-invocable: true
|
|
7
|
+
allowed-tools: Read, Glob, Grep, Write, Edit
|
|
8
|
+
---
|
|
9
|
+
# music-composition — Decisiones Musicales
|
|
10
|
+
|
|
11
|
+
Resuelve problemas de composición con salida accionable: cifrados concretos, melodías en grados de escala, bocetos de groove, voicings específicos.
|
|
12
|
+
|
|
13
|
+
**La teoría es un mapa de efectos posibles, no una lista de mandamientos.** Cada técnica se presenta con *qué efecto produce* y *por qué*, para que la elección sea informada y no obediente.
|
|
14
|
+
|
|
15
|
+
## Alcance
|
|
16
|
+
|
|
17
|
+
**Sí:** armonía, melodía, contrapunto, ritmo/groove, forma, orquestación, idioma instrumental, análisis, songwriting.
|
|
18
|
+
|
|
19
|
+
**No:** generar archivos MIDI (→ `/midi-compose`), generar audio, operar DAW, mixing/mastering (→ `/audio-mix`), sound design (→ `/sfx-design`), notación en Sibelius/Dorico.
|
|
20
|
+
|
|
21
|
+
## Cuándo usar
|
|
22
|
+
|
|
23
|
+
- Problemas técnicos: "qué escala va sobre Cm7", "cómo modulo de C a Eb"
|
|
24
|
+
- Problemas de craft: "esta melodía no se pega", "el estribillo se siente flojo"
|
|
25
|
+
- Problemas vagos: "la transición es incómoda", "suena genérico" — traducirlos a movimientos compositivos es exactamente el trabajo de esta skill
|
|
26
|
+
- Música de juego: temas, variaciones, material que después se vuelve adaptativo
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Método
|
|
31
|
+
|
|
32
|
+
### 1. Traducir el problema
|
|
33
|
+
|
|
34
|
+
El usuario casi nunca llega con vocabulario técnico. Antes de proponer nada, convertir la queja en un diagnóstico:
|
|
35
|
+
|
|
36
|
+
| Queja | Diagnóstico probable | Dónde mirar |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| "El estribillo se siente flojo" | No hay contraste de registro/densidad/ritmo armónico con la estrofa | `references/form.md` |
|
|
39
|
+
| "Suena genérico" | Progresión y ritmo armónico predecibles, melodía sin perfil | `references/harmony.md`, `references/melody.md` |
|
|
40
|
+
| "No se me pega" | Falta un gesto repetible: motivo corto, salto identificable, ritmo asimétrico | `references/melody.md` |
|
|
41
|
+
| "La transición es incómoda" | Salto de tonalidad/textura sin preparación ni pivote | `references/harmony.md` |
|
|
42
|
+
| "Se cansa rápido" | Loop sin variación de material ni cambio de densidad | `references/game-music.md` |
|
|
43
|
+
| "Se siente barroso" | Voicings apilados en el mismo registro, doblajes innecesarios | `references/orchestration.md` |
|
|
44
|
+
|
|
45
|
+
### 2. Ofrecer opciones con trade-off
|
|
46
|
+
|
|
47
|
+
Nunca una sola respuesta. Dos o tres movimientos, cada uno con el efecto que produce y su costo:
|
|
48
|
+
|
|
49
|
+
> Para levantar el estribillo tenés tres palancas:
|
|
50
|
+
> - **Subir el ritmo armónico** (un acorde por compás → dos): más urgencia, pero pierde espacio para la melodía
|
|
51
|
+
> - **Abrir el registro** (melodía una 6ª arriba): más brillo y esfuerzo vocal percibido, pero quema el recurso si ya lo usaste
|
|
52
|
+
> - **Cambiar el bajo a inversiones** (I → I/3 → IV): más movimiento sin tocar la melodía, efecto sutil
|
|
53
|
+
|
|
54
|
+
### 3. Ser concreto
|
|
55
|
+
|
|
56
|
+
Salida útil, no descriptiva:
|
|
57
|
+
|
|
58
|
+
- **Armonía:** cifrado real — `| Cmaj7 | Am7 | Dm7 | G7sus4 G7 |`, no "una progresión menor melancólica"
|
|
59
|
+
- **Melodía:** grados de escala con ritmo — `5 - 3 - 2 - 1` (blanca, negra, negra, redonda), portable a cualquier tonalidad
|
|
60
|
+
- **Groove:** boceto en grilla de subdivisión, con dónde cae el acento y dónde se corre
|
|
61
|
+
- **Voicing:** notas exactas por registro — `bajo: C2 / piano: E3-G3-B3-D4 / cuerdas: G4-C5`
|
|
62
|
+
|
|
63
|
+
### 4. Validar
|
|
64
|
+
|
|
65
|
+
Antes de cerrar: ¿la propuesta resuelve el problema original o lo esquiva? ¿Es tocable por el instrumento/voz target? ¿Respeta las convenciones del género sin ser esclava de ellas?
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Referencias
|
|
70
|
+
|
|
71
|
+
Cargar solo la que aplica al problema:
|
|
72
|
+
|
|
73
|
+
| Área | Archivo |
|
|
74
|
+
|---|---|
|
|
75
|
+
| Armonía funcional, modal, cromática, modulación, voice leading, reharmonización | `references/harmony.md` |
|
|
76
|
+
| Construcción melódica, desarrollo motívico, estructura de frase | `references/melody.md` |
|
|
77
|
+
| Formas populares y clásicas, transiciones, narrativa formal | `references/form.md` |
|
|
78
|
+
| Groove, feel, métricas impares, polirritmia, recursos rítmicos | `references/rhythm-groove.md` |
|
|
79
|
+
| Rangos y carácter instrumental, densidad de arreglo, voicing y textura | `references/orchestration.md` |
|
|
80
|
+
| Música de juego: loops, variación, temas y transformación, escalas y modos por mood | `references/game-music.md` |
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Principios
|
|
85
|
+
|
|
86
|
+
- Explicar **por qué** una técnica produce su efecto, no solo que existe
|
|
87
|
+
- Respetar convenciones de género sin esclavizarse a ellas — nombrar la convención y la opción de romperla
|
|
88
|
+
- El silencio y la reducción de densidad son movimientos compositivos, no falta de material
|
|
89
|
+
- Si el usuario describe el objetivo en términos no técnicos, traducir primero y confirmar el diagnóstico antes de proponer
|
|
90
|
+
- Cuando escribas material a archivo, preguntar "¿Escribo esto a `<path>`?" antes de usar Write
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Verdict
|
|
95
|
+
|
|
96
|
+
Al cerrar una consulta:
|
|
97
|
+
|
|
98
|
+
- **COMPLETE** — el problema está diagnosticado y hay al menos dos movimientos concretos con trade-off explícito
|
|
99
|
+
- **CONCERNS** — hay propuesta pero el diagnóstico no se confirmó con el usuario, o la salida quedó descriptiva en vez de concreta
|
|
100
|
+
- **BLOCKED** — falta información que no se puede inferir: tonalidad, instrumentación target, género, o el material existente
|
|
101
|
+
|
|
102
|
+
## Next steps
|
|
103
|
+
|
|
104
|
+
- `/midi-compose` para bajar el material a un `.mid` real
|
|
105
|
+
- `/adaptive-music` para convertir un tema en un sistema que responde al gameplay
|
|
106
|
+
- `/audio-bible` si las decisiones musicales no tienen dirección de la cual colgarse
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Forma
|
|
2
|
+
|
|
3
|
+
## El principio
|
|
4
|
+
|
|
5
|
+
La forma administra **atención**. Cada sección existe para contrastar con la anterior. Si dos secciones consecutivas tienen la misma densidad, registro y ritmo armónico, funcionalmente son una sola.
|
|
6
|
+
|
|
7
|
+
## Palancas de contraste
|
|
8
|
+
|
|
9
|
+
Para diferenciar secciones, en orden de eficacia:
|
|
10
|
+
|
|
11
|
+
1. **Densidad** — cuántos elementos suenan a la vez
|
|
12
|
+
2. **Registro** — dónde vive la melodía
|
|
13
|
+
3. **Ritmo armónico** — velocidad de cambio de acorde
|
|
14
|
+
4. **Textura** — homofónica, polifónica, monofónica
|
|
15
|
+
5. **Instrumentación** — quién toca
|
|
16
|
+
6. **Dinámica** — la más obvia y la más débil por sí sola
|
|
17
|
+
|
|
18
|
+
Un estribillo que solo es "más fuerte" no es un estribillo. Uno que sube el registro, dobla el ritmo armónico y agrega dos capas, sí.
|
|
19
|
+
|
|
20
|
+
## Formas populares
|
|
21
|
+
|
|
22
|
+
| Forma | Esquema | Nota |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| Estrofa-estribillo | A B A B (C) B | Estándar; el puente C aporta el contraste que evita la fatiga |
|
|
25
|
+
| AABA | A A B A | Estándar de jazz/musical; el B es el "bridge" |
|
|
26
|
+
| Verso-pre-estribillo-estribillo | A A' B A B | El pre-estribillo hace el trabajo de tensión |
|
|
27
|
+
| Through-composed | sin repetición | Narrativo; costoso de escuchar dos veces |
|
|
28
|
+
| Loop con capas | un solo material, capas entrando/saliendo | La forma nativa de música de juego |
|
|
29
|
+
|
|
30
|
+
## Formas clásicas
|
|
31
|
+
|
|
32
|
+
- **Binaria / ternaria** — AB, ABA. Base de todo
|
|
33
|
+
- **Sonata** — exposición (dos temas en tonalidades distintas), desarrollo (fragmentación y modulación), reexposición (los dos temas en la tónica). El motor es el conflicto tonal
|
|
34
|
+
- **Tema y variaciones** — un material, N transformaciones. La forma más directamente aplicable a juegos
|
|
35
|
+
- **Rondó** — ABACA. El retorno del A da estabilidad, los episodios dan variedad
|
|
36
|
+
- **Passacaglia / chacona** — bajo obstinado con variación arriba. Loop infinito que no aburre
|
|
37
|
+
|
|
38
|
+
## Transiciones
|
|
39
|
+
|
|
40
|
+
Lo que hace que dos secciones se peguen o se choquen:
|
|
41
|
+
|
|
42
|
+
| Recurso | Efecto |
|
|
43
|
+
|---|---|
|
|
44
|
+
| **Anacrusa** | Anticipar la sección con una figura antes del downbeat |
|
|
45
|
+
| **Turnaround** | 1-2 compases de armonía que devuelven a la tónica |
|
|
46
|
+
| **Fill / break** | Silencio o solo percusión antes de la entrada |
|
|
47
|
+
| **Pedal** | Sostener una nota mientras cambia todo arriba |
|
|
48
|
+
| **Corte seco** | Sin preparación. Efectivo una vez por pieza |
|
|
49
|
+
| **Riser / crescendo de densidad** | Sumar capas hacia el punto de cambio |
|
|
50
|
+
|
|
51
|
+
La transición incómoda casi siempre es un cambio de tonalidad o textura sin ninguno de estos.
|
|
52
|
+
|
|
53
|
+
## Narrativa formal
|
|
54
|
+
|
|
55
|
+
Una pieza tiene un arco, y el arco tiene un punto de máxima información. Ubicarlo entre el 60% y el 75% de la duración: suficientemente tarde para haber construido, suficientemente temprano para tener resolución.
|
|
56
|
+
|
|
57
|
+
La **reducción** funciona mejor que la adición para señalizar un momento importante. Sacar todo y dejar una voz llama más atención que sumar la décima capa.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Música de Juego
|
|
2
|
+
|
|
3
|
+
## La restricción central
|
|
4
|
+
|
|
5
|
+
La música de juego se escucha **muchas más veces que la música lineal, en duración desconocida, y compitiendo con SFX y diálogo**. Eso cambia las decisiones compositivas, no solo la implementación.
|
|
6
|
+
|
|
7
|
+
Consecuencias directas:
|
|
8
|
+
|
|
9
|
+
1. **Lo que en música lineal es "interesante" acá es "fatigante".** Un gesto llamativo cada 8 compases se vuelve insoportable a la vigésima repetición
|
|
10
|
+
2. **La duración es indefinida.** No hay clímax garantizado; el arco tiene que funcionar cortado en cualquier punto
|
|
11
|
+
3. **El registro medio no es tuyo.** Diálogo y feedback de UI viven ahí
|
|
12
|
+
4. **El silencio es barato y potente.** Es el único recurso que no se gasta con la repetición
|
|
13
|
+
|
|
14
|
+
## Escribir para repetición
|
|
15
|
+
|
|
16
|
+
- **Loops largos con material corto** — un loop de 2 minutos construido con 4 células es menos fatigante que uno de 30 segundos con 12 ideas
|
|
17
|
+
- **Evitar el gesto firma dentro del loop** — guardar los eventos distintivos para stingers o para capas que entran ocasionalmente
|
|
18
|
+
- **Ritmo armónico lento** — la progresión que se puede escuchar 50 veces es la que no llama la atención sobre sí misma
|
|
19
|
+
- **Ambigüedad de fin** — si el loop cierra con cadencia auténtica, cada repetición suena a arranque forzado. Cerrar con semicadencia, pedal o cadencia plagal
|
|
20
|
+
- **Priorizar textura sobre melodía en gameplay sostenido** — la melodía fuerte es para momentos, no para fondo
|
|
21
|
+
|
|
22
|
+
## Composición por capas
|
|
23
|
+
|
|
24
|
+
La forma nativa. Escribir un mismo material en estratos que pueden entrar y salir sin romper nada:
|
|
25
|
+
|
|
26
|
+
| Capa | Rol | Entra cuando |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| **Bed** | Pad/textura sostenida, define tonalidad | Siempre |
|
|
29
|
+
| **Pulse** | Pulso rítmico mínimo | Hay actividad |
|
|
30
|
+
| **Harmony** | Movimiento armónico explícito | Sube la tensión |
|
|
31
|
+
| **Melody** | Tema identificable | Momento narrativo |
|
|
32
|
+
| **Intensity** | Percusión densa, metales, disonancia | Combate/clímax |
|
|
33
|
+
|
|
34
|
+
**Regla de composición:** cada capa tiene que sonar bien sola y en cualquier combinación con las de abajo. Eso se logra escribiéndolas todas contra la misma armonía y el mismo grid, y no dejando que ninguna dependa de otra para tener sentido.
|
|
35
|
+
|
|
36
|
+
## Temas y transformación
|
|
37
|
+
|
|
38
|
+
Un tema, N estados de juego. Las transformaciones más rentables:
|
|
39
|
+
|
|
40
|
+
| Transformación | Estado que representa |
|
|
41
|
+
|---|---|
|
|
42
|
+
| Aumentación + reducción de densidad | Memoria, calma, safe zone |
|
|
43
|
+
| Modo menor / ♭6 | Amenaza, pérdida |
|
|
44
|
+
| Fragmentación + aumento de tempo | Persecución, urgencia |
|
|
45
|
+
| Reorquestación a solo | Intimidad, muerte de personaje |
|
|
46
|
+
| Sobre pedal disonante | Corrupción, algo está mal |
|
|
47
|
+
| Métrica cambiada | Extrañeza, otro plano |
|
|
48
|
+
|
|
49
|
+
Esto le da coherencia a una banda de sonido entera con una sola idea musical, y hace que el jugador "reconozca" sin saber por qué.
|
|
50
|
+
|
|
51
|
+
## Modos por mood
|
|
52
|
+
|
|
53
|
+
| Mood buscado | Recurso |
|
|
54
|
+
|---|---|
|
|
55
|
+
| Aventura, optimismo | Mixolidio, ♭7; cuartas y quintas abiertas |
|
|
56
|
+
| Melancolía sin tragedia | Dórico, ♮6 |
|
|
57
|
+
| Asombro, escala | Lidio, ♯4; acordes mayores por movimiento de terceras |
|
|
58
|
+
| Amenaza | Frigio, ♭2; tritono; cluster |
|
|
59
|
+
| Misterio | Escala de tonos enteros; armonía cuartal; ambigüedad de centro |
|
|
60
|
+
| Tensión sostenida | Pedal + disonancia que no resuelve |
|
|
61
|
+
| Vacío, desolación | Intervalos abiertos, silencio, un solo timbre |
|
|
62
|
+
|
|
63
|
+
## Enfoques generativos
|
|
64
|
+
|
|
65
|
+
Cuando el contenido tiene que exceder lo que se puede escribir a mano:
|
|
66
|
+
|
|
67
|
+
- **Cadenas de Markov** — melodía estocástica entrenada sobre material propio. Preserva el idioma; no genera estructura
|
|
68
|
+
- **L-systems / gramáticas recursivas** — generan *estructura* jerárquica (frases, secciones) por reescritura de reglas. Complemento natural de Markov, que no tiene forma
|
|
69
|
+
- **Restricciones de armonía** — capa de validación: el generador propone, las reglas de voice leading y función filtran
|
|
70
|
+
- **Selección estocástica de capas** — la variación viene de qué combinación suena, no de generar notas nuevas. Mucho más barato y más controlable
|
|
71
|
+
|
|
72
|
+
**Recomendación práctica:** la selección estocástica de capas resuelve el 90% de la necesidad real de variación en un juego, sin ninguno de los riesgos de calidad de la generación de notas. Ir a Markov/L-systems solo cuando la variación de capas ya no alcanza.
|
|
73
|
+
|
|
74
|
+
## Anti-patterns
|
|
75
|
+
|
|
76
|
+
- **Loop de 30 segundos con melodía fuerte** — el camino más rápido a que el jugador baje la música
|
|
77
|
+
- **Cadencia auténtica al final del loop** — cada vuelta suena a error
|
|
78
|
+
- **Melodía en 500 Hz-2 kHz durante diálogo** — pelea con VO y pierde
|
|
79
|
+
- **Todas las capas dependientes entre sí** — al quitar una, el resto suena incompleto
|
|
80
|
+
- **Un tema por zona sin relación entre ellos** — la banda de sonido no acumula significado
|
|
81
|
+
- **Componer a duración fija** — el gameplay no dura lo que dura tu pieza
|
|
82
|
+
- **Sub saturado de música** — se come el espacio de los impactos
|