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">
|
||||
<option value="">No device</option>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
|
|
@ -269,6 +272,13 @@
|
|||
<button class="drawer-close" data-drawer="midi-cc">×</button>
|
||||
</div>
|
||||
<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">
|
||||
<label>Parameters</label>
|
||||
<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>
|
||||
</div>
|
||||
<div class="synth-row">
|
||||
<button class="action-btn" id="midi-cc-import">Import</button>
|
||||
<button class="action-btn" id="midi-cc-export">Export</button>
|
||||
<button class="action-btn" id="midi-cc-import">Paste JSON</button>
|
||||
<button class="action-btn" id="midi-cc-export">Copy JSON</button>
|
||||
</div>
|
||||
<div id="midi-cc-param-list"></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 { MIDIOutput } from './midi/midi-output.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 { HandTracker } from './ui/hand-tracker.js';
|
||||
import { createDevPanel } from './ui/dev-panel.js';
|
||||
|
|
@ -719,6 +720,10 @@ async function resizeMLP(newOutputCount) {
|
|||
if (newOutputCount === N_OUTPUTS) return;
|
||||
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.setLogger(msg => console.log('[NISPS:joy]', msg));
|
||||
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);
|
||||
|
||||
// Randomize with current spread
|
||||
imlJoy.drawWeights(spreadLevel);
|
||||
imlHand.drawWeights(spreadLevel);
|
||||
imlJoy.randomiseWeights(spreadLevel);
|
||||
imlHand.randomiseWeights(spreadLevel);
|
||||
|
||||
// Re-run inference
|
||||
iml.setInput(0, joyX);
|
||||
|
|
@ -1922,7 +1927,7 @@ function onThumbsUp() {
|
|||
|
||||
pushUndoSnapshot();
|
||||
const inputs = getCurrentInputs();
|
||||
const outputs = [...iml.getOutputs()];
|
||||
const outputs = [...rawParamValues];
|
||||
iml.addExample(inputs, outputs);
|
||||
|
||||
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 ----
|
||||
async function initMIDICCControls() {
|
||||
const enableBtn = document.getElementById('midi-cc-enable-btn');
|
||||
|
|
@ -2279,6 +2315,8 @@ async function initMIDICCControls() {
|
|||
const removeBtn = document.getElementById('midi-cc-remove');
|
||||
const importBtn = document.getElementById('midi-cc-import');
|
||||
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;
|
||||
|
||||
|
|
@ -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
|
||||
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