Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

notepat: ZOO + LASERS kits w/ classic subtractive synthesis recipes

Two new kit modes on the pgup/pgdn cycle:
off → perc → warA → warB → zoo → lasers → off

ZOO (12 animals). Each pad is 1-3 voices shaped by pitch/amp envelopes.
Recipes follow the classic analog-synth animal-sound tradition (MOD
WIGGLER analog-animal thread, Robert Rich's "Bestiary"):
dog = saw burst + pitch drop + noise attack
cat = two-peak pitch envelope "eow" (the 'm' isn't achievable in pure
subtractive so we skip it — stagger three sines instead)
cow = low triangle + sub-sine, slow attack/sustain/release
sheep = triangle with staggered pitch for vibrato
bird = 3 descending high sine pulses 40ms apart (chirp)
pig = two saw bursts
lion = resonant noise + sub-saw, long roar
owl = soft sine with long body + sub
frog = double low-saw ribbit
horse = 4-voice chained-square whinny
snake = BPF noise, sustained hiss (hold to extend)
whale = slow 3-voice low glide over 2s

LASERS (12 sci-fi). Classic sci-fi recipes:
pew = descending sine triad (Atari Asteroids)
blast = descending saw + noise crack (Star Wars blaster)
phaser = sine + co-modulating noise (Star Trek phaser was derived
from War of the Worlds tape feedback per SlashFilm)
cannon = big descending saw + sub thump
stun = two detuned squares (ring-mod feel)
plasma = triple-staggered square with detune/vibrato
disruptor = gritty saw + noise
charge = exp pitch rise (6 staggered sines, 100→2500 Hz log)
beam = sustained buzz (hold to extend)
hit = noise crack + sine thump + saw mid
ricochet = descending filtered-noise ping + sine tail
warp = fast sine sweep down 1800→180 Hz (Doppler)

Kit dispatcher + banner labels + kitNamesFor/Labels/Colors/Notation
all extended. Noise voices continue to honor the per-kit pitch factor
(octave/pitch shift) for consistency with perc.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+344 -11
+344 -11
fedac/native/pieces/notepat.mjs
··· 1085 1085 "a#": [["s",3000,0.9],["s",5500,0.7],["s",9000,0.5]], // ricochet 1086 1086 }; 1087 1087 1088 + // === ZOO KIT === 1089 + // 12 animal calls synthesized with classic subtractive/FM recipes. Each 1090 + // sound is 1-3 voices, pitch-envelope shaped. Animal calls rely heavily 1091 + // on pitch modulation — bird chirps = fast descending pulses, cat meow = 1092 + // two-peak pitch envelope ("eow", the 'm' is impossible in pure 1093 + // subtractive), lion roar = noise + sub-saw, etc. (Classic synthesis 1094 + // references: MOD WIGGLER analog-animal thread, Robert Rich's "Bestiary"). 1095 + const ZOO_NAMES = { 1096 + c: "dog", d: "cat", e: "cow", f: "sheep", 1097 + g: "bird", a: "pig", b: "lion", 1098 + "c#": "owl", "d#": "frog", 1099 + "f#": "horse", "g#": "snake", "a#": "whale", 1100 + }; 1101 + const ZOO_LABELS = { 1102 + c: "DOG", d: "CAT", e: "COW", f: "SHP", 1103 + g: "BRD", a: "PIG", b: "LION", 1104 + "c#": "OWL", "d#": "FRG", 1105 + "f#": "HRS", "g#": "SNK", "a#": "WHL", 1106 + }; 1107 + const ZOO_COLORS = { 1108 + c: [200, 130, 80], // dog — warm tan 1109 + d: [230, 180, 220], // cat — pink 1110 + e: [220, 200, 170], // cow — cream 1111 + f: [240, 240, 220], // sheep — wool white 1112 + g: [140, 220, 220], // bird — pale teal 1113 + a: [240, 180, 180], // pig — pink-salmon 1114 + b: [220, 170, 90], // lion — golden 1115 + "c#": [120, 120, 180], // owl — twilight blue 1116 + "d#": [140, 220, 140], // frog — green 1117 + "f#": [170, 120, 80], // horse — chestnut 1118 + "g#": [170, 230, 130], // snake — lime-olive 1119 + "a#": [100, 180, 230], // whale — ocean blue 1120 + }; 1121 + const ZOO_NOTATION = { 1122 + c: [["w",180,0.8],["n",2500,0.3]], // dog 1123 + d: [["s",500,0.6],["s",350,0.4]], // cat 1124 + e: [["t",90,1.2],["s",45,0.8]], // cow 1125 + f: [["t",380,0.5]], // sheep 1126 + g: [["s",2500,0.5],["s",3500,0.4]], // bird 1127 + a: [["w",140,0.7],["w",180,0.5]], // pig 1128 + b: [["n",400,1.0],["w",70,1.2]], // lion 1129 + "c#": [["s",320,0.6]], // owl 1130 + "d#": [["w",110,0.6]], // frog 1131 + "f#": [["q",450,0.5],["s",500,0.3]], // horse 1132 + "g#": [["n",2000,0.8]], // snake 1133 + "a#": [["s",100,1.2]], // whale 1134 + }; 1135 + // Which zoo sounds sustain while held (release kills the voice). 1136 + const ZOO_SUSTAIN = { 1137 + "g#": true, // snake — long hiss 1138 + }; 1139 + const ZOO_DURATION = { 1140 + c: 0.12, d: 0.45, e: 0.60, f: 0.35, 1141 + g: 0.16, a: 0.25, b: 0.90, 1142 + "c#": 0.55, "d#": 0.18, 1143 + "f#": 0.55, "a#": 2.0, 1144 + }; 1145 + const ZOO_RELEASE = { 1146 + "g#": 0.25, 1147 + }; 1148 + 1149 + // Fire a zoo-kit animal sound. Classic subtractive recipes, one-shot or 1150 + // sustained. `pitchFactor` scales all tonal voices so the kit bends 1151 + // with octave/pitch like perc does. 1152 + function playZoo(sound, letter, volume = 1.0, pan = 0, pitchFactor = 1.0, phase = "both", holdVoices = null) { 1153 + if (!sound?.synth) return; 1154 + if (phase === "up") return; 1155 + const v = Math.max(0.1, Math.min(2.2, volume)); 1156 + const pf = Math.max(0.25, Math.min(4, pitchFactor)); 1157 + const isLive = phase === "down" && Array.isArray(holdVoices); 1158 + const rj = (c, f) => c * (1 + (Math.random() - 0.5) * 2 * f); 1159 + const fire = (params, dur, sustainLen) => { 1160 + if (isLive && ZOO_SUSTAIN[letter]) { 1161 + const h = sound.synth({ ...params, duration: Infinity }); 1162 + if (h) holdVoices.push({ handle: h, releaseFade: ZOO_RELEASE[letter] ?? 0.1 }); 1163 + } else { 1164 + sound.synth({ ...params, duration: dur ?? sustainLen ?? 0.2 }); 1165 + } 1166 + }; 1167 + switch (letter) { 1168 + case "c": { // dog bark — saw burst with pitch drop + noise attack 1169 + fire({ type: "sawtooth", tone: 200 * pf, volume: rj(0.75, 0.1) * v, attack: 0.001, decay: 0.07, pan }, 0.08); 1170 + sound.synth({ type: "sawtooth", tone: 110 * pf, duration: 0.05, volume: rj(0.55, 0.1) * v, attack: 0.015, decay: 0.035, pan }); 1171 + sound.synth({ type: "noise", tone: 2500 * pf, duration: 0.006, volume: 0.45 * v, attack: 0.0005, decay: 0.005, pan }); 1172 + break; 1173 + } 1174 + case "d": { // cat meow — 2-peak pitch envelope "eow" 1175 + const base = 500 * pf; 1176 + sound.synth({ type: "sine", tone: base, duration: 0.12, volume: rj(0.55, 0.1) * v, attack: 0.02, decay: 0.1, pan }); 1177 + sound.synth({ type: "sine", tone: base * 0.65, duration: 0.12, volume: rj(0.50, 0.1) * v, attack: 0.08, decay: 0.1, pan }); 1178 + sound.synth({ type: "sine", tone: base * 0.9, duration: 0.12, volume: rj(0.45, 0.1) * v, attack: 0.20, decay: 0.1, pan }); 1179 + break; 1180 + } 1181 + case "e": { // cow moo — low triangle with slow attack/sustain 1182 + sound.synth({ type: "triangle", tone: 95 * pf, duration: 0.55, volume: rj(0.85, 0.08) * v, attack: 0.05, decay: 0.4, pan }); 1183 + sound.synth({ type: "sine", tone: 48 * pf, duration: 0.55, volume: rj(0.65, 0.08) * v, attack: 0.08, decay: 0.4, pan }); 1184 + break; 1185 + } 1186 + case "f": { // sheep baa — vibrato + triple pitch stagger 1187 + const base = 380 * pf; 1188 + sound.synth({ type: "triangle", tone: base, duration: 0.1, volume: rj(0.55, 0.1) * v, attack: 0.002, decay: 0.09, pan }); 1189 + sound.synth({ type: "triangle", tone: base * 0.9, duration: 0.1, volume: rj(0.50, 0.1) * v, attack: 0.08, decay: 0.09, pan }); 1190 + sound.synth({ type: "triangle", tone: base, duration: 0.1, volume: rj(0.45, 0.1) * v, attack: 0.18, decay: 0.09, pan }); 1191 + break; 1192 + } 1193 + case "g": { // bird chirp — three fast descending high pulses 1194 + for (let i = 0; i < 3; i++) { 1195 + const atk = 0.04 * i; 1196 + const tone = (3200 - i * 450) * pf; 1197 + sound.synth({ type: "sine", tone, duration: 0.03, volume: rj(0.55, 0.15) * v, attack: 0.0005 + atk, decay: 0.025, pan }); 1198 + } 1199 + break; 1200 + } 1201 + case "a": { // pig oink — two short saw bursts 1202 + sound.synth({ type: "sawtooth", tone: 140 * pf, duration: 0.07, volume: rj(0.70, 0.1) * v, attack: 0.003, decay: 0.060, pan }); 1203 + sound.synth({ type: "sawtooth", tone: 180 * pf, duration: 0.06, volume: rj(0.60, 0.1) * v, attack: 0.09, decay: 0.05, pan }); 1204 + break; 1205 + } 1206 + case "b": { // lion roar — resonant noise + sub saw 1207 + sound.synth({ type: "noise", tone: 420 * pf, duration: 0.85, volume: rj(0.60, 0.1) * v, attack: 0.06, decay: 0.7, pan }); 1208 + sound.synth({ type: "sawtooth", tone: 70 * pf, duration: 0.85, volume: rj(0.70, 0.1) * v, attack: 0.08, decay: 0.7, pan }); 1209 + sound.synth({ type: "sawtooth", tone: 110 * pf, duration: 0.5, volume: rj(0.35, 0.1) * v, attack: 0.12, decay: 0.35, pan }); 1210 + break; 1211 + } 1212 + case "c#": { // owl hoot — soft sine with long body 1213 + sound.synth({ type: "sine", tone: 320 * pf, duration: 0.5, volume: rj(0.65, 0.08) * v, attack: 0.08, decay: 0.4, pan }); 1214 + sound.synth({ type: "sine", tone: 160 * pf, duration: 0.5, volume: rj(0.45, 0.08) * v, attack: 0.1, decay: 0.4, pan }); 1215 + break; 1216 + } 1217 + case "d#": { // frog ribbit — double low saw bursts 1218 + sound.synth({ type: "sawtooth", tone: 110 * pf, duration: 0.04, volume: rj(0.65, 0.08) * v, attack: 0.002, decay: 0.036, pan }); 1219 + sound.synth({ type: "sawtooth", tone: 95 * pf, duration: 0.04, volume: rj(0.55, 0.08) * v, attack: 0.07, decay: 0.036, pan }); 1220 + break; 1221 + } 1222 + case "f#": { // horse neigh — square with whinny vibrato chain 1223 + const base = 440 * pf; 1224 + sound.synth({ type: "square", tone: base, duration: 0.08, volume: rj(0.5, 0.1) * v, attack: 0.002, decay: 0.075, pan }); 1225 + sound.synth({ type: "square", tone: base * 1.15, duration: 0.08, volume: rj(0.45, 0.1) * v, attack: 0.09, decay: 0.075, pan }); 1226 + sound.synth({ type: "square", tone: base * 0.9, duration: 0.08, volume: rj(0.40, 0.1) * v, attack: 0.18, decay: 0.075, pan }); 1227 + sound.synth({ type: "square", tone: base, duration: 0.08, volume: rj(0.35, 0.1) * v, attack: 0.27, decay: 0.075, pan }); 1228 + break; 1229 + } 1230 + case "g#": { // snake hiss — filtered noise, sustained 1231 + const params = { type: "noise", tone: 2000 * pf, volume: rj(0.45, 0.08) * v, attack: 0.04, decay: 0.2, pan }; 1232 + fire(params, 0.9, 0.9); 1233 + break; 1234 + } 1235 + case "a#": { // whale song — slow low glide 1236 + const base = 110 * pf; 1237 + sound.synth({ type: "sine", tone: base, duration: 0.8, volume: rj(0.55, 0.08) * v, attack: 0.3, decay: 0.5, pan }); 1238 + sound.synth({ type: "sine", tone: base * 0.7, duration: 0.9, volume: rj(0.45, 0.08) * v, attack: 0.4, decay: 0.5, pan }); 1239 + sound.synth({ type: "sine", tone: base * 1.4, duration: 0.8, volume: rj(0.35, 0.08) * v, attack: 0.5, decay: 0.3, pan }); 1240 + break; 1241 + } 1242 + } 1243 + } 1244 + 1245 + // === LASERS KIT === 1246 + // 12 sci-fi weapon/energy sounds. Classic recipes: pew = descending 1247 + // sine/square (Atari Asteroids), blaster = saw + noise (Star Wars), 1248 + // phaser = sine modulated by noise (Star Trek's sound was derived from 1249 + // War of the Worlds tape feedback — see SlashFilm). Charge-up = exp 1250 + // pitch rise; warp = fast sine sweep (Doppler). 1251 + const LASER_NAMES = { 1252 + c: "pew", d: "blast", e: "phaser", f: "cannon", 1253 + g: "stun", a: "plasma", b: "disruptor", 1254 + "c#": "charge", "d#": "beam", 1255 + "f#": "hit", "g#": "ricochet", "a#": "warp", 1256 + }; 1257 + const LASER_LABELS = { 1258 + c: "PEW", d: "BLS", e: "PHS", f: "CAN", 1259 + g: "STN", a: "PLS", b: "DSR", 1260 + "c#": "CHG", "d#": "BEM", 1261 + "f#": "HIT", "g#": "RIC", "a#": "WRP", 1262 + }; 1263 + const LASER_COLORS = { 1264 + c: [180, 255, 140], // pew — neon green 1265 + d: [255, 140, 140], // blast — red 1266 + e: [180, 200, 255], // phaser — electric blue 1267 + f: [255, 120, 80], // cannon — orange flame 1268 + g: [200, 255, 255], // stun — pale cyan 1269 + a: [220, 140, 255], // plasma — violet 1270 + b: [255, 200, 80], // disruptor — gold 1271 + "c#": [100, 180, 255], // charge — deep blue 1272 + "d#": [140, 255, 200], // beam — lime 1273 + "f#": [255, 100, 60], // hit — impact red-orange 1274 + "g#": [220, 220, 180], // ricochet — metallic 1275 + "a#": [200, 120, 255], // warp — magenta 1276 + }; 1277 + const LASER_NOTATION = { 1278 + c: [["s",2500,0.6]], // pew 1279 + d: [["w",150,0.8],["n",2500,0.4]], // blast 1280 + e: [["s",700,0.5],["n",1200,0.3]], // phaser 1281 + f: [["w",500,0.7],["s",55,0.9]], // cannon 1282 + g: [["q",400,0.6]], // stun 1283 + a: [["q",260,0.6]], // plasma 1284 + b: [["w",520,0.6]], // disruptor 1285 + "c#": [["s",800,0.6]], // charge 1286 + "d#": [["q",800,0.7]], // beam 1287 + "f#": [["n",3000,0.6],["s",60,1.0]], // hit 1288 + "g#": [["n",4500,0.5]], // ricochet 1289 + "a#": [["s",1000,0.6]], // warp 1290 + }; 1291 + const LASER_SUSTAIN = { 1292 + "d#": true, // beam — sustained hum 1293 + }; 1294 + const LASER_DURATION = { 1295 + c: 0.10, d: 0.20, e: 0.28, f: 0.25, 1296 + g: 0.40, a: 0.50, b: 0.35, 1297 + "c#": 0.75, "f#": 0.30, 1298 + "g#": 0.30, "a#": 0.55, 1299 + }; 1300 + const LASER_RELEASE = { 1301 + "d#": 0.08, 1302 + }; 1303 + 1304 + // Fire a laser-kit sci-fi sound. Pitch envelopes and FM-ish modulation 1305 + // via short stacked voices with staggered attacks. `pitchFactor` scales 1306 + // tonal voices like every other kit. 1307 + function playLaser(sound, letter, volume = 1.0, pan = 0, pitchFactor = 1.0, phase = "both", holdVoices = null) { 1308 + if (!sound?.synth) return; 1309 + if (phase === "up") return; 1310 + const v = Math.max(0.1, Math.min(2.2, volume)); 1311 + const pf = Math.max(0.25, Math.min(4, pitchFactor)); 1312 + const isLive = phase === "down" && Array.isArray(holdVoices); 1313 + const rj = (c, f) => c * (1 + (Math.random() - 0.5) * 2 * f); 1314 + const fire = (params, dur) => { 1315 + if (isLive && LASER_SUSTAIN[letter]) { 1316 + const h = sound.synth({ ...params, duration: Infinity }); 1317 + if (h) holdVoices.push({ handle: h, releaseFade: LASER_RELEASE[letter] ?? 0.08 }); 1318 + } else { 1319 + sound.synth({ ...params, duration: dur }); 1320 + } 1321 + }; 1322 + switch (letter) { 1323 + case "c": { // pew — fast descending sine (Atari Asteroids) 1324 + sound.synth({ type: "sine", tone: 3200 * pf, duration: 0.03, volume: rj(0.75, 0.1) * v, attack: 0.0005, decay: 0.028, pan }); 1325 + sound.synth({ type: "sine", tone: 900 * pf, duration: 0.06, volume: rj(0.55, 0.1) * v, attack: 0.02, decay: 0.055, pan }); 1326 + sound.synth({ type: "sine", tone: 300 * pf, duration: 0.05, volume: rj(0.45, 0.1) * v, attack: 0.05, decay: 0.045, pan }); 1327 + break; 1328 + } 1329 + case "d": { // blast — saw descending with noise crack (Star Wars) 1330 + sound.synth({ type: "sawtooth", tone: 300 * pf, duration: 0.04, volume: rj(0.80, 0.08) * v, attack: 0.001, decay: 0.038, pan }); 1331 + sound.synth({ type: "sawtooth", tone: 100 * pf, duration: 0.08, volume: rj(0.70, 0.08) * v, attack: 0.03, decay: 0.07, pan }); 1332 + sound.synth({ type: "noise", tone: 3500 * pf, duration: 0.02, volume: rj(0.50, 0.1) * v, attack: 0.0005, decay: 0.018, pan }); 1333 + break; 1334 + } 1335 + case "e": { // phaser — sine + co-modulating noise (War-of-Worlds feedback) 1336 + sound.synth({ type: "sine", tone: 700 * pf, duration: 0.25, volume: rj(0.55, 0.1) * v, attack: 0.005, decay: 0.22, pan }); 1337 + sound.synth({ type: "sine", tone: 480 * pf, duration: 0.25, volume: rj(0.40, 0.1) * v, attack: 0.01, decay: 0.22, pan }); 1338 + sound.synth({ type: "noise", tone: 1400 * pf, duration: 0.25, volume: rj(0.25, 0.1) * v, attack: 0.005, decay: 0.22, pan }); 1339 + break; 1340 + } 1341 + case "f": { // cannon — descending saw + sub thump 1342 + sound.synth({ type: "sawtooth", tone: 700 * pf, duration: 0.05, volume: rj(0.70, 0.08) * v, attack: 0.001, decay: 0.045, pan }); 1343 + sound.synth({ type: "sawtooth", tone: 200 * pf, duration: 0.10, volume: rj(0.80, 0.08) * v, attack: 0.03, decay: 0.09, pan }); 1344 + sound.synth({ type: "sine", tone: 55 * pf, duration: 0.20, volume: rj(0.90, 0.08) * v, attack: 0.005, decay: 0.19, pan }); 1345 + break; 1346 + } 1347 + case "g": { // stun — buzzing ring-mod-ish square with slight wobble 1348 + sound.synth({ type: "square", tone: 400 * pf, duration: 0.38, volume: rj(0.35, 0.1) * v, attack: 0.01, decay: 0.35, pan }); 1349 + sound.synth({ type: "square", tone: 420 * pf, duration: 0.38, volume: rj(0.30, 0.1) * v, attack: 0.02, decay: 0.35, pan }); 1350 + break; 1351 + } 1352 + case "a": { // plasma — slow vibrato square 1353 + const base = 260 * pf; 1354 + sound.synth({ type: "square", tone: base, duration: 0.48, volume: rj(0.45, 0.08) * v, attack: 0.02, decay: 0.44, pan }); 1355 + sound.synth({ type: "square", tone: base * 1.06, duration: 0.48, volume: rj(0.38, 0.08) * v, attack: 0.05, decay: 0.44, pan }); 1356 + sound.synth({ type: "square", tone: base * 0.94, duration: 0.48, volume: rj(0.32, 0.08) * v, attack: 0.08, decay: 0.44, pan }); 1357 + break; 1358 + } 1359 + case "b": { // disruptor — gritty saw + noise body 1360 + sound.synth({ type: "sawtooth", tone: 520 * pf, duration: 0.33, volume: rj(0.55, 0.1) * v, attack: 0.003, decay: 0.32, pan }); 1361 + sound.synth({ type: "noise", tone: 800 * pf, duration: 0.33, volume: rj(0.30, 0.1) * v, attack: 0.01, decay: 0.32, pan }); 1362 + break; 1363 + } 1364 + case "c#": { // charge-up — exp pitch rise 1365 + for (let i = 0; i < 6; i++) { 1366 + const t = i / 5; 1367 + const tone = 100 * Math.pow(25, t) * pf; 1368 + sound.synth({ type: "sine", tone, duration: 0.2, volume: rj(0.45 - t * 0.1, 0.08) * v, attack: 0.12 * i, decay: 0.18, pan }); 1369 + } 1370 + break; 1371 + } 1372 + case "d#": { // beam — sustained buzz 1373 + const params = { type: "square", tone: 820 * pf, volume: rj(0.40, 0.06) * v, attack: 0.02, decay: 0.12, pan }; 1374 + fire(params, 0.35); 1375 + sound.synth({ type: "square", tone: 410 * pf, duration: 0.3, volume: rj(0.30, 0.06) * v, attack: 0.02, decay: 0.28, pan }); 1376 + break; 1377 + } 1378 + case "f#": { // hit — noise crack + sub thump 1379 + sound.synth({ type: "noise", tone: 3000 * pf, duration: 0.05, volume: rj(0.60, 0.1) * v, attack: 0.0005, decay: 0.045, pan }); 1380 + sound.synth({ type: "sine", tone: 60 * pf, duration: 0.25, volume: rj(0.75, 0.08) * v, attack: 0.005, decay: 0.24, pan }); 1381 + sound.synth({ type: "sawtooth", tone: 120 * pf, duration: 0.08, volume: rj(0.35, 0.1) * v, attack: 0.01, decay: 0.07, pan }); 1382 + break; 1383 + } 1384 + case "g#": { // ricochet — descending filtered noise ping 1385 + sound.synth({ type: "noise", tone: 5500 * pf, duration: 0.04, volume: rj(0.45, 0.08) * v, attack: 0.001, decay: 0.038, pan }); 1386 + sound.synth({ type: "noise", tone: 3000 * pf, duration: 0.06, volume: rj(0.35, 0.08) * v, attack: 0.04, decay: 0.055, pan }); 1387 + sound.synth({ type: "sine", tone: 3000 * pf, duration: 0.08, volume: rj(0.30, 0.1) * v, attack: 0.02, decay: 0.075, pan }); 1388 + break; 1389 + } 1390 + case "a#": { // warp — fast sine sweep down (Doppler) 1391 + for (let i = 0; i < 5; i++) { 1392 + const t = i / 4; 1393 + const tone = (1800 * Math.pow(0.1, t)) * pf; 1394 + sound.synth({ type: "sine", tone, duration: 0.12, volume: rj(0.55 - t * 0.1, 0.06) * v, attack: 0.08 * i, decay: 0.1, pan }); 1395 + } 1396 + break; 1397 + } 1398 + } 1399 + } 1400 + 1088 1401 // === KIT HELPERS === 1089 1402 // Unified accessors: these return the active-kit metadata for a given 1090 1403 // (letter, gridOffset) so render code doesn't need to branch on ··· 1095 1408 function kitNamesFor(kit) { 1096 1409 if (isWarKit(kit)) return WAR_NAMES; 1097 1410 if (kit === "perc") return PERCUSSION_NAMES; 1411 + if (kit === "zoo") return ZOO_NAMES; 1412 + if (kit === "lasers") return LASER_NAMES; 1098 1413 return null; 1099 1414 } 1100 1415 function kitLabelsFor(kit) { 1101 1416 if (isWarKit(kit)) return WAR_LABELS; 1102 1417 if (kit === "perc") return PERCUSSION_LABELS; 1418 + if (kit === "zoo") return ZOO_LABELS; 1419 + if (kit === "lasers") return LASER_LABELS; 1103 1420 return null; 1104 1421 } 1105 1422 function kitColorsFor(kit) { 1106 1423 if (isWarKit(kit)) return WAR_COLORS; 1107 1424 if (kit === "perc") return PERCUSSION_COLORS; 1425 + if (kit === "zoo") return ZOO_COLORS; 1426 + if (kit === "lasers") return LASER_COLORS; 1108 1427 return null; 1109 1428 } 1110 1429 function kitNotationFor(kit) { 1111 1430 if (isWarKit(kit)) return WAR_NOTATION; 1112 1431 if (kit === "perc") return PERCUSSION_NOTATION; 1432 + if (kit === "zoo") return ZOO_NOTATION; 1433 + if (kit === "lasers") return LASER_NOTATION; 1113 1434 return null; 1114 1435 } 1115 1436 1116 - // Cycle the kit state for a side: off → perc → warA → warB → off. 1117 - // warA = preset-default model per weapon (mostly classic), warB forces 1118 - // physical/DWG synthesis on every weapon for direct A/B comparison. 1437 + // Cycle the kit state for a side: off → perc → warA → warB → zoo → lasers → off. 1438 + // warA = preset-default model per weapon (mostly classic); warB forces 1439 + // physical/DWG synthesis for direct A/B comparison; zoo + lasers are 1440 + // sound-design sketches grounded in classic subtractive synthesis 1441 + // recipes (animal calls, sci-fi phasers/blasters/warps). 1119 1442 function cycleKit(side) { 1120 1443 const cur = side === "left" ? kitLeft : kitRight; 1121 - const next = cur === "off" ? "perc" 1122 - : cur === "perc" ? "warA" 1123 - : cur === "warA" ? "warB" 1444 + const next = cur === "off" ? "perc" 1445 + : cur === "perc" ? "warA" 1446 + : cur === "warA" ? "warB" 1447 + : cur === "warB" ? "zoo" 1448 + : cur === "zoo" ? "lasers" 1124 1449 : "off"; 1125 1450 if (side === "left") kitLeft = next; else kitRight = next; 1126 1451 return next; ··· 1285 1610 const voices = []; 1286 1611 if (isWarKit(kit)) { 1287 1612 playWar(sound, letter, volume, pan, pitchFactor, "down", voices, warModelFor(kit)); 1613 + } else if (kit === "zoo") { 1614 + playZoo(sound, letter, volume, pan, pitchFactor, "down", voices); 1615 + } else if (kit === "lasers") { 1616 + playLaser(sound, letter, volume, pan, pitchFactor, "down", voices); 1288 1617 } else { 1289 1618 playPercussion(sound, letter, volume, pan, pitchFactor, "down", voices); 1290 1619 } ··· 2349 2678 const next = cycleKit(side); 2350 2679 const label = `${side} ${next === "off" ? "notes" : next}`; 2351 2680 const banner = { 2352 - off: side === "left" ? `${arrow} notes` : `notes ${arrow}`, 2353 - perc: side === "left" ? `${arrow} DRUMS` : `DRUMS ${arrow}`, 2354 - warA: side === "left" ? `${arrow} WAR A` : `WAR A ${arrow}`, 2355 - warB: side === "left" ? `${arrow} WAR B` : `WAR B ${arrow}`, 2681 + off: side === "left" ? `${arrow} notes` : `notes ${arrow}`, 2682 + perc: side === "left" ? `${arrow} DRUMS` : `DRUMS ${arrow}`, 2683 + warA: side === "left" ? `${arrow} WAR A` : `WAR A ${arrow}`, 2684 + warB: side === "left" ? `${arrow} WAR B` : `WAR B ${arrow}`, 2685 + zoo: side === "left" ? `${arrow} ZOO` : `ZOO ${arrow}`, 2686 + lasers: side === "left" ? `${arrow} LASERS` : `LASERS ${arrow}`, 2356 2687 }[next]; 2357 2688 flashPercussionNotice(banner); 2358 2689 sound?.speak?.(label); ··· 2364 2695 duration: 0.10, volume: 0.18, attack: 0.002, decay: 0.09, pan: feedbackPan, 2365 2696 }), 70); 2366 2697 // Preview the kit's signature sound on "c" so the user hears what 2367 - // they just switched to (kick for perc, pistol for whichever war model). 2698 + // they just switched to. 2368 2699 if (next === "perc") playPercussion(sound, "c", 1.6, pan); 2369 2700 else if (isWarKit(next)) playWar(sound, "c", 1.2, pan, 1.0, "both", null, warModelFor(next)); 2701 + else if (next === "zoo") playZoo(sound, "c", 1.2, pan); 2702 + else if (next === "lasers") playLaser(sound, "c", 1.2, pan); 2370 2703 return; 2371 2704 } 2372 2705 // F12 (star key): recital mode — hide UI, show only colored backdrops