chore(schemas): retire the clobbering seed script; fix the direction-of-truth text
Phase 3 (L39, ST13, ST9, ST10). - L39: deleted codegen/seed-midi-devices.ts. It regenerated — and therefore silently CLOBBERED — the hand-tuned device schemas it had originally created. Its purpose is served and git retains it. Confirmed nothing invokes it (the root package.json that once might have was deleted in Phase 1). - ST13: moved the provenance artifact out of the repo root. AUDIT CORRECTION: the finding's proposed destination, schemas/midi_devices/synth-midi-cc.sources.json, would have BROKEN codegen and CI — generate-midi-devices.ts treats every *.json directly under that directory as a device template and ajv-validates it, and the sources file has no device_id/midi/params, so the generator would exit non-zero. It went to schemas/midi_devices/sources/ instead. - ST9: schemas/schema.json's own description stated the direction of truth BACKWARDS. Rewritten: schemas are canonical, codegen emits C++ and TypeScript. (The audit cited params_notes.md; the real path is schemas/modes/params_notes.md.) - ST10: codegen/README.md rewritten against current reality — it described retired playground paths and a phantom temp dir. Descriptions are never emitted, so this is behaviour-inert; proven by regenerating all four generated directories byte-identically.
This commit is contained in:
parent
ae2657a01b
commit
7ce4598a0b
5 changed files with 653 additions and 171 deletions
|
|
@ -1,67 +1,50 @@
|
||||||
# MEMLNaut Mode-Schema Codegen
|
# codegen — schemas → C++ + TypeScript
|
||||||
|
|
||||||
Bun + TypeScript tool that turns `schemas/modes/*.json` into:
|
Bun scripts that turn the JSON schemas under `schemas/` into generated code for both
|
||||||
|
targets. The per-file inventory lives in `MAP.md` (§ `codegen/`) — this README only
|
||||||
|
covers how to run it.
|
||||||
|
|
||||||
- C++ headers under `nisps/modes/generated/<mode_id>_schema.hpp` (`constexpr` data, no runtime cost).
|
- `generate.ts` — validates `schemas/modes/*.json` against `schemas/schema.json`, emits
|
||||||
- TypeScript modules under `playground/src/modes/generated/<mode_id>_schema.ts` (typed `ModeSchema` objects).
|
`nisps/modes/generated/<mode>_schema.hpp` (+ `schema_types.hpp`) and
|
||||||
|
`manifold/src/modes/generated/<mode>_schema.ts` (+ `types.ts`, `index.ts`).
|
||||||
|
- `generate-midi-devices.ts` — validates `schemas/midi_devices/*.json` against
|
||||||
|
`schemas/midi_device.schema.json`, emits `nisps/midi/generated/midi_devices.hpp` and
|
||||||
|
`manifold/src/midi-devices/generated/{types,devices,index}.ts`.
|
||||||
|
|
||||||
The schemas are validated against `schemas/schema.json` (JSON Schema Draft 2020-12) on every run. Codegen exits non-zero if any schema fails validation.
|
Both validate with ajv (Draft 2020-12), exit non-zero on failure, and are idempotent
|
||||||
|
(re-running with unchanged schemas is byte-identical).
|
||||||
|
|
||||||
## Run
|
## Run
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd codegen
|
cd codegen
|
||||||
bun install # one-shot, fetches ajv + types
|
bun install
|
||||||
bun run generate.ts # or: bun run generate
|
bun run generate.ts # mode schemas
|
||||||
|
bun run generate-midi-devices.ts # MIDI device templates
|
||||||
```
|
```
|
||||||
|
|
||||||
The script writes everything into the two output dirs in one shot. Re-running with no schema changes is a no-op (byte-identical output).
|
Schema or codegen changes ship with the regenerated C++ **and** TypeScript in the same
|
||||||
|
commit — CI re-runs both generators and fails on any diff.
|
||||||
|
|
||||||
## Tests
|
## Golden test
|
||||||
|
|
||||||
`tests/golden_test.ts` regenerates from the live schemas into a temp dir and diffs against `tests/golden/`. The golden directory contains a snapshot of `paf_synth_schema.{hpp,ts}`. To refresh after intentional codegen changes:
|
`tests/golden_test.ts` re-runs `generate.ts` against the live output dirs, diffs
|
||||||
|
`paf_synth_schema.{hpp,ts}` against the snapshots in `tests/golden/`, then re-runs to
|
||||||
```bash
|
prove idempotency:
|
||||||
bun run generate.ts
|
|
||||||
cp ../nisps/modes/generated/paf_synth_schema.hpp tests/golden/
|
|
||||||
cp ../playground/src/modes/generated/paf_synth_schema.ts tests/golden/
|
|
||||||
```
|
|
||||||
|
|
||||||
Run the test:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bun run test
|
bun run test
|
||||||
```
|
```
|
||||||
|
|
||||||
## Adding a new mode
|
After an intentional emission change, refresh the goldens:
|
||||||
|
|
||||||
1. Drop a new `<mode_id>.json` into `schemas/modes/` (must validate against `schemas/schema.json`).
|
```bash
|
||||||
2. Run `bun run generate.ts`.
|
bun run generate.ts
|
||||||
3. Commit the JSON + the regenerated C++/TS pair.
|
cp ../nisps/modes/generated/paf_synth_schema.hpp tests/golden/
|
||||||
|
cp ../manifold/src/modes/generated/paf_synth_schema.ts tests/golden/
|
||||||
## Layout
|
|
||||||
|
|
||||||
```
|
|
||||||
codegen/
|
|
||||||
├── package.json # ajv (+ formats) + bun-types
|
|
||||||
├── tsconfig.json
|
|
||||||
├── generate.ts # ~400 lines, single entrypoint
|
|
||||||
├── README.md # this file
|
|
||||||
├── templates/ # reference templates (NOT consumed — for review)
|
|
||||||
│ ├── cpp_schema.hpp.template
|
|
||||||
│ └── ts_schema.ts.template
|
|
||||||
└── tests/
|
|
||||||
├── golden_test.ts # diffs latest output against tests/golden/
|
|
||||||
└── golden/
|
|
||||||
├── paf_synth_schema.hpp
|
|
||||||
└── paf_synth_schema.ts
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Conventions
|
## Adding a mode / device
|
||||||
|
|
||||||
- **C++ namespace**: `nisps::modes::generated`. All generated symbols are `inline constexpr` so `#include`-ing the header in multiple TUs is safe.
|
Drop the new JSON into `schemas/modes/` (or `schemas/midi_devices/`), run the matching
|
||||||
- **C++ types**: `std::array`, `std::string_view`, `std::size_t`. No `std::vector`, no heap.
|
generator, and commit the JSON plus the regenerated outputs together.
|
||||||
- **`Curve` enum**: declared in `nisps/modes/generated/schema_types.hpp` as a temporary local copy. Once stream 1 lands `nisps/core/math.hpp`, replace the local enum with an `#include` (search for `TODO(stream-1)` in the generated header).
|
|
||||||
- **Float literals**: emitted with explicit `.f` suffix and decimal point, per the perf contract (architecture §3.3).
|
|
||||||
- **Order**: schemas are processed in alphabetical order of mode_id so output is stable.
|
|
||||||
- **Naming**: `mode_id` is `snake_case` in JSON; the generated C++ const prefix is `k` + PascalCase (e.g. `kPafSynthParams`); TS const is PascalCase + `Schema` (e.g. `PafSynthSchema`).
|
|
||||||
|
|
|
||||||
|
|
@ -1,121 +0,0 @@
|
||||||
#!/usr/bin/env bun
|
|
||||||
/**
|
|
||||||
* One-time seed: derive canonical MIDI device templates (schemas/midi_devices/*.json)
|
|
||||||
* from the verified research artifact synth-midi-cc.json.
|
|
||||||
*
|
|
||||||
* Only the CC-controllable devices are emitted (has_midi_cc === true). For each,
|
|
||||||
* every {cc, name} becomes a param with a stable snake_case `id` (slugified from the
|
|
||||||
* label, de-duplicated by appending _cc<NN>), min/max from the device value range,
|
|
||||||
* a neutral default, and a coarse `group` assigned by keyword heuristic.
|
|
||||||
*
|
|
||||||
* Re-runnable and idempotent. Edit the emitted schemas/midi_devices/*.json afterwards
|
|
||||||
* if you want to hand-tune labels, groups, ranges, or defaults — they are the canonical
|
|
||||||
* source consumed by codegen/generate-midi-devices.ts. synth-midi-cc.json stays as the
|
|
||||||
* human-readable provenance/sources document.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
||||||
import { join, dirname, resolve } from "node:path";
|
|
||||||
import { fileURLToPath } from "node:url";
|
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
||||||
const REPO_ROOT = resolve(__dirname, "..");
|
|
||||||
const RESEARCH_PATH = join(REPO_ROOT, "synth-midi-cc.json");
|
|
||||||
const OUT_DIR = join(REPO_ROOT, "schemas", "midi_devices");
|
|
||||||
|
|
||||||
const DEFAULT_VALUE = 64; // neutral midpoint; only used when a param is "fixed", not ML-driven
|
|
||||||
|
|
||||||
// Map research `name` -> canonical device_id + default channel. Only these are seeded.
|
|
||||||
const DEVICE_MAP: Record<string, { device_id: string; channel: number }> = {
|
|
||||||
"Moog Sub 37": { device_id: "moog_sub37", channel: 1 },
|
|
||||||
"Creamware Pro-12 ASB": { device_id: "creamware_pro12_asb", channel: 1 },
|
|
||||||
"Elektron Analog Keys": { device_id: "elektron_analog_keys", channel: 1 },
|
|
||||||
"ASM Hydrasynth": { device_id: "asm_hydrasynth", channel: 1 },
|
|
||||||
"Roland JD-800": { device_id: "roland_jd800", channel: 1 },
|
|
||||||
"Moog Sub Phatty": { device_id: "moog_sub_phatty", channel: 1 },
|
|
||||||
};
|
|
||||||
|
|
||||||
function slugify(label: string): string {
|
|
||||||
// Drop parenthetical notes, lowercase, non-alnum -> _, collapse, trim.
|
|
||||||
let s = label.replace(/\([^)]*\)/g, " ").toLowerCase();
|
|
||||||
s = s.replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").replace(/_+/g, "_");
|
|
||||||
if (s.length === 0) s = "param";
|
|
||||||
if (!/^[a-z]/.test(s)) s = "p_" + s;
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
function groupFor(label: string): string {
|
|
||||||
const l = label.toLowerCase();
|
|
||||||
const has = (...ks: string[]) => ks.some(k => l.includes(k));
|
|
||||||
if (has("arp", "arpeggiat")) return "arp";
|
|
||||||
if (has("env", "attack", "decay", "sustain", "release", " adsr", "adr", "hold", "delay time")) return "env";
|
|
||||||
if (has("lfo")) return "lfo";
|
|
||||||
if (has("filter", "cutoff", "resonance", "res ", " res", "multidrive", "poles", "keytrack", "kb track")) return "filter";
|
|
||||||
if (has("delay", "reverb", "chorus", "flanger", "fx", "wet", "dry")) return "fx";
|
|
||||||
if (has("macro", "mod ", "mutator", "ring mod", " mod", "modulation", "wheel", "aftertouch")) return "mod";
|
|
||||||
if (has("osc", "vco", "wave", "pitch", "tune", "freq", "sub", "noise", "detune", "sync", "cent", "octave", "glide", "portamento")) return "osc";
|
|
||||||
if (has("vol", "level", "pan", "mix", "amp", "output")) return "mix";
|
|
||||||
return "global";
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ResearchParam { cc: number; name: string }
|
|
||||||
interface ResearchSynth {
|
|
||||||
name: string;
|
|
||||||
manufacturer: string;
|
|
||||||
year?: number;
|
|
||||||
has_midi_cc: boolean;
|
|
||||||
cc_parameters: ResearchParam[];
|
|
||||||
}
|
|
||||||
interface Research { synths: ResearchSynth[] }
|
|
||||||
|
|
||||||
function main(): number {
|
|
||||||
const research = JSON.parse(readFileSync(RESEARCH_PATH, "utf8")) as Research;
|
|
||||||
if (!existsSync(OUT_DIR)) mkdirSync(OUT_DIR, { recursive: true });
|
|
||||||
|
|
||||||
let count = 0;
|
|
||||||
for (const synth of research.synths) {
|
|
||||||
const map = DEVICE_MAP[synth.name];
|
|
||||||
if (!map) continue; // skip non-CC / out-of-scope devices
|
|
||||||
if (!synth.has_midi_cc || synth.cc_parameters.length === 0) continue;
|
|
||||||
|
|
||||||
const seenIds = new Set<string>();
|
|
||||||
const params = synth.cc_parameters.map(p => {
|
|
||||||
let id = slugify(p.name);
|
|
||||||
if (seenIds.has(id)) id = `${id}_cc${p.cc}`;
|
|
||||||
while (seenIds.has(id)) id = `${id}_x`;
|
|
||||||
seenIds.add(id);
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
cc: p.cc,
|
|
||||||
label: p.name,
|
|
||||||
min: 0,
|
|
||||||
max: 127,
|
|
||||||
default: DEFAULT_VALUE,
|
|
||||||
group: groupFor(p.name),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const device = {
|
|
||||||
$schema: "../midi_device.schema.json",
|
|
||||||
device_id: map.device_id,
|
|
||||||
name: synth.name,
|
|
||||||
manufacturer: synth.manufacturer,
|
|
||||||
...(synth.year ? { year: synth.year } : {}),
|
|
||||||
midi: {
|
|
||||||
default_channel: map.channel,
|
|
||||||
value_range: [0, 127],
|
|
||||||
},
|
|
||||||
params,
|
|
||||||
};
|
|
||||||
|
|
||||||
const out = join(OUT_DIR, `${map.device_id}.json`);
|
|
||||||
writeFileSync(out, JSON.stringify(device, null, 2) + "\n");
|
|
||||||
count++;
|
|
||||||
console.log(` ${map.device_id}: ${params.length} params`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`OK seeded ${count} device template(s) -> ${OUT_DIR}`);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
process.exit(main());
|
|
||||||
|
|
@ -10,7 +10,9 @@ browser engine from one set of files.
|
||||||
- `<device>.json` — one template per device. Each param:
|
- `<device>.json` — one template per device. Each param:
|
||||||
`{ id, cc, label, min, max, default, group }` — `label` is what humans see, `cc`
|
`{ id, cc, label, min, max, default, group }` — `label` is what humans see, `cc`
|
||||||
is the MIDI Control Change number, `group` is a coarse UI section.
|
is the MIDI Control Change number, `group` is a coarse UI section.
|
||||||
- Verified CC provenance + sources for every device live in `../../synth-midi-cc.json`.
|
- `sources/synth-midi-cc.json` — verified CC provenance + sources for every device
|
||||||
|
(research artifact; kept out of the top-level dir because codegen globs `*.json`
|
||||||
|
there as device templates).
|
||||||
|
|
||||||
Currently shipped (CC-controllable): `moog_sub37`, `moog_sub_phatty`,
|
Currently shipped (CC-controllable): `moog_sub37`, `moog_sub_phatty`,
|
||||||
`creamware_pro12_asb`, `elektron_analog_keys`, `asm_hydrasynth`, `roland_jd800`.
|
`creamware_pro12_asb`, `elektron_analog_keys`, `asm_hydrasynth`, `roland_jd800`.
|
||||||
|
|
@ -25,8 +27,9 @@ Emits (do **not** hand-edit the outputs):
|
||||||
- `nisps/midi/generated/midi_devices.hpp` — no-heap `constexpr` (firmware + WASM).
|
- `nisps/midi/generated/midi_devices.hpp` — no-heap `constexpr` (firmware + WASM).
|
||||||
- `manifold/src/midi-devices/generated/` — typed catalogue for the browser.
|
- `manifold/src/midi-devices/generated/` — typed catalogue for the browser.
|
||||||
|
|
||||||
Idempotent. To (re)derive these files from the research artifact:
|
Idempotent. The templates were originally machine-seeded from
|
||||||
`bun run codegen/seed-midi-devices.ts`.
|
`sources/synth-midi-cc.json` (by `codegen/seed-midi-devices.ts`, since deleted — see git
|
||||||
|
history) and are hand-tuned since; edit the `<device>.json` files directly.
|
||||||
|
|
||||||
## Using it
|
## Using it
|
||||||
|
|
||||||
|
|
|
||||||
617
schemas/midi_devices/sources/synth-midi-cc.json
Normal file
617
schemas/midi_devices/sources/synth-midi-cc.json
Normal file
|
|
@ -0,0 +1,617 @@
|
||||||
|
{
|
||||||
|
"$comment": "Compiled MIDI Continuous Controller (CC) maps for a set of hardware synths/drum machines. Researched and double-verified against official manuals + independent community sources by parallel Sonnet research agents on 2026-06-28. Each entry records confidence and verification_notes; read those before trusting an individual CC. NRPN-only parameters are NOT listed here (CC only). Where a device's panel transmits SysEx/NRPN rather than CC, that is noted.",
|
||||||
|
"generated": "2026-06-28",
|
||||||
|
"global_notes": [
|
||||||
|
"CC numbers are MSB / primary numbers. Several Moog and Creamware params support 14-bit CC where the LSB = MSB + 32 (active only in 14-bit mode).",
|
||||||
|
"Two requested names were corrected during research: the 'Creamware Pro-12 ASB' emulates the Sequential Prophet-5 (Rev 3), NOT a Prophet-12; and there is NO 'Roland RD-9' -- the RD-9 Rhythm Designer is a Behringer TR-909 clone (Roland's RD series are stage pianos; Roland's 909 reissue is the TR-09).",
|
||||||
|
"The original 1981 Korg Polysix predates MIDI entirely and has no CC support; only third-party retrofits and the Korg Legacy software emulation add MIDI."
|
||||||
|
],
|
||||||
|
"synths": [
|
||||||
|
{
|
||||||
|
"name": "Moog Sub 37",
|
||||||
|
"manufacturer": "Moog",
|
||||||
|
"year": 2014,
|
||||||
|
"midi_channel_notes": "Default MIDI In/Out channel 1 (configurable 1-16; In supports ALL/Omni). Panel controls transmit when the 'PANEL' out-filter is ON. 'KNB NRPN/CC' menu selects CC (default) or NRPN; NRPN required for full coverage of 150+ params. 'CC FORMAT' selects 7-bit (default) or 14-bit; most continuous CCs have a 14-bit form (LSB = MSB + 32, e.g. Filter Cutoff CC19 MSB / CC51 LSB). Numbers below are MSB/primary CCs.",
|
||||||
|
"has_midi_cc": true,
|
||||||
|
"cc_parameters": [
|
||||||
|
{"cc": 0, "name": "Bank Select"},
|
||||||
|
{"cc": 1, "name": "Mod Wheel"},
|
||||||
|
{"cc": 2, "name": "Reserved - MIDI Breath Control"},
|
||||||
|
{"cc": 3, "name": "LFO 1 Rate (also LFO 1 Clock Divider)"},
|
||||||
|
{"cc": 4, "name": "Mod 1 Pitch Amount"},
|
||||||
|
{"cc": 5, "name": "Glide Time"},
|
||||||
|
{"cc": 6, "name": "Reserved - Data Entry"},
|
||||||
|
{"cc": 7, "name": "Master Volume"},
|
||||||
|
{"cc": 8, "name": "LFO 2 Rate (also LFO 2 Clock Divider)"},
|
||||||
|
{"cc": 9, "name": "Osc 1 Wave"},
|
||||||
|
{"cc": 10, "name": "Reserved - MIDI Pan"},
|
||||||
|
{"cc": 11, "name": "Mod 1 Filter Amount"},
|
||||||
|
{"cc": 12, "name": "Osc 2 Frequency"},
|
||||||
|
{"cc": 13, "name": "Osc 2 Beat Frequency"},
|
||||||
|
{"cc": 14, "name": "Osc 2 Wave"},
|
||||||
|
{"cc": 15, "name": "Mod 2 Pitch Amount"},
|
||||||
|
{"cc": 16, "name": "Mod 2 Filter Amount"},
|
||||||
|
{"cc": 17, "name": "Mod 2 Pgm Dest Amount"},
|
||||||
|
{"cc": 18, "name": "Filter Multidrive"},
|
||||||
|
{"cc": 19, "name": "Filter Cutoff"},
|
||||||
|
{"cc": 20, "name": "Mod 1 Pgm Dest Amount"},
|
||||||
|
{"cc": 21, "name": "Filter Resonance"},
|
||||||
|
{"cc": 22, "name": "Filter KB Amount"},
|
||||||
|
{"cc": 23, "name": "Filter EG Attack Time"},
|
||||||
|
{"cc": 24, "name": "Filter EG Decay Time"},
|
||||||
|
{"cc": 25, "name": "Filter EG Sustain Time"},
|
||||||
|
{"cc": 26, "name": "Filter EG Release Time"},
|
||||||
|
{"cc": 27, "name": "Filter EG Amount"},
|
||||||
|
{"cc": 28, "name": "Amp EG Attack Time"},
|
||||||
|
{"cc": 29, "name": "Amp EG Decay Time"},
|
||||||
|
{"cc": 30, "name": "Amp EG Sustain Time"},
|
||||||
|
{"cc": 31, "name": "Amp EG Release Time"},
|
||||||
|
{"cc": 32, "name": "Bank Select (LSB)"},
|
||||||
|
{"cc": 33, "name": "Mod Wheel (LSB)"},
|
||||||
|
{"cc": 64, "name": "Hold Pedal/Sustain"},
|
||||||
|
{"cc": 65, "name": "Glide On"},
|
||||||
|
{"cc": 69, "name": "Arpeggiator Latch"},
|
||||||
|
{"cc": 70, "name": "Mod 1 Osc 1/2 Select"},
|
||||||
|
{"cc": 71, "name": "Mod 1 Source"},
|
||||||
|
{"cc": 72, "name": "Mod 2 Source"},
|
||||||
|
{"cc": 73, "name": "Arp On/Off"},
|
||||||
|
{"cc": 74, "name": "Osc 1 Octave"},
|
||||||
|
{"cc": 75, "name": "Osc 2 Octave"},
|
||||||
|
{"cc": 76, "name": "LFO 1 Range"},
|
||||||
|
{"cc": 77, "name": "Osc 2 Hard Sync On/Off"},
|
||||||
|
{"cc": 78, "name": "LFO 2 Range"},
|
||||||
|
{"cc": 79, "name": "Filter EG KB Amount"},
|
||||||
|
{"cc": 80, "name": "Amp EG KB Amount"},
|
||||||
|
{"cc": 81, "name": "Osc KB Reset On/Off"},
|
||||||
|
{"cc": 82, "name": "Filter EG Reset"},
|
||||||
|
{"cc": 83, "name": "Amp EG Reset"},
|
||||||
|
{"cc": 84, "name": "Reserved - Portamento Control"},
|
||||||
|
{"cc": 85, "name": "Glide Type"},
|
||||||
|
{"cc": 86, "name": "Filter EG Velocity Amount"},
|
||||||
|
{"cc": 87, "name": "Amp EG Velocity Amount"},
|
||||||
|
{"cc": 88, "name": "Mod 2 Osc 1/2 Select"},
|
||||||
|
{"cc": 89, "name": "KB Octave"},
|
||||||
|
{"cc": 91, "name": "Mod 1 Destination"},
|
||||||
|
{"cc": 92, "name": "Mod 2 Destination"},
|
||||||
|
{"cc": 93, "name": "LFO 1 KB Reset"},
|
||||||
|
{"cc": 94, "name": "Glide Legato"},
|
||||||
|
{"cc": 95, "name": "LFO 2 KB Reset"},
|
||||||
|
{"cc": 102, "name": "Glide Dest Osc 1/2/Both"},
|
||||||
|
{"cc": 103, "name": "Filter EG Delay"},
|
||||||
|
{"cc": 104, "name": "Amp EG Delay"},
|
||||||
|
{"cc": 105, "name": "Filter EG Hold"},
|
||||||
|
{"cc": 106, "name": "Amp EG Hold"},
|
||||||
|
{"cc": 107, "name": "Pitch Bend Up Amount"},
|
||||||
|
{"cc": 108, "name": "Pitch Bend Down Amount"},
|
||||||
|
{"cc": 109, "name": "Filter Slopes (Poles)"},
|
||||||
|
{"cc": 110, "name": "Osc Duo Mode On/Off"},
|
||||||
|
{"cc": 111, "name": "KB Ctrl Lo/Hi"},
|
||||||
|
{"cc": 112, "name": "Filter EG Multi Trig"},
|
||||||
|
{"cc": 113, "name": "Amp EG Multi Trig"},
|
||||||
|
{"cc": 114, "name": "Osc 1 Level"},
|
||||||
|
{"cc": 115, "name": "Osc 1 Sub Level"},
|
||||||
|
{"cc": 116, "name": "Osc 2 Level"},
|
||||||
|
{"cc": 117, "name": "Noise Level"},
|
||||||
|
{"cc": 118, "name": "Feedback/Ext Level"},
|
||||||
|
{"cc": 119, "name": "KB Transpose (receive only)"},
|
||||||
|
{"cc": 122, "name": "Local Control On/Off"}
|
||||||
|
],
|
||||||
|
"sources": [
|
||||||
|
"Official Moog Sub 37 User Manual v1.1 (SUB_37_MANUAL_v1.1_0.pdf), 'MIDI Control Change (CC) Messages' tables pp. 52-55",
|
||||||
|
"midi.guide - Moog Subsequent 37 (shares CC map with Sub 37): https://midi.guide/d/moog/subsequent-37/",
|
||||||
|
"ManualsLib - Moog Sub 37 User Manual: https://www.manualslib.com/manual/923162/Moog-Sub-37.html",
|
||||||
|
"Gearspace 'Sub37 MIDI CC MSB LSB' thread (corroborates 14-bit cutoff CC19/CC51)"
|
||||||
|
],
|
||||||
|
"confidence": "high",
|
||||||
|
"verification_notes": "Read directly from the official Moog manual PDF and cross-checked against midi.guide and ManualsLib. A real conflict was resolved: an OCR read of ManualsLib returned a +1 shift (CC20=Cutoff, CC22=Resonance); both the official manual and midi.guide confirm CC19=Cutoff, CC21=Resonance, CC18=Multidrive. Manual treated as authoritative. CC123 (All Notes Off) is standard MIDI and not in the manual's param table, so omitted. Sub 37 (2014) and Subsequent 37 (2017) share this CC map."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Creamware Pro-12 ASB",
|
||||||
|
"manufacturer": "Creamware / Sonic Core",
|
||||||
|
"year": 2005,
|
||||||
|
"midi_channel_notes": "MIDI In/Out/Thru (DIN-5) plus USB MIDI; receive channel set in PREFS. Every front-panel parameter has a dedicated CC and is transmitted when a patch is selected. External MIDI Clock can drive LFO speed. Standard Channel Mode messages (CC 120-127) implemented.",
|
||||||
|
"has_midi_cc": true,
|
||||||
|
"cc_parameters": [
|
||||||
|
{"cc": 0, "name": "Bank Select"},
|
||||||
|
{"cc": 1, "name": "Modulation"},
|
||||||
|
{"cc": 2, "name": "Unison Voices"},
|
||||||
|
{"cc": 3, "name": "Unison Detune"},
|
||||||
|
{"cc": 5, "name": "Glide Time"},
|
||||||
|
{"cc": 6, "name": "Data Entry"},
|
||||||
|
{"cc": 7, "name": "Device Volume"},
|
||||||
|
{"cc": 8, "name": "LFO Settings Retrig"},
|
||||||
|
{"cc": 9, "name": "LFO Settings Phase"},
|
||||||
|
{"cc": 11, "name": "Volume"},
|
||||||
|
{"cc": 12, "name": "Osc A Frequency"},
|
||||||
|
{"cc": 13, "name": "Osc A Shape Saw"},
|
||||||
|
{"cc": 14, "name": "Osc A Shape Pulse"},
|
||||||
|
{"cc": 15, "name": "Osc A Pulse Width"},
|
||||||
|
{"cc": 16, "name": "Osc A Sync"},
|
||||||
|
{"cc": 17, "name": "Osc B Frequency"},
|
||||||
|
{"cc": 18, "name": "Osc B Fine"},
|
||||||
|
{"cc": 19, "name": "Osc B Shape Saw"},
|
||||||
|
{"cc": 20, "name": "Osc B Shape Triangle"},
|
||||||
|
{"cc": 21, "name": "Master Tune"},
|
||||||
|
{"cc": 22, "name": "Osc B Shape Pulse"},
|
||||||
|
{"cc": 23, "name": "Aftertouch Pitch"},
|
||||||
|
{"cc": 24, "name": "Bend Range"},
|
||||||
|
{"cc": 25, "name": "Osc A Volume"},
|
||||||
|
{"cc": 26, "name": "Osc B Volume"},
|
||||||
|
{"cc": 27, "name": "Osc B Pulse Width"},
|
||||||
|
{"cc": 28, "name": "Osc B Lo Freq"},
|
||||||
|
{"cc": 29, "name": "Osc B Keyboard"},
|
||||||
|
{"cc": 30, "name": "Aftertouch Dest Freq A"},
|
||||||
|
{"cc": 31, "name": "Aftertouch Dest Freq B"},
|
||||||
|
{"cc": 32, "name": "Bank Select"},
|
||||||
|
{"cc": 33, "name": "Noise Volume"},
|
||||||
|
{"cc": 34, "name": "Aftertouch PW"},
|
||||||
|
{"cc": 35, "name": "Aftertouch MW"},
|
||||||
|
{"cc": 36, "name": "External Volume"},
|
||||||
|
{"cc": 37, "name": "Aftertouch Dest PW"},
|
||||||
|
{"cc": 38, "name": "Aftertouch Dest PW"},
|
||||||
|
{"cc": 39, "name": "Keyboard Amount"},
|
||||||
|
{"cc": 40, "name": "Cutoff"},
|
||||||
|
{"cc": 41, "name": "Resonance"},
|
||||||
|
{"cc": 42, "name": "Envelope Amount"},
|
||||||
|
{"cc": 43, "name": "Filter Env Attack"},
|
||||||
|
{"cc": 44, "name": "Filter Env Decay"},
|
||||||
|
{"cc": 45, "name": "Filter Env Sustain"},
|
||||||
|
{"cc": 46, "name": "Filter Env Velocity"},
|
||||||
|
{"cc": 47, "name": "Filter Release"},
|
||||||
|
{"cc": 48, "name": "Aftertouch LFO Freq"},
|
||||||
|
{"cc": 49, "name": "Release"},
|
||||||
|
{"cc": 50, "name": "Aftertouch Filter"},
|
||||||
|
{"cc": 51, "name": "Aftertouch Amp"},
|
||||||
|
{"cc": 52, "name": "Amp Env Attack"},
|
||||||
|
{"cc": 53, "name": "Amp Env Decay"},
|
||||||
|
{"cc": 54, "name": "Amp Env Sustain"},
|
||||||
|
{"cc": 55, "name": "Amp Env Velocity"},
|
||||||
|
{"cc": 56, "name": "MW Intensity"},
|
||||||
|
{"cc": 57, "name": "MW Offset"},
|
||||||
|
{"cc": 58, "name": "Poly Mod OSC B"},
|
||||||
|
{"cc": 59, "name": "ADR"},
|
||||||
|
{"cc": 60, "name": "Keyb Mode Retrig"},
|
||||||
|
{"cc": 61, "name": "Low Note"},
|
||||||
|
{"cc": 62, "name": "Amp Env Release"},
|
||||||
|
{"cc": 63, "name": "Poly Mod Filter Env"},
|
||||||
|
{"cc": 75, "name": "Poly Mod Destination Freq A"},
|
||||||
|
{"cc": 76, "name": "Poly Mod Destination Freq B"},
|
||||||
|
{"cc": 77, "name": "Poly Mod Destination PW"},
|
||||||
|
{"cc": 78, "name": "Poly Mod Destination PW"},
|
||||||
|
{"cc": 79, "name": "Poly Mod Destination Filter"},
|
||||||
|
{"cc": 98, "name": "Amt"},
|
||||||
|
{"cc": 108, "name": "Wheel Mod Source Mix"},
|
||||||
|
{"cc": 113, "name": "Time Velocity Attack Filter Env"},
|
||||||
|
{"cc": 114, "name": "Time Velocity Dec/Rel Filter Env"},
|
||||||
|
{"cc": 116, "name": "Time Velocity Dec/Rel Amp Env"},
|
||||||
|
{"cc": 117, "name": "Time Velocity Attack Amp Env"},
|
||||||
|
{"cc": 120, "name": "All Sounds Off"},
|
||||||
|
{"cc": 121, "name": "Local Control Off"},
|
||||||
|
{"cc": 122, "name": "All Notes Off"},
|
||||||
|
{"cc": 124, "name": "Omni Off"},
|
||||||
|
{"cc": 125, "name": "Omni On"},
|
||||||
|
{"cc": 126, "name": "Mono On"},
|
||||||
|
{"cc": 127, "name": "Poly On"}
|
||||||
|
],
|
||||||
|
"sources": [
|
||||||
|
"Creamware PRO-12 ASB Owner's Manual (Rev. 1.1), MIDI implementation table p.36",
|
||||||
|
"https://www.manualslib.com/manual/949793/Creamware-Pro-12-Asb.html",
|
||||||
|
"https://www.manualslib.com/manual/744066/Creamware-Pro-12-Asb.html",
|
||||||
|
"https://www.strumentimusicali.net/manuali/CREAMWARE_Pro12_en.pdf",
|
||||||
|
"https://www.soundonsound.com/reviews/creamware-pro-12-asb"
|
||||||
|
],
|
||||||
|
"confidence": "high",
|
||||||
|
"verification_notes": "Emulation CONFIRMED as the Sequential Circuits Prophet-5 (Rev 3), NOT the Prophet-12 (ASB = 'Authentic Sound Box'). Creamware was rebranded Sonic Core c.2007-2008. CC 0-117 synth block extracted from three independent ManualsLib renderings of the official manual and agree byte-for-byte. Two duplicate labels (CC 37/38 'Aftertouch Dest PW'; CC 77/78 'Poly Mod Destination PW') are reproduced verbatim from the manual. Coverage gap: an effects/delay/reverb CC block in the 64-112 range appeared in only one extraction and was left out to avoid OCR error (only the consistently-seen CC 98 and CC 108 were kept from it)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Elektron Analog Keys",
|
||||||
|
"manufacturer": "Elektron",
|
||||||
|
"year": 2014,
|
||||||
|
"midi_channel_notes": "CCs are per-track. Default channels (all configurable): Synth Track 1-4 = Ch 1-4, FX = Ch 5, CV = Ch 6, Performance = Ch 8, Auto Channel = Ch 9 (routes to the currently-selected synth track). Shares its CC map with the Analog Four.",
|
||||||
|
"has_midi_cc": true,
|
||||||
|
"cc_parameters": [
|
||||||
|
{"cc": 1, "name": "Modulation: Modwheel (MSB, 14-bit pair with CC 33)"},
|
||||||
|
{"cc": 2, "name": "Modulation: Breath (MSB, 14-bit pair with CC 34)"},
|
||||||
|
{"cc": 3, "name": "Performance: Parameter A"},
|
||||||
|
{"cc": 4, "name": "Performance: Parameter B"},
|
||||||
|
{"cc": 5, "name": "OSC: Slide Time"},
|
||||||
|
{"cc": 7, "name": "Amp: Volume"},
|
||||||
|
{"cc": 8, "name": "Performance: Parameter C"},
|
||||||
|
{"cc": 9, "name": "Performance: Parameter D"},
|
||||||
|
{"cc": 10, "name": "Amp: Pan"},
|
||||||
|
{"cc": 11, "name": "Performance: Parameter E"},
|
||||||
|
{"cc": 16, "name": "OSC1: Pitch (MSB, 14-bit pair with CC 48)"},
|
||||||
|
{"cc": 17, "name": "OSC2: Pitch (MSB, 14-bit pair with CC 49)"},
|
||||||
|
{"cc": 18, "name": "Filter 1: Frequency (MSB, 14-bit pair with CC 50)"},
|
||||||
|
{"cc": 19, "name": "Filter 2: Frequency (MSB, 14-bit pair with CC 51)"},
|
||||||
|
{"cc": 20, "name": "Filter Env: Depth A (MSB, 14-bit pair with CC 52)"},
|
||||||
|
{"cc": 21, "name": "Filter Env: Depth B (MSB, 14-bit pair with CC 53)"},
|
||||||
|
{"cc": 22, "name": "AUX Env: Depth A (MSB, 14-bit pair with CC 54)"},
|
||||||
|
{"cc": 23, "name": "AUX Env: Depth B (MSB, 14-bit pair with CC 55)"},
|
||||||
|
{"cc": 24, "name": "LFO1: Depth A (MSB, 14-bit pair with CC 56)"},
|
||||||
|
{"cc": 25, "name": "LFO1: Depth B (MSB, 14-bit pair with CC 57)"},
|
||||||
|
{"cc": 26, "name": "LFO2: Depth A (MSB, 14-bit pair with CC 58)"},
|
||||||
|
{"cc": 27, "name": "LFO2: Depth B (MSB, 14-bit pair with CC 59)"},
|
||||||
|
{"cc": 33, "name": "Modulation: Modwheel (LSB)"},
|
||||||
|
{"cc": 34, "name": "Modulation: Breath (LSB)"},
|
||||||
|
{"cc": 48, "name": "OSC1: Pitch (LSB)"},
|
||||||
|
{"cc": 49, "name": "OSC2: Pitch (LSB)"},
|
||||||
|
{"cc": 50, "name": "Filter 1: Frequency (LSB)"},
|
||||||
|
{"cc": 51, "name": "Filter 2: Frequency (LSB)"},
|
||||||
|
{"cc": 52, "name": "Filter Env: Depth A (LSB; MkI: 'EnvF Gate Length')"},
|
||||||
|
{"cc": 53, "name": "Filter Env: Depth B (LSB)"},
|
||||||
|
{"cc": 54, "name": "AUX Env: Depth A (LSB)"},
|
||||||
|
{"cc": 55, "name": "AUX Env: Depth B (LSB)"},
|
||||||
|
{"cc": 56, "name": "LFO1: Depth A (LSB)"},
|
||||||
|
{"cc": 57, "name": "LFO1: Depth B (LSB)"},
|
||||||
|
{"cc": 58, "name": "LFO2: Depth A (LSB)"},
|
||||||
|
{"cc": 59, "name": "LFO2: Depth B (LSB)"},
|
||||||
|
{"cc": 64, "name": "Performance: Parameter F"},
|
||||||
|
{"cc": 65, "name": "Performance: Parameter G"},
|
||||||
|
{"cc": 66, "name": "Performance: Parameter H"},
|
||||||
|
{"cc": 67, "name": "Performance: Parameter I"},
|
||||||
|
{"cc": 68, "name": "Performance: Parameter J"},
|
||||||
|
{"cc": 69, "name": "OSC1: Level"},
|
||||||
|
{"cc": 70, "name": "OSC1: Waveform"},
|
||||||
|
{"cc": 71, "name": "OSC1: Sub Oscillator"},
|
||||||
|
{"cc": 72, "name": "OSC1: Pulsewidth"},
|
||||||
|
{"cc": 73, "name": "OSC1: PWM Speed"},
|
||||||
|
{"cc": 74, "name": "OSC1: PWM Depth"},
|
||||||
|
{"cc": 75, "name": "Noise: S&H"},
|
||||||
|
{"cc": 76, "name": "Noise: Fade"},
|
||||||
|
{"cc": 77, "name": "Noise: Level"},
|
||||||
|
{"cc": 78, "name": "OSC2: Level"},
|
||||||
|
{"cc": 79, "name": "OSC2: Waveform"},
|
||||||
|
{"cc": 80, "name": "OSC2: Sub Oscillator"},
|
||||||
|
{"cc": 81, "name": "OSC2: Pulsewidth"},
|
||||||
|
{"cc": 82, "name": "OSC2: PWM Speed"},
|
||||||
|
{"cc": 83, "name": "OSC2: PWM Depth"},
|
||||||
|
{"cc": 84, "name": "OSC: Sync Amount"},
|
||||||
|
{"cc": 85, "name": "OSC: Bend Amount"},
|
||||||
|
{"cc": 86, "name": "Filter 1: Overdrive"},
|
||||||
|
{"cc": 88, "name": "OSC: Vibrato Speed (Analog Keys/MkI)"},
|
||||||
|
{"cc": 89, "name": "Filter 1: Resonance"},
|
||||||
|
{"cc": 90, "name": "Filter 2: Resonance"},
|
||||||
|
{"cc": 91, "name": "Amp: Chorus Send"},
|
||||||
|
{"cc": 92, "name": "Amp: Delay Send"},
|
||||||
|
{"cc": 93, "name": "Amp: Reverb Send"},
|
||||||
|
{"cc": 94, "name": "Track: Mute"},
|
||||||
|
{"cc": 95, "name": "Track: Level"},
|
||||||
|
{"cc": 102, "name": "Filter 1: Envelope Depth"},
|
||||||
|
{"cc": 103, "name": "Filter 2: Envelope Depth"},
|
||||||
|
{"cc": 104, "name": "Amp Env: Attack"},
|
||||||
|
{"cc": 105, "name": "Amp Env: Decay"},
|
||||||
|
{"cc": 106, "name": "Amp Env: Sustain"},
|
||||||
|
{"cc": 107, "name": "Amp Env: Release"},
|
||||||
|
{"cc": 108, "name": "Filter Env: Attack"},
|
||||||
|
{"cc": 109, "name": "Filter Env: Decay"},
|
||||||
|
{"cc": 110, "name": "Filter Env: Sustain"},
|
||||||
|
{"cc": 111, "name": "Filter Env: Release"},
|
||||||
|
{"cc": 112, "name": "AUX Env: Attack"},
|
||||||
|
{"cc": 113, "name": "AUX Env: Decay"},
|
||||||
|
{"cc": 114, "name": "AUX Env: Sustain"},
|
||||||
|
{"cc": 115, "name": "AUX Env: Release"},
|
||||||
|
{"cc": 116, "name": "LFO1: Speed"},
|
||||||
|
{"cc": 117, "name": "LFO1: Multiplier"},
|
||||||
|
{"cc": 118, "name": "LFO2: Speed"},
|
||||||
|
{"cc": 119, "name": "LFO2: Multiplier"}
|
||||||
|
],
|
||||||
|
"sources": [
|
||||||
|
"Elektron Analog Keys User Manual, Appendix D (MIDI spec, OS 1.40B) via ManualsLib",
|
||||||
|
"pencilresearch/midi GitHub: Analog Four.csv / Analog Four MKII.csv (CC BY-SA 4.0)",
|
||||||
|
"midi.guide - Elektron Analog Four / Analog Four MkII",
|
||||||
|
"Elektronauts forum MIDI implementation threads"
|
||||||
|
],
|
||||||
|
"confidence": "medium",
|
||||||
|
"verification_notes": "Cross-checked against 4 sources. The core ~32-CC set is confirmed for Analog Keys hardware. Two MkI-vs-MkII conflicts exist: (1) Vibrato Speed is CC 88 on Analog Keys/MkI (no Vibrato Depth CC); the Four MkII moved it to CC 87 and added CC 88 = Vibrato Depth. (2) The 14-bit MSB/LSB pairs (CC 16-27 MSB, 48-59 LSB) are MkII-era additions; the original Analog Keys manual lists CC 52 as 'EnvF Gate Length' and CC 53 as a standalone 7-bit param. The 14-bit pairs above reflect the later/Four MkII convention. NRPN covers params not reachable by CC (env shapes/curves, filter types, LFO waveforms/dests, FX/CV tracks, arp)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ASM Hydrasynth",
|
||||||
|
"manufacturer": "Ashun Sound Machines (ASM)",
|
||||||
|
"year": 2019,
|
||||||
|
"midi_channel_notes": "Transmits/receives 1-16. CC mode must be enabled in System Setup (Param TX/RX = CC); DEFAULT is NRPN. CC mode was introduced in firmware v1.5 (Aug 2020); v1.0 had no CC. When in CC mode, CC 6/38/98/99 serve as NRPN infrastructure. The large majority of engine params (osc/filter types, LFO shapes, mod matrix, macro targets) are NRPN-only; the ~115 CCs below are the continuous/performance subset. CC map is identical across Keyboard/Desktop/Explorer/Deluxe.",
|
||||||
|
"has_midi_cc": true,
|
||||||
|
"cc_parameters": [
|
||||||
|
{"cc": 0, "name": "Bank Select MSB"},
|
||||||
|
{"cc": 1, "name": "Mod Wheel"},
|
||||||
|
{"cc": 3, "name": "Noise Vol"},
|
||||||
|
{"cc": 5, "name": "Glide Time"},
|
||||||
|
{"cc": 7, "name": "Master Volume"},
|
||||||
|
{"cc": 8, "name": "Noise Pan"},
|
||||||
|
{"cc": 9, "name": "Ring Mod Vol"},
|
||||||
|
{"cc": 10, "name": "Ring Mod Pan"},
|
||||||
|
{"cc": 11, "name": "Expression Pedal"},
|
||||||
|
{"cc": 12, "name": "PRE-FX Param1"},
|
||||||
|
{"cc": 13, "name": "PRE-FX Param2"},
|
||||||
|
{"cc": 14, "name": "Delay Feedback"},
|
||||||
|
{"cc": 15, "name": "Delay Time"},
|
||||||
|
{"cc": 16, "name": "Macro 1"},
|
||||||
|
{"cc": 17, "name": "Macro 2"},
|
||||||
|
{"cc": 18, "name": "Macro 3"},
|
||||||
|
{"cc": 19, "name": "Macro 4"},
|
||||||
|
{"cc": 20, "name": "Macro 5"},
|
||||||
|
{"cc": 21, "name": "Macro 6"},
|
||||||
|
{"cc": 22, "name": "Macro 7"},
|
||||||
|
{"cc": 23, "name": "Macro 8"},
|
||||||
|
{"cc": 24, "name": "OSC1 WavScan"},
|
||||||
|
{"cc": 25, "name": "ENV4 Attack"},
|
||||||
|
{"cc": 26, "name": "OSC2 WavScan"},
|
||||||
|
{"cc": 27, "name": "ENV4 Decay"},
|
||||||
|
{"cc": 28, "name": "LFO2 Gain"},
|
||||||
|
{"cc": 29, "name": "Mutator1 Ratio"},
|
||||||
|
{"cc": 30, "name": "Mutator1 Depth"},
|
||||||
|
{"cc": 31, "name": "Mutator1 Dry/Wet"},
|
||||||
|
{"cc": 32, "name": "Bank Select LSB"},
|
||||||
|
{"cc": 33, "name": "Mutator2 Ratio"},
|
||||||
|
{"cc": 34, "name": "Mutator2 Depth"},
|
||||||
|
{"cc": 35, "name": "Mutator2 Dry/Wet"},
|
||||||
|
{"cc": 36, "name": "Mutator3 Ratio"},
|
||||||
|
{"cc": 37, "name": "Mutator3 Depth"},
|
||||||
|
{"cc": 39, "name": "Mutator3 Dry/Wet"},
|
||||||
|
{"cc": 40, "name": "Mutator4 Ratio"},
|
||||||
|
{"cc": 41, "name": "Mutator4 Depth"},
|
||||||
|
{"cc": 42, "name": "Mutator4 Dry/Wet"},
|
||||||
|
{"cc": 43, "name": "RM12 Depth (Ring Mod Depth)"},
|
||||||
|
{"cc": 44, "name": "OSC1 Vol"},
|
||||||
|
{"cc": 45, "name": "OSC1 Pan"},
|
||||||
|
{"cc": 46, "name": "OSC2 Vol"},
|
||||||
|
{"cc": 47, "name": "OSC2 Pan"},
|
||||||
|
{"cc": 48, "name": "OSC3 Vol"},
|
||||||
|
{"cc": 49, "name": "OSC3 Pan"},
|
||||||
|
{"cc": 50, "name": "Filter1 Drive"},
|
||||||
|
{"cc": 51, "name": "Filter1 Keytrack"},
|
||||||
|
{"cc": 52, "name": "Filter1 LFO1 Amt"},
|
||||||
|
{"cc": 53, "name": "Filter1 Vel Env"},
|
||||||
|
{"cc": 54, "name": "Filter1 ENV1 Amt"},
|
||||||
|
{"cc": 55, "name": "Filter2 Cutoff"},
|
||||||
|
{"cc": 56, "name": "Filter2 Res"},
|
||||||
|
{"cc": 57, "name": "Filter2 Type/Morph"},
|
||||||
|
{"cc": 58, "name": "Filter2 Keytrack"},
|
||||||
|
{"cc": 59, "name": "Filter2 LFO1 Amt"},
|
||||||
|
{"cc": 60, "name": "Filter2 Vel Env"},
|
||||||
|
{"cc": 61, "name": "Filter2 ENV1 Amt"},
|
||||||
|
{"cc": 62, "name": "Amp LFO2 Amt"},
|
||||||
|
{"cc": 63, "name": "Delay Wet Tone"},
|
||||||
|
{"cc": 64, "name": "Sustain Pedal"},
|
||||||
|
{"cc": 65, "name": "Reverb Time"},
|
||||||
|
{"cc": 66, "name": "Glide On/Off"},
|
||||||
|
{"cc": 67, "name": "Reverb Tone"},
|
||||||
|
{"cc": 68, "name": "POST-FX Param1"},
|
||||||
|
{"cc": 69, "name": "POST-FX Param2"},
|
||||||
|
{"cc": 70, "name": "LFO1 Gain"},
|
||||||
|
{"cc": 71, "name": "Filter1 Res"},
|
||||||
|
{"cc": 72, "name": "LFO1 Rate"},
|
||||||
|
{"cc": 73, "name": "LFO2 Rate"},
|
||||||
|
{"cc": 74, "name": "Filter1 Cutoff"},
|
||||||
|
{"cc": 75, "name": "LFO3 Gain"},
|
||||||
|
{"cc": 76, "name": "LFO3 Rate"},
|
||||||
|
{"cc": 77, "name": "LFO4 Gain"},
|
||||||
|
{"cc": 78, "name": "LFO4 Rate"},
|
||||||
|
{"cc": 79, "name": "LFO5 Gain"},
|
||||||
|
{"cc": 80, "name": "LFO5 Rate"},
|
||||||
|
{"cc": 81, "name": "ENV1 Attack"},
|
||||||
|
{"cc": 82, "name": "ENV1 Decay"},
|
||||||
|
{"cc": 83, "name": "ENV1 Sustain"},
|
||||||
|
{"cc": 84, "name": "ENV1 Release"},
|
||||||
|
{"cc": 85, "name": "ENV2 Attack"},
|
||||||
|
{"cc": 86, "name": "ENV2 Decay"},
|
||||||
|
{"cc": 87, "name": "ENV2 Sustain"},
|
||||||
|
{"cc": 88, "name": "ENV2 Release"},
|
||||||
|
{"cc": 89, "name": "ENV3 Attack"},
|
||||||
|
{"cc": 90, "name": "ENV3 Decay"},
|
||||||
|
{"cc": 91, "name": "Reverb Dry/Wet"},
|
||||||
|
{"cc": 92, "name": "Delay Dry/Wet"},
|
||||||
|
{"cc": 93, "name": "PRE-FX Mix"},
|
||||||
|
{"cc": 94, "name": "POST-FX Mix"},
|
||||||
|
{"cc": 95, "name": "Detune"},
|
||||||
|
{"cc": 96, "name": "ENV3 Sustain"},
|
||||||
|
{"cc": 97, "name": "ENV3 Release"},
|
||||||
|
{"cc": 102, "name": "ENV5 Attack"},
|
||||||
|
{"cc": 103, "name": "ENV5 Decay"},
|
||||||
|
{"cc": 104, "name": "ENV5 Sustain"},
|
||||||
|
{"cc": 105, "name": "ENV5 Release"},
|
||||||
|
{"cc": 106, "name": "ARP Division"},
|
||||||
|
{"cc": 107, "name": "ARP Gate"},
|
||||||
|
{"cc": 108, "name": "ARP Mode"},
|
||||||
|
{"cc": 109, "name": "ARP Ratchet"},
|
||||||
|
{"cc": 110, "name": "ARP Chance"},
|
||||||
|
{"cc": 111, "name": "OSC1 Cent"},
|
||||||
|
{"cc": 112, "name": "OSC2 Cent"},
|
||||||
|
{"cc": 113, "name": "OSC3 Cent"},
|
||||||
|
{"cc": 114, "name": "OSC3 F Rate"},
|
||||||
|
{"cc": 115, "name": "Noise F Rate"},
|
||||||
|
{"cc": 116, "name": "Ring Mod F Rate"},
|
||||||
|
{"cc": 117, "name": "Stereo Width"},
|
||||||
|
{"cc": 118, "name": "OSC1 F Rate"},
|
||||||
|
{"cc": 119, "name": "OSC2 F Rate"},
|
||||||
|
{"cc": 120, "name": "ARP Octave (repurposed from standard 'All Sounds Off')"},
|
||||||
|
{"cc": 122, "name": "ARP Length"},
|
||||||
|
{"cc": 123, "name": "All Notes Off"},
|
||||||
|
{"cc": 124, "name": "ENV4 Release"},
|
||||||
|
{"cc": 125, "name": "ENV4 Sustain"}
|
||||||
|
],
|
||||||
|
"sources": [
|
||||||
|
"Official ASM MIDI NRPN & CC Spec v1.5: https://www.mecldata.com/download/asm/legacy/Hydrasynth_KB_DR_MIDI_Spec_1.5.0.pdf",
|
||||||
|
"Hydrasynth Owner's Manuals v2.2.0 (KB/DR pp.116-119, Explorer pp.94-97, Deluxe pp.124-126)",
|
||||||
|
"midi.guide - ASM Hydrasynth (115 params, updated Sep 2024): https://midi.guide/d/asm/hydrasynth/",
|
||||||
|
"pencilresearch/midi GitHub: ASM/Hydrasynth.csv",
|
||||||
|
"eclab/edisyn reverse-engineered nrpn.csv (notes the official PDF has errors)"
|
||||||
|
],
|
||||||
|
"confidence": "high",
|
||||||
|
"verification_notes": "115 CC assignments cross-verified across the official ASM spec, current owner's manuals, midi.guide and the pencilresearch CSV - all consistent. CC 120 is repurposed for ARP Octave (standard 'All Sounds Off' will move ARP Octave instead). Gaps: CC 2,4,6,38,98-101,121,126-127 unused. ENV4 (Amp env) ADSR is non-contiguous: A=25, D=27, S=125, R=124. An older Explorer-manual page showed a divergent layout (likely a render artifact); the v2.2.0 manuals + spec values above are authoritative. Hundreds of additional params are NRPN-only."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Roland JD-800",
|
||||||
|
"manufacturer": "Roland",
|
||||||
|
"year": 1991,
|
||||||
|
"midi_channel_notes": "Transmits/receives on a configurable channel (1-16); SysEx Device ID defaults to 17. IMPORTANT: the front-panel sliders do NOT transmit CC -- they emit Roland SysEx (F0 41 dev 3D 12 ... F7), and 'TX Edit Data' must be enabled. The synth RECEIVES a small set of standard CCs. There is no NRPN. (The 2021 JD-08 Boutique reissue is a different architecture that maps ~80 CCs.)",
|
||||||
|
"has_midi_cc": true,
|
||||||
|
"cc_parameters": [
|
||||||
|
{"cc": 1, "name": "Modulation"},
|
||||||
|
{"cc": 2, "name": "Breath Controller"},
|
||||||
|
{"cc": 5, "name": "Portamento Time"},
|
||||||
|
{"cc": 6, "name": "Data Entry MSB (RPN; receive only)"},
|
||||||
|
{"cc": 7, "name": "Volume / Part Level"},
|
||||||
|
{"cc": 10, "name": "Pan"},
|
||||||
|
{"cc": 11, "name": "Expression"},
|
||||||
|
{"cc": 64, "name": "Hold / Sustain Pedal"},
|
||||||
|
{"cc": 65, "name": "Portamento On/Off"},
|
||||||
|
{"cc": 100, "name": "RPN LSB (receive only)"},
|
||||||
|
{"cc": 101, "name": "RPN MSB (receive only)"},
|
||||||
|
{"cc": 121, "name": "Reset All Controllers (receive only)"},
|
||||||
|
{"cc": 123, "name": "All Notes Off (receive only)"}
|
||||||
|
],
|
||||||
|
"sources": [
|
||||||
|
"Roland JD-800 Owner's Manual / MIDI Implementation (Owner's Manual II, pp. V-53 to V-65): https://cdn.roland.com/assets/media/pdf/JD-800_OM.pdf",
|
||||||
|
"Roland JD-800 MIDI Annex (deepsonic archive): https://www.deepsonic.ch/deep/docs_manuals/roland_jd-800_midi_anleitung.pdf",
|
||||||
|
"Roland Support: 'Transmitting the JD-800's Slider Information to a Sequencer'",
|
||||||
|
"Vintage Synth Explorer forum (confirms SysEx-only slider transmission): https://forum.vintagesynth.com/viewtopic.php?t=52284",
|
||||||
|
"Gearspace 'JD800 MIDI automation' thread"
|
||||||
|
],
|
||||||
|
"confidence": "medium",
|
||||||
|
"verification_notes": "The core received CCs (1, 5, 7, 10, 64, 65) are confirmed across the official manual proxy, Roland support, and forum sources. The defining JD-800 fact: every synthesis parameter (cutoff, resonance, envelopes, LFO, FX) is transmitted/edited via Roland SysEx, NOT CC -- the famous sliders emit SysEx only, so DAW automation needs a SysEx<->CC translator (e.g. Bome). The original manual's MIDI chart pages are image-scanned and not machine-extractable, so the extended receive list (CC 2, 6, 100/101, 121, 123) carries slightly lower confidence than the core set. Do not confuse with the JD-08 reissue."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Korg Polysix",
|
||||||
|
"manufacturer": "Korg",
|
||||||
|
"year": 1981,
|
||||||
|
"midi_channel_notes": "The original 1981 Polysix has no MIDI port and no MIDI channel concept -- it predates the MIDI standard (1983). MIDI/CC exist only on aftermarket retrofits (channel-selectable 1-16) and the Korg Legacy software emulation (follows host channel).",
|
||||||
|
"has_midi_cc": false,
|
||||||
|
"cc_parameters": [],
|
||||||
|
"sources": [
|
||||||
|
"https://en.wikipedia.org/wiki/Korg_Polysix",
|
||||||
|
"https://www.vintagesynth.com/korg/polysix",
|
||||||
|
"https://www.soundonsound.com/reviews/korg-legacy-collection-part-3",
|
||||||
|
"CHD Elektroservis Polysix MIDI interface manuals (P6-KBD, PS-6M): https://www.chd-el.cz/products/korg-polysix-midi-interface/",
|
||||||
|
"Kiwitechnics KiwiSix: http://www.kiwitechnics.com/kiwisix.htm",
|
||||||
|
"Kenton Polysix MIDI kit: https://kentonuk.com/product/polysix/",
|
||||||
|
"Tubbutec ModyPoly: https://tubbutec.de/modypoly/"
|
||||||
|
],
|
||||||
|
"confidence": "high",
|
||||||
|
"verification_notes": "GOTCHA CONFIRMED: the original 1981 Korg Polysix (PS-6) is a PRE-MIDI analog polysynth with NO native MIDI (MIDI standardized 1983). It has only CV/arpeggiator-sync-era connectivity. Therefore has_midi_cc=false and cc_parameters=[] for the original hardware. CC/MIDI exist only on (1) third-party retrofits -- CHD P6-KBD (receive-only; documented CCs incl. CC64 Hold and custom CC16-19) and the fuller CHD PS-6M (CC1 Modulation, CC7 Volume, CC64 Hold, CC70 Chord Memory, CC71 Arpeggiator, CC98/99 NRPN, CC122 Local, CC123 All Notes Off), plus Kenton, Tubbutec ModyPoly and Kiwitechnics KiwiSix mainboard (advertises full CC/NRPN but no public per-CC chart); and (2) the Korg Legacy Collection software emulation (only documented mapping: CC1 -> Vibrato Intensity, rest via MIDI-learn). Any 'Polysix' CC numbers belong to those, not the 1981 unit. No CC numbers were invented."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Roland RD-9 Rhythm Designer",
|
||||||
|
"manufacturer": "Behringer (NOT Roland -- see notes)",
|
||||||
|
"year": 2021,
|
||||||
|
"midi_channel_notes": "Settable per device on SETTINGS > MIDI: MIDI IN channel 1-16/All/Out, separate MIDI OUT channel 1-16, plus independent USB MIDI channels and a SysEx Device ID (1-16). Voices are triggered by MIDI NOTES (note map below), not CC. The official manual documents no CC implementation. Accent is sent via high note velocity.",
|
||||||
|
"has_midi_cc": false,
|
||||||
|
"cc_parameters": [],
|
||||||
|
"community_reported_cc_unofficial": [
|
||||||
|
{"cc": 75, "name": "Hi-Hat / Crash Pitch (receive-only; could glitch Kick pre-fw 1.1.4)"},
|
||||||
|
{"cc": 76, "name": "Hi-Hat / Crash Pitch (second of pair)"},
|
||||||
|
{"cc": 77, "name": "Ride Pitch"}
|
||||||
|
],
|
||||||
|
"note_map": {
|
||||||
|
"Bass Drum": 36,
|
||||||
|
"Snare Drum": 40,
|
||||||
|
"Low Tom / Low Conga": 45,
|
||||||
|
"Mid Tom / Mid Conga": 47,
|
||||||
|
"Hi Tom / Hi Conga": 50,
|
||||||
|
"Rim Shot / Claves": 37,
|
||||||
|
"Clap / Maracas": 39,
|
||||||
|
"Cowbell": 56,
|
||||||
|
"Cymbal": 51,
|
||||||
|
"Open Hat": 46,
|
||||||
|
"Closed Hat": 42
|
||||||
|
},
|
||||||
|
"sources": [
|
||||||
|
"Behringer RD-9 User Manual (note map p.17): https://www.manualslib.com/manual/2367155/Behringer-Rd-9.html",
|
||||||
|
"Behringer RD-9 manual PDF: https://mediadl.musictribe.com/media/PLM/data/docs/P0DG6/P0DG6_RD-9_M_EN.pdf",
|
||||||
|
"Gearspace 'Behringer RD9 sending midi' thread (CC 75/76/77, community-sourced)",
|
||||||
|
"Behringer product page: https://www.behringer.com/en/products/0704-AAB",
|
||||||
|
"For a true Roland 909 with a published CC chart: Roland TR-09 MIDI Implementation: https://static.roland.com/assets/media/pdf/TR-09_MIDI_Imple_e01_W.pdf"
|
||||||
|
],
|
||||||
|
"confidence": "high (that NO official CC chart exists); low (for the community CC values)",
|
||||||
|
"verification_notes": "NAME CORRECTION: there is NO 'Roland RD-9'. The RD-9 'Rhythm Designer' is a BEHRINGER product (Music Tribe, 2021), an analog/digital TR-909 clone. Roland's RD-x models are stage pianos; Roland's official 909 reissue is the TR-09 and the fuller TR-8S (both publish per-instrument CC charts). The full 38-page official Behringer RD-9 manual was downloaded and exhaustively text-searched: it contains ZERO Control Change / NRPN / RPN mappings -- only the MIDI NOTE trigger map (above) plus SysEx dumps and clock. So has_midi_cc=false and cc_parameters=[] (per-instrument Tune/Decay/Level knobs are physical controls, not CC-addressable). The 'community_reported_cc_unofficial' CC 75/76/77 (cymbal pitch) are forum-sourced, receive-only, firmware-dependent, and NOT in any official documentation. One earlier research pass reported CC 62/63/64 as manual-sourced, but the full-manual read found no such chart, so those are rejected. The TR-909/TR-8S charts were deliberately NOT substituted and no CC numbers were invented. For a documented per-voice CC map, use the Roland TR-8S (e.g. BD Tune CC20, BD Decay CC23) or TR-09."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Moog Sub Phatty",
|
||||||
|
"manufacturer": "Moog",
|
||||||
|
"year": 2013,
|
||||||
|
"midi_channel_notes": "MIDI In channel set by holding SHIFT + a keyboard key (low C to D# = Ch 1-16). Nearly every panel knob/switch transmits and receives CC. CC3 (LFO Rate) doubles as an LFO clock-divider selector when LFO MIDI Sync is on. Continuous params support optional 14-bit CC (LSB = MSB + 32). Distinct CC map from the Sub 37 / Subsequent 37.",
|
||||||
|
"has_midi_cc": true,
|
||||||
|
"cc_parameters": [
|
||||||
|
{"cc": 1, "name": "Modulation Wheel"},
|
||||||
|
{"cc": 2, "name": "Filter Mod Amount"},
|
||||||
|
{"cc": 3, "name": "LFO Rate"},
|
||||||
|
{"cc": 4, "name": "Pitch Mod Amount"},
|
||||||
|
{"cc": 5, "name": "Glide Rate"},
|
||||||
|
{"cc": 7, "name": "Output Level (Volume)"},
|
||||||
|
{"cc": 8, "name": "Noise Level"},
|
||||||
|
{"cc": 9, "name": "VCO 1 Wave"},
|
||||||
|
{"cc": 12, "name": "VCO 2 Frequency"},
|
||||||
|
{"cc": 13, "name": "VCO 2 Beat Frequency"},
|
||||||
|
{"cc": 14, "name": "VCO 2 Wave"},
|
||||||
|
{"cc": 15, "name": "VCO 1 Level"},
|
||||||
|
{"cc": 16, "name": "VCO 2 Level"},
|
||||||
|
{"cc": 17, "name": "VCO 1 Sub Level"},
|
||||||
|
{"cc": 18, "name": "Multidrive Amount"},
|
||||||
|
{"cc": 19, "name": "Filter Cutoff Frequency"},
|
||||||
|
{"cc": 20, "name": "Wave Mod Amount"},
|
||||||
|
{"cc": 21, "name": "Filter Resonance"},
|
||||||
|
{"cc": 22, "name": "Filter EG Amount"},
|
||||||
|
{"cc": 23, "name": "Filter EG Attack"},
|
||||||
|
{"cc": 24, "name": "Filter EG Decay"},
|
||||||
|
{"cc": 25, "name": "Filter EG Sustain"},
|
||||||
|
{"cc": 26, "name": "Filter EG Release"},
|
||||||
|
{"cc": 27, "name": "Filter KB Tracking Amount"},
|
||||||
|
{"cc": 28, "name": "Amplifier EG Attack"},
|
||||||
|
{"cc": 29, "name": "Amplifier EG Decay"},
|
||||||
|
{"cc": 30, "name": "Amplifier EG Sustain"},
|
||||||
|
{"cc": 31, "name": "Amplifier EG Release"},
|
||||||
|
{"cc": 65, "name": "Glide On/Off"},
|
||||||
|
{"cc": 68, "name": "Legato"},
|
||||||
|
{"cc": 70, "name": "Pitch Mod OSC 2 Only"},
|
||||||
|
{"cc": 71, "name": "Modulation Source"},
|
||||||
|
{"cc": 72, "name": "Wave Mod Destination"},
|
||||||
|
{"cc": 73, "name": "Gated Glide"},
|
||||||
|
{"cc": 74, "name": "VCO 1 Octave"},
|
||||||
|
{"cc": 75, "name": "VCO 2 Octave"},
|
||||||
|
{"cc": 76, "name": "LFO Range"},
|
||||||
|
{"cc": 77, "name": "VCO Hard Sync"},
|
||||||
|
{"cc": 78, "name": "LFO KB Tracking Amount"},
|
||||||
|
{"cc": 79, "name": "Filter EG Velocity to Time"},
|
||||||
|
{"cc": 80, "name": "Amplifier EG Velocity to Time"},
|
||||||
|
{"cc": 81, "name": "VCO Gate Reset"},
|
||||||
|
{"cc": 82, "name": "Filter EG Reset"},
|
||||||
|
{"cc": 83, "name": "Amplifier EG Reset"},
|
||||||
|
{"cc": 85, "name": "Glide Type"},
|
||||||
|
{"cc": 86, "name": "Filter EG Repeat"},
|
||||||
|
{"cc": 87, "name": "Amplifier EG Repeat"},
|
||||||
|
{"cc": 88, "name": "Release On/Off"},
|
||||||
|
{"cc": 89, "name": "Keyboard Octave"},
|
||||||
|
{"cc": 90, "name": "Filter EG KB Amount"},
|
||||||
|
{"cc": 91, "name": "Amplifier EG KB Amount"},
|
||||||
|
{"cc": 92, "name": "Amplifier EG Velocity to Amplitude"},
|
||||||
|
{"cc": 93, "name": "LFO Gate Reset"},
|
||||||
|
{"cc": 94, "name": "Glide Legato"},
|
||||||
|
{"cc": 102, "name": "LFO MIDI Sync"},
|
||||||
|
{"cc": 103, "name": "Filter EG Delay"},
|
||||||
|
{"cc": 104, "name": "Amplifier EG Delay"},
|
||||||
|
{"cc": 105, "name": "Filter EG Hold"},
|
||||||
|
{"cc": 106, "name": "Amplifier EG Hold"},
|
||||||
|
{"cc": 107, "name": "Pitch Bend Up"},
|
||||||
|
{"cc": 108, "name": "Pitch Bend Down"},
|
||||||
|
{"cc": 109, "name": "Filter Poles"},
|
||||||
|
{"cc": 110, "name": "Filter EG Velocity to Amplitude"},
|
||||||
|
{"cc": 111, "name": "Note Priority"},
|
||||||
|
{"cc": 113, "name": "Filter EG Gate Source"},
|
||||||
|
{"cc": 114, "name": "Volume (Amplifier) EG Gate Source"},
|
||||||
|
{"cc": 115, "name": "Filter EG Trigger Mode"},
|
||||||
|
{"cc": 116, "name": "External Input Level"},
|
||||||
|
{"cc": 117, "name": "Filter EG Fast Decay"},
|
||||||
|
{"cc": 118, "name": "Amplifier EG Fast Decay"},
|
||||||
|
{"cc": 119, "name": "Keyboard Transpose"}
|
||||||
|
],
|
||||||
|
"sources": [
|
||||||
|
"Official Moog Sub Phatty User Manual (Sub_Phatty_Manual.pdf), MIDI parameter tables pp. 38-41",
|
||||||
|
"https://www.manualslib.com/manual/564539/Moog-Sub-Phatty.html",
|
||||||
|
"midi.guide - Moog Sub Phatty: https://midi.guide/d/moog/sub-phatty/",
|
||||||
|
"Moog forum Sub Phatty MIDI CC threads"
|
||||||
|
],
|
||||||
|
"confidence": "high",
|
||||||
|
"verification_notes": "Built from the official Moog manual MIDI tables (pp. 39-41 via ManualsLib) cross-checked against midi.guide and two Moog forum threads. Conflicts resolved: (1) manual OCR was internally inconsistent for CC 18/22/27 -- used the cleaner midi.guide mapping (CC18=Multidrive, CC22=Filter EG Amount, CC27=Filter KB Tracking); (2) CC65=Glide On/Off, CC107/108=Pitch Bend Up/Down (midi.guide + forum over an OCR shift); (3) CC94=Glide Legato. The forum notes a few documented CCs (CC7, CC68, CC88) reportedly did nothing in fw 2.2.0. Confirmed distinct from the Sub 37 CC map, not copied."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"$id": "https://memlnaut/schemas/mode.schema.json",
|
"$id": "https://memlnaut/schemas/mode.schema.json",
|
||||||
"title": "MEMLNaut Mode Schema",
|
"title": "MEMLNaut Mode Schema",
|
||||||
"description": "Describes the parameter contract, ML config, and UI hints for one MEMLNaut mode. Authoritative source: firmware C++ code under modes/ and voicespaces/. Codegen consumes this to emit C++ headers and TypeScript types.",
|
"description": "Meta-schema for one MEMLNaut mode: parameter contract, ML config, and UI hints. The schemas/modes/*.json files it validates are the canonical contract — codegen/generate.ts emits the C++ headers (nisps/modes/generated/) and TypeScript modules (manifold/src/modes/generated/) from them. Provenance notes and judgement calls live in schemas/modes/params_notes.md.",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"mode_id",
|
"mode_id",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue