Skip to content
This repository was archived by the owner on Jun 19, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions packages/core/controls.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1755,6 +1755,23 @@ export const { miditouch } = registerControl('miditouch');
// TODO: what is this?
export const { polyTouch } = registerControl('polyTouch');

/**
* Checks if a control name exists in the controlAlias map.
* @name hasControlName
* @param {string} alias The control name to check
* @returns {boolean} True if the control name exists, false otherwise
*/
export const hasControlName = (alias) => {
// Check if the name exists as a key (alias) or value (main control name)
return controlAlias.has(alias) || Array.from(controlAlias.values()).includes(alias);
};

/**
* Gets the control name from the controlAlias map.
* @name getControlName
* @param {string} alias The control name to get
* @returns {string} The control name
*/
export const getControlName = (alias) => {
if (controlAlias.has(alias)) {
return controlAlias.get(alias);
Expand Down
8 changes: 8 additions & 0 deletions packages/midi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ This package adds midi functionality to strudel Patterns.
npm i @strudel/midi --save
```

## Enabling MIDI for Local Development in Chrome

1. Open Chrome and navigate to `chrome://flags`
2. Search for "Insecure origins treated as secure"
3. In the text field that appears, add your development origin (e.g., http://localhost:3000)
4. Enable the flag
5. Restart Chrome

## Available Controls

The following MIDI controls are available:
Expand Down
47 changes: 40 additions & 7 deletions packages/midi/midi.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th

import * as _WebMidi from 'webmidi';
import { Pattern, getEventOffsetMs, isPattern, logger, ref } from '@strudel/core';
import { noteToMidi, getControlName } from '@strudel/core';
import { noteToMidi, hasControlName, getControlName, registerControl } from '@strudel/core';
import { Note } from 'webmidi';

// if you use WebMidi from outside of this package, make sure to import that instance:
Expand Down Expand Up @@ -100,10 +100,34 @@ export const midicontrolMap = new Map();
function unifyMapping(mapping) {
return Object.fromEntries(
Object.entries(mapping).map(([key, mapping]) => {
// Convert number to object with ccn property
if (typeof mapping === 'number') {
mapping = { ccn: mapping };
}
return [getControlName(key), mapping];

// Get the non-aliased control name from the key
const controlName = getControlName(key);

// Check if the key or controlName already exists in the controlAlias map
if (hasControlName(key) || hasControlName(controlName)) {
// Show warning and carry on.
logger(`[midimap] '[${key}, ${controlName}]' overwrites a Strudel API.`);

// Throw error to stop the music
//throw new Error(`[midimap] '${key}' overwrites a Strudel API.`);
}

// Register the control in the midicontrolMap if it doesn't exist
if (!midicontrolMap.has(controlName)) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if the control name exists already? then we shouldn't register it i think. when doing so, we might override it in a way that breaks it. for example, some controls are allowed to set multiple controls, like cutoff:resonance:lpenv, which would probably break when calling registerControl('cutoff'), as things like note("g1").cutoff("200:8:4") wouldn't set resonance and lpenv anymore

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's what I meant by namespace pollution.
I think an easier solution would be to add a prefix like mm_ to distinguish the names.
Alternatively, it could just throw an error if controlAlias.has(controlName) returns true, and prompt the user to choose a different control name.

Copy link
Collaborator

@felixroos felixroos Mar 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not reuse existing names? one idea behind these midimaps was that you could easily play a non-midi pattern with your midi device without changing control names (e.g. lpf maps to your synths cutoff cc)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, in that case how about registering a selective control function that checks whether hap.value.midi exists, and then decides whether to use the original lpf or the midi-mapped lpf? Or is there a better way to tell if the current trigger is to MIDI, SuperDough, or else?

try {
registerControl(controlName);
} catch (err) {
throw new Error(`[midimap] Failed to register midimap control '${controlName}': ${err.message}`);
}
} else {
logger(`[midimap] '${controlName}' already registered as a midimap control. Skipping registration.`);
}
return [controlName, mapping];
}),
);
}
Expand Down Expand Up @@ -162,7 +186,14 @@ export async function midimaps(map) {
map = await loadCache[map];
}
if (typeof map === 'object') {
Object.entries(map).forEach(([name, mapping]) => midicontrolMap.set(name, unifyMapping(mapping)));
Object.entries(map).forEach(([name, mapping]) => {
try {
midicontrolMap.set(name, unifyMapping(mapping));
} catch (err) {
logger(`[midi] Error setting midimap '${name}': ${err.message}`);
throw err;
}
});
}
}

Expand Down Expand Up @@ -324,18 +355,18 @@ Pattern.prototype.midi = function (midiport, options = {}) {
const device = getDevice(midiConfig.midiport, outputs);
const otherOutputs = outputs.filter((o) => o.name !== device.name);
logger(
`Midi enabled! Using "${device.name}". ${
`[midi] Midi enabled! Using "${device.name}". ${
otherOutputs?.length ? `Also available: ${getMidiDeviceNamesString(otherOutputs)}` : ''
}`,
);
},
onDisconnected: ({ outputs }) =>
logger(`Midi device disconnected! Available: ${getMidiDeviceNamesString(outputs)}`),
logger(`[midi] Midi device disconnected! Available: ${getMidiDeviceNamesString(outputs)}`),
});

return this.onTrigger((time_deprecate, hap, currentTime, cps, targetTime) => {
if (!WebMidi.enabled) {
logger('Midi not enabled');
logger('[midi] Midi not enabled');
return;
}
hap.ensureObjectValue();
Expand Down Expand Up @@ -383,7 +414,9 @@ Pattern.prototype.midi = function (midiport, options = {}) {
ccs.forEach(({ ccn, ccv }) => sendCC(ccn, ccv, device, midichan, timeOffsetString));
} else if (midimap !== 'default') {
// Add warning when a non-existent midimap is specified
logger(`[midi] midimap "${midimap}" not found! Available maps: ${[...midicontrolMap.keys()].join(', ')}`);
throw new Error(
`[midimap] midimap "${midimap}" not found! Available maps: ${[...midicontrolMap.keys()].join(', ')}`,
);
}

// Handle note
Expand Down