feat(playground): MIDI CC device presets + bug fixes
Add MIDI CC preset system for bundled device configurations: - midi-cc-presets.js: listPresets/loadPreset/loadPresetFromFile API - presets/polybrute.json: Arturia PolyBrute CC map - Preset selector in quick controls bar and MIDI CC drawer - File import button for loading JSON presets from disk Bug fixes in a-app.js: - Destroy IML instances before resizeMLP() to free WASM memory - Fix randomiseWeights() call (was drawWeights()) - Fix thumbs-up to store rawParamValues instead of post-pipeline outputs
This commit is contained in:
parent
aa0ffcfd32
commit
0a112f2d84
5 changed files with 292 additions and 5 deletions
|
|
@ -64,6 +64,9 @@
|
||||||
<select class="preset-select" id="midi-cc-output-select" title="MIDI output device">
|
<select class="preset-select" id="midi-cc-output-select" title="MIDI output device">
|
||||||
<option value="">No device</option>
|
<option value="">No device</option>
|
||||||
</select>
|
</select>
|
||||||
|
<select class="preset-select" id="midi-cc-quick-preset" title="MIDI CC preset" style="max-width:130px">
|
||||||
|
<option value="">Manual</option>
|
||||||
|
</select>
|
||||||
<span class="synth-status" id="midi-cc-status" style="font-size:0.65rem;opacity:0.7"></span>
|
<span class="synth-status" id="midi-cc-status" style="font-size:0.65rem;opacity:0.7"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -269,6 +272,13 @@
|
||||||
<button class="drawer-close" data-drawer="midi-cc">×</button>
|
<button class="drawer-close" data-drawer="midi-cc">×</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="drawer-body" id="midi-cc-panel">
|
<div class="drawer-body" id="midi-cc-panel">
|
||||||
|
<div class="synth-row">
|
||||||
|
<label>Preset</label>
|
||||||
|
<select id="midi-cc-preset-select" style="flex:1;min-width:0">
|
||||||
|
<option value="">Manual</option>
|
||||||
|
</select>
|
||||||
|
<button class="action-btn" id="midi-cc-file-import" title="Load preset from JSON file">File</button>
|
||||||
|
</div>
|
||||||
<div class="synth-row">
|
<div class="synth-row">
|
||||||
<label>Parameters</label>
|
<label>Parameters</label>
|
||||||
<span id="midi-cc-count">8</span>
|
<span id="midi-cc-count">8</span>
|
||||||
|
|
@ -276,8 +286,8 @@
|
||||||
<button class="action-btn" id="midi-cc-remove" title="Remove last CC parameter">−</button>
|
<button class="action-btn" id="midi-cc-remove" title="Remove last CC parameter">−</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="synth-row">
|
<div class="synth-row">
|
||||||
<button class="action-btn" id="midi-cc-import">Import</button>
|
<button class="action-btn" id="midi-cc-import">Paste JSON</button>
|
||||||
<button class="action-btn" id="midi-cc-export">Export</button>
|
<button class="action-btn" id="midi-cc-export">Copy JSON</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="midi-cc-param-list"></div>
|
<div id="midi-cc-param-list"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import { MIDIInput } from './synth/midi-input.js';
|
||||||
import { SYNTH_PARAM_MAP, SYNTH_PARAM_NAMES, SYNTH_PARAM_COLORS, applyCurve, applyGroupOverride } from './synth/param-map.js';
|
import { SYNTH_PARAM_MAP, SYNTH_PARAM_NAMES, SYNTH_PARAM_COLORS, applyCurve, applyGroupOverride } from './synth/param-map.js';
|
||||||
import { MIDIOutput } from './midi/midi-output.js';
|
import { MIDIOutput } from './midi/midi-output.js';
|
||||||
import { loadCCMap, saveCCMap, createCCParam, exportCCMap, importCCMap, CC_NAMES } from './midi/midi-cc-map.js';
|
import { loadCCMap, saveCCMap, createCCParam, exportCCMap, importCCMap, CC_NAMES } from './midi/midi-cc-map.js';
|
||||||
|
import { listPresets as listMidiPresets, loadPreset as loadMidiPreset, loadPresetFromFile as loadMidiPresetFromFile } from './midi/midi-cc-presets.js';
|
||||||
import { GamepadInput } from './ui/gamepad.js';
|
import { GamepadInput } from './ui/gamepad.js';
|
||||||
import { HandTracker } from './ui/hand-tracker.js';
|
import { HandTracker } from './ui/hand-tracker.js';
|
||||||
import { createDevPanel } from './ui/dev-panel.js';
|
import { createDevPanel } from './ui/dev-panel.js';
|
||||||
|
|
@ -719,6 +720,10 @@ async function resizeMLP(newOutputCount) {
|
||||||
if (newOutputCount === N_OUTPUTS) return;
|
if (newOutputCount === N_OUTPUTS) return;
|
||||||
N_OUTPUTS = newOutputCount;
|
N_OUTPUTS = newOutputCount;
|
||||||
|
|
||||||
|
// Destroy old IML instances (free WASM memory)
|
||||||
|
if (imlJoy) imlJoy.destroy();
|
||||||
|
if (imlHand) imlHand.destroy();
|
||||||
|
|
||||||
imlJoy = await WasmIML.create(N_JOY_INPUTS, N_OUTPUTS, [32, 48, 64], 1000, 1.0, 0.00001);
|
imlJoy = await WasmIML.create(N_JOY_INPUTS, N_OUTPUTS, [32, 48, 64], 1000, 1.0, 0.00001);
|
||||||
imlJoy.setLogger(msg => console.log('[NISPS:joy]', msg));
|
imlJoy.setLogger(msg => console.log('[NISPS:joy]', msg));
|
||||||
imlHand = await WasmIML.create(N_HAND_INPUTS, N_OUTPUTS, [48, 48, 64], 1000, 1.0, 0.00001);
|
imlHand = await WasmIML.create(N_HAND_INPUTS, N_OUTPUTS, [48, 48, 64], 1000, 1.0, 0.00001);
|
||||||
|
|
@ -729,8 +734,8 @@ async function resizeMLP(newOutputCount) {
|
||||||
rawParamValues = new Array(N_OUTPUTS).fill(0.5);
|
rawParamValues = new Array(N_OUTPUTS).fill(0.5);
|
||||||
|
|
||||||
// Randomize with current spread
|
// Randomize with current spread
|
||||||
imlJoy.drawWeights(spreadLevel);
|
imlJoy.randomiseWeights(spreadLevel);
|
||||||
imlHand.drawWeights(spreadLevel);
|
imlHand.randomiseWeights(spreadLevel);
|
||||||
|
|
||||||
// Re-run inference
|
// Re-run inference
|
||||||
iml.setInput(0, joyX);
|
iml.setInput(0, joyX);
|
||||||
|
|
@ -1922,7 +1927,7 @@ function onThumbsUp() {
|
||||||
|
|
||||||
pushUndoSnapshot();
|
pushUndoSnapshot();
|
||||||
const inputs = getCurrentInputs();
|
const inputs = getCurrentInputs();
|
||||||
const outputs = [...iml.getOutputs()];
|
const outputs = [...rawParamValues];
|
||||||
iml.addExample(inputs, outputs);
|
iml.addExample(inputs, outputs);
|
||||||
|
|
||||||
noiseLevel *= rlExplorationDecay;
|
noiseLevel *= rlExplorationDecay;
|
||||||
|
|
@ -2269,6 +2274,37 @@ async function initMIDIControls() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- MIDI CC Preset Application ----
|
||||||
|
async function applyMidiCCPreset(preset) {
|
||||||
|
// Replace the CC map with preset params
|
||||||
|
midiCCMap.length = 0;
|
||||||
|
midiCCMap.push(...preset.params.map(p => ({ ...p })));
|
||||||
|
|
||||||
|
// Rebuild overrides
|
||||||
|
midiCCOverrides.length = 0;
|
||||||
|
midiCCOverrides.push(...preset.params.map(p => ({
|
||||||
|
min: p.min ?? 0, max: p.max ?? 1, curve: p.curve ?? 0.5,
|
||||||
|
frozen: p.muted ?? false, fixedValue: p.fixedValue ?? 0.5,
|
||||||
|
})));
|
||||||
|
|
||||||
|
_generateCCColors(midiCCMap.length);
|
||||||
|
saveCCMap(midiCCMap);
|
||||||
|
|
||||||
|
// Resize MLP if in midi-cc mode
|
||||||
|
if (outputMode === 'midi-cc') {
|
||||||
|
await resizeMLP(midiCCMap.length);
|
||||||
|
buildHeatmap();
|
||||||
|
updateHeatmap(iml.getOutputs());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update UI
|
||||||
|
const countEl = document.getElementById('midi-cc-count');
|
||||||
|
if (countEl) countEl.textContent = midiCCMap.length;
|
||||||
|
buildMidiCCParamList();
|
||||||
|
|
||||||
|
console.log(`[NISPS] Applied MIDI CC preset: ${preset.name} (${preset.params.length} params)`);
|
||||||
|
}
|
||||||
|
|
||||||
// ---- MIDI CC Controls ----
|
// ---- MIDI CC Controls ----
|
||||||
async function initMIDICCControls() {
|
async function initMIDICCControls() {
|
||||||
const enableBtn = document.getElementById('midi-cc-enable-btn');
|
const enableBtn = document.getElementById('midi-cc-enable-btn');
|
||||||
|
|
@ -2279,6 +2315,8 @@ async function initMIDICCControls() {
|
||||||
const removeBtn = document.getElementById('midi-cc-remove');
|
const removeBtn = document.getElementById('midi-cc-remove');
|
||||||
const importBtn = document.getElementById('midi-cc-import');
|
const importBtn = document.getElementById('midi-cc-import');
|
||||||
const exportBtn = document.getElementById('midi-cc-export');
|
const exportBtn = document.getElementById('midi-cc-export');
|
||||||
|
const presetSelect = document.getElementById('midi-cc-preset-select');
|
||||||
|
const fileImportBtn = document.getElementById('midi-cc-file-import');
|
||||||
|
|
||||||
if (!enableBtn || !outputSelect) return;
|
if (!enableBtn || !outputSelect) return;
|
||||||
|
|
||||||
|
|
@ -2395,6 +2433,85 @@ async function initMIDICCControls() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Quick preset selector (in quick controls bar)
|
||||||
|
const quickPresetSelect = document.getElementById('midi-cc-quick-preset');
|
||||||
|
if (quickPresetSelect) {
|
||||||
|
quickPresetSelect.innerHTML = '<option value="">Manual</option>';
|
||||||
|
for (const p of listMidiPresets()) {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = p.id;
|
||||||
|
opt.textContent = p.name;
|
||||||
|
quickPresetSelect.appendChild(opt);
|
||||||
|
}
|
||||||
|
quickPresetSelect.addEventListener('change', async () => {
|
||||||
|
const id = quickPresetSelect.value;
|
||||||
|
if (!id) return;
|
||||||
|
try {
|
||||||
|
const preset = await loadMidiPreset(id);
|
||||||
|
await applyMidiCCPreset(preset);
|
||||||
|
updateCountDisplay();
|
||||||
|
if (statusEl) statusEl.textContent = `Loaded: ${preset.name}`;
|
||||||
|
// Sync drawer preset selector
|
||||||
|
if (presetSelect) presetSelect.value = id;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[MIDI CC] Failed to load preset:', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preset selector (in drawer)
|
||||||
|
if (presetSelect) {
|
||||||
|
// Populate with built-in presets
|
||||||
|
presetSelect.innerHTML = '<option value="">Manual</option>';
|
||||||
|
for (const p of listMidiPresets()) {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = p.id;
|
||||||
|
opt.textContent = p.name;
|
||||||
|
presetSelect.appendChild(opt);
|
||||||
|
}
|
||||||
|
presetSelect.addEventListener('change', async () => {
|
||||||
|
const id = presetSelect.value;
|
||||||
|
if (!id) return; // "Manual" selected — keep current config
|
||||||
|
try {
|
||||||
|
const preset = await loadMidiPreset(id);
|
||||||
|
await applyMidiCCPreset(preset);
|
||||||
|
updateCountDisplay();
|
||||||
|
if (statusEl) statusEl.textContent = `Loaded: ${preset.name}`;
|
||||||
|
// Sync quick preset selector
|
||||||
|
if (quickPresetSelect) quickPresetSelect.value = id;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[MIDI CC] Failed to load preset:', err);
|
||||||
|
if (statusEl) statusEl.textContent = `Error: ${err.message}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// File import (JSON file from disk)
|
||||||
|
if (fileImportBtn) {
|
||||||
|
// Create hidden file input
|
||||||
|
const fileInput = document.createElement('input');
|
||||||
|
fileInput.type = 'file';
|
||||||
|
fileInput.accept = '.json';
|
||||||
|
fileInput.style.display = 'none';
|
||||||
|
document.body.appendChild(fileInput);
|
||||||
|
|
||||||
|
fileImportBtn.addEventListener('click', () => fileInput.click());
|
||||||
|
fileInput.addEventListener('change', async () => {
|
||||||
|
const file = fileInput.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
const preset = await loadMidiPresetFromFile(file);
|
||||||
|
if (!preset) {
|
||||||
|
alert('Invalid MIDI CC preset file');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await applyMidiCCPreset(preset);
|
||||||
|
updateCountDisplay();
|
||||||
|
if (statusEl) statusEl.textContent = `Loaded: ${preset.name || file.name}`;
|
||||||
|
// Reset file input so the same file can be re-selected
|
||||||
|
fileInput.value = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Build the per-param editor in the drawer
|
// Build the per-param editor in the drawer
|
||||||
buildMidiCCParamList();
|
buildMidiCCParamList();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
60
playground/js/midi/midi-cc-presets.js
Normal file
60
playground/js/midi/midi-cc-presets.js
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
// MIDI CC Presets — bundled device configurations
|
||||||
|
// Each preset is a JSON file with { name, description, channel, params[] }
|
||||||
|
|
||||||
|
const PRESET_URLS = {
|
||||||
|
'polybrute': './js/midi/presets/polybrute.json',
|
||||||
|
};
|
||||||
|
|
||||||
|
// In-memory cache
|
||||||
|
const _cache = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of available preset IDs and names.
|
||||||
|
* @returns {Array<{id: string, name: string}>}
|
||||||
|
*/
|
||||||
|
export function listPresets() {
|
||||||
|
return [
|
||||||
|
{ id: 'polybrute', name: 'Arturia PolyBrute' },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a preset by ID. Returns the parsed preset object.
|
||||||
|
* @param {string} id — preset identifier
|
||||||
|
* @returns {Promise<{name: string, description: string, channel: number, params: Array}>}
|
||||||
|
*/
|
||||||
|
export async function loadPreset(id) {
|
||||||
|
if (_cache.has(id)) return _cache.get(id);
|
||||||
|
|
||||||
|
const url = PRESET_URLS[id];
|
||||||
|
if (!url) throw new Error(`Unknown MIDI CC preset: ${id}`);
|
||||||
|
|
||||||
|
const resp = await fetch(url);
|
||||||
|
if (!resp.ok) throw new Error(`Failed to load preset ${id}: ${resp.status}`);
|
||||||
|
|
||||||
|
const preset = await resp.json();
|
||||||
|
_cache.set(id, preset);
|
||||||
|
return preset;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a preset from a user-provided JSON file (File object).
|
||||||
|
* @param {File} file
|
||||||
|
* @returns {Promise<{name: string, description: string, channel: number, params: Array}|null>}
|
||||||
|
*/
|
||||||
|
export async function loadPresetFromFile(file) {
|
||||||
|
try {
|
||||||
|
const text = await file.text();
|
||||||
|
const preset = JSON.parse(text);
|
||||||
|
if (!preset.params || !Array.isArray(preset.params) || preset.params.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Validate param shape
|
||||||
|
for (const p of preset.params) {
|
||||||
|
if (typeof p.cc !== 'number' || typeof p.channel !== 'number') return null;
|
||||||
|
}
|
||||||
|
return preset;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
68
playground/js/midi/presets/polybrute.json
Normal file
68
playground/js/midi/presets/polybrute.json
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
{
|
||||||
|
"name": "Arturia PolyBrute",
|
||||||
|
"description": "Full CC map for PolyBrute analog synthesizer (firmware 3.0+)",
|
||||||
|
"channel": 1,
|
||||||
|
"params": [
|
||||||
|
{ "name": "Ladder Cutoff", "cc": 25, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "Steiner Cutoff", "cc": 23, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "Master Cutoff", "cc": 27, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "Ladder Resonance", "cc": 87, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "Steiner Resonance", "cc": 83, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "Ladder Disto", "cc": 85, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "Steiner Brute", "cc": 82, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "Ladder Level", "cc": 8, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "Steiner Level", "cc": 7, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "VCO 1 Tune", "cc": 66, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "VCO 2 Tune", "cc": 72, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "VCO 1 Metalizer", "cc": 70, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "VCO 1 Saw/Tri", "cc": 17, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "VCO 1 Saw/Sq", "cc": 12, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "VCO 1 PW", "cc": 69, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "VCO 2 Sub Mix", "cc": 14, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "VCO 2 Saw/Tri", "cc": 15, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "VCO 2 Saw/Sq", "cc": 16, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "VCO 2 PW", "cc": 75, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "VCO 2 FM 2>1", "cc": 77, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "Mixer VCO 1", "cc": 18, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "Mixer VCO 2", "cc": 19, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "Mixer Noise", "cc": 21, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "Noise Color", "cc": 22, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "VCO2>VCF1", "cc": 79, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "Noise>VCF2", "cc": 80, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "VCF Env Amt Lad", "cc": 26, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "VCF Env Amt Stein", "cc": 24, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "Lad Series/Para", "cc": 86, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "Stein LP>HP>BP", "cc": 81, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "Key Track", "cc": 71, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "VCF Env Attack", "cc": 102, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "VCF Env Decay", "cc": 103, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "VCF Env Sustain", "cc": 28, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "VCF Env Release", "cc": 104, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "VCA Env Attack", "cc": 105, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "VCA Env Decay", "cc": 106, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "VCA Env Sustain", "cc": 29, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "VCA Env Release", "cc": 107, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "Mod Env Delay", "cc": 108, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "Mod Env Attack", "cc": 109, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "Mod Env Decay", "cc": 110, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "Mod Env Sustain", "cc": 30, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "Mod Env Release", "cc": 111, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "LFO 1 Phase", "cc": 90, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "LFO 1 Rate", "cc": 91, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "LFO 2 Fade In", "cc": 92, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "LFO 2 Rate", "cc": 93, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "LFO 3 Curve", "cc": 67, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "LFO 3 Symmetry", "cc": 68, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "LFO 3 Rate", "cc": 73, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "Mod Intensity", "cc": 13, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "Delay Level", "cc": 31, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "Delay Time", "cc": 112, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "Delay Regen", "cc": 113, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "Reverb Level", "cc": 2, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "Reverb Time", "cc": 78, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "Reverb Damping", "cc": 76, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "Stereo", "cc": 10, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "Mod Wheel", "cc": 1, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 },
|
||||||
|
{ "name": "Glide", "cc": 5, "channel": 1, "min": 0, "max": 1, "curve": 0.5, "muted": false, "fixedValue": 0.5 }
|
||||||
|
]
|
||||||
|
}
|
||||||
32
playground/test-midi-receiver.py
Normal file
32
playground/test-midi-receiver.py
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""MIDI CC receiver — listens on all ALSA ports and prints CC messages."""
|
||||||
|
import mido
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ports = mido.get_input_names()
|
||||||
|
print(f"Available MIDI inputs: {ports}")
|
||||||
|
|
||||||
|
if not ports:
|
||||||
|
print("No MIDI input ports found!")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Open the Midi Through port (Chrome sends to this)
|
||||||
|
port_name = ports[0]
|
||||||
|
for p in ports:
|
||||||
|
if 'Through' in p:
|
||||||
|
port_name = p
|
||||||
|
break
|
||||||
|
|
||||||
|
print(f"Listening on: {port_name}")
|
||||||
|
print("Waiting for MIDI CC messages...\n")
|
||||||
|
|
||||||
|
with mido.open_input(port_name) as inport:
|
||||||
|
for msg in inport:
|
||||||
|
if msg.type == 'control_change':
|
||||||
|
print(f" CC #{msg.control:3d} val={msg.value:3d} ch={msg.channel + 1:2d}")
|
||||||
|
else:
|
||||||
|
print(f" {msg}")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
Loading…
Reference in a new issue