mirror of
https://github.com/eliasstepanik/strudel.git
synced 2026-01-11 13:48:40 +00:00
commit
5e81b542dc
@ -1,26 +0,0 @@
|
|||||||
krill-parser.js
|
|
||||||
krill.pegjs
|
|
||||||
.eslintrc.json
|
|
||||||
server.js
|
|
||||||
tidal-sniffer.js
|
|
||||||
*.jsx
|
|
||||||
tunejs.js
|
|
||||||
out/**
|
|
||||||
postcss.config.js
|
|
||||||
postcss.config.cjs
|
|
||||||
tailwind.config.js
|
|
||||||
tailwind.config.cjs
|
|
||||||
vite.config.js
|
|
||||||
/**/dist/**/*
|
|
||||||
!**/*.mjs
|
|
||||||
**/*.tsx
|
|
||||||
**/*.ts
|
|
||||||
**/*.json
|
|
||||||
**/dev-dist
|
|
||||||
**/dist
|
|
||||||
/src-tauri/target/**/*
|
|
||||||
reverbGen.mjs
|
|
||||||
hydra.mjs
|
|
||||||
jsdoc-synonyms.js
|
|
||||||
packages/hs2js/src/hs2js.mjs
|
|
||||||
samples
|
|
||||||
@ -1514,6 +1514,8 @@ export const { binshift } = registerControl('binshift');
|
|||||||
export const { hbrick } = registerControl('hbrick');
|
export const { hbrick } = registerControl('hbrick');
|
||||||
export const { lbrick } = registerControl('lbrick');
|
export const { lbrick } = registerControl('lbrick');
|
||||||
export const { midichan } = registerControl('midichan');
|
export const { midichan } = registerControl('midichan');
|
||||||
|
export const { midimap } = registerControl('midimap');
|
||||||
|
export const { midiport } = registerControl('midiport');
|
||||||
export const { control } = registerControl('control');
|
export const { control } = registerControl('control');
|
||||||
export const { ccn } = registerControl('ccn');
|
export const { ccn } = registerControl('ccn');
|
||||||
export const { ccv } = registerControl('ccv');
|
export const { ccv } = registerControl('ccv');
|
||||||
|
|||||||
@ -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 * as _WebMidi from 'webmidi';
|
||||||
import { Pattern, getEventOffsetMs, isPattern, logger, ref } from '@strudel/core';
|
import { Pattern, getEventOffsetMs, isPattern, logger, ref } from '@strudel/core';
|
||||||
import { noteToMidi } from '@strudel/core';
|
import { noteToMidi, getControlName } from '@strudel/core';
|
||||||
import { Note } from 'webmidi';
|
import { Note } from 'webmidi';
|
||||||
// if you use WebMidi from outside of this package, make sure to import that instance:
|
// if you use WebMidi from outside of this package, make sure to import that instance:
|
||||||
export const { WebMidi } = _WebMidi;
|
export const { WebMidi } = _WebMidi;
|
||||||
@ -89,6 +89,113 @@ if (typeof window !== 'undefined') {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// registry for midi mappings, converting control names to cc messages
|
||||||
|
export const midicontrolMap = new Map();
|
||||||
|
|
||||||
|
// takes midimap and converts each control key to the main control name
|
||||||
|
function unifyMapping(mapping) {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(mapping).map(([key, mapping]) => {
|
||||||
|
if (typeof mapping === 'number') {
|
||||||
|
mapping = { ccn: mapping };
|
||||||
|
}
|
||||||
|
return [getControlName(key), mapping];
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function githubPath(base, subpath = '') {
|
||||||
|
if (!base.startsWith('github:')) {
|
||||||
|
throw new Error('expected "github:" at the start of pseudoUrl');
|
||||||
|
}
|
||||||
|
let [_, path] = base.split('github:');
|
||||||
|
path = path.endsWith('/') ? path.slice(0, -1) : path;
|
||||||
|
if (path.split('/').length === 2) {
|
||||||
|
// assume main as default branch if none set
|
||||||
|
path += '/main';
|
||||||
|
}
|
||||||
|
return `https://raw.githubusercontent.com/${path}/${subpath}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* configures the default midimap, which is used when no "midimap" port is set
|
||||||
|
* @example
|
||||||
|
* defaultmidimap({ lpf: 74 })
|
||||||
|
* $: note("c a f e").midi();
|
||||||
|
* $: lpf(sine.slow(4).segment(16)).midi();
|
||||||
|
*/
|
||||||
|
export function defaultmidimap(mapping) {
|
||||||
|
midicontrolMap.set('default', unifyMapping(mapping));
|
||||||
|
}
|
||||||
|
|
||||||
|
let loadCache = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds midimaps to the registry. Inside each midimap, control names (e.g. lpf) are mapped to cc numbers.
|
||||||
|
* @example
|
||||||
|
* midimaps({ mymap: { lpf: 74 } })
|
||||||
|
* $: note("c a f e")
|
||||||
|
* .lpf(sine.slow(4))
|
||||||
|
* .midimap('mymap')
|
||||||
|
* .midi()
|
||||||
|
* @example
|
||||||
|
* midimaps({ mymap: {
|
||||||
|
* lpf: { ccn: 74, min: 0, max: 20000, exp: 0.5 }
|
||||||
|
* }})
|
||||||
|
* $: note("c a f e")
|
||||||
|
* .lpf(sine.slow(2).range(400,2000))
|
||||||
|
* .midimap('mymap')
|
||||||
|
* .midi()
|
||||||
|
*/
|
||||||
|
export async function midimaps(map) {
|
||||||
|
if (typeof map === 'string') {
|
||||||
|
if (map.startsWith('github:')) {
|
||||||
|
map = githubPath(map, 'midimap.json');
|
||||||
|
}
|
||||||
|
if (!loadCache[map]) {
|
||||||
|
loadCache[map] = fetch(map).then((res) => res.json());
|
||||||
|
}
|
||||||
|
map = await loadCache[map];
|
||||||
|
}
|
||||||
|
if (typeof map === 'object') {
|
||||||
|
Object.entries(map).forEach(([name, mapping]) => midicontrolMap.set(name, unifyMapping(mapping)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// registry for midi sounds, converting sound names to controls
|
||||||
|
export const midisoundMap = new Map();
|
||||||
|
|
||||||
|
// normalizes the given value from the given range and exponent
|
||||||
|
function normalize(value = 0, min = 0, max = 1, exp = 1) {
|
||||||
|
if (min === max) {
|
||||||
|
throw new Error('min and max cannot be the same value');
|
||||||
|
}
|
||||||
|
let normalized = (value - min) / (max - min);
|
||||||
|
normalized = Math.min(1, Math.max(0, normalized));
|
||||||
|
return Math.pow(normalized, exp);
|
||||||
|
}
|
||||||
|
function mapCC(mapping, value) {
|
||||||
|
return Object.keys(value)
|
||||||
|
.filter((key) => !!mapping[getControlName(key)])
|
||||||
|
.map((key) => {
|
||||||
|
const { ccn, min = 0, max = 1, exp = 1 } = mapping[key];
|
||||||
|
const ccv = normalize(value[key], min, max, exp);
|
||||||
|
return { ccn, ccv };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// sends a cc message to the given device on the given channel
|
||||||
|
function sendCC(ccn, ccv, device, midichan, timeOffsetString) {
|
||||||
|
if (typeof ccv !== 'number' || ccv < 0 || ccv > 1) {
|
||||||
|
throw new Error('expected ccv to be a number between 0 and 1');
|
||||||
|
}
|
||||||
|
if (!['string', 'number'].includes(typeof ccn)) {
|
||||||
|
throw new Error('expected ccn to be a number or a string');
|
||||||
|
}
|
||||||
|
const scaled = Math.round(ccv * 127);
|
||||||
|
device.sendControlChange(ccn, scaled, midichan, { time: timeOffsetString });
|
||||||
|
}
|
||||||
|
|
||||||
Pattern.prototype.midi = function (output) {
|
Pattern.prototype.midi = function (output) {
|
||||||
if (isPattern(output)) {
|
if (isPattern(output)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@ -117,16 +224,39 @@ Pattern.prototype.midi = function (output) {
|
|||||||
console.log('not enabled');
|
console.log('not enabled');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const device = getDevice(output, WebMidi.outputs);
|
|
||||||
hap.ensureObjectValue();
|
hap.ensureObjectValue();
|
||||||
//magic number to get audio engine to line up, can probably be calculated somehow
|
//magic number to get audio engine to line up, can probably be calculated somehow
|
||||||
const latencyMs = 34;
|
const latencyMs = 34;
|
||||||
// passing a string with a +num into the webmidi api adds an offset to the current time https://webmidijs.org/api/classes/Output
|
// passing a string with a +num into the webmidi api adds an offset to the current time https://webmidijs.org/api/classes/Output
|
||||||
const timeOffsetString = `+${getEventOffsetMs(targetTime, currentTime) + latencyMs}`;
|
const timeOffsetString = `+${getEventOffsetMs(targetTime, currentTime) + latencyMs}`;
|
||||||
|
|
||||||
// destructure value
|
// destructure value
|
||||||
let { note, nrpnn, nrpv, ccn, ccv, midichan = 1, midicmd, gain = 1, velocity = 0.9 } = hap.value;
|
let {
|
||||||
|
note,
|
||||||
|
ccn,
|
||||||
|
ccv,
|
||||||
|
midichan = 1,
|
||||||
|
midicmd,
|
||||||
|
gain = 1,
|
||||||
|
velocity = 0.9,
|
||||||
|
midimap = 'default',
|
||||||
|
midiport = output,
|
||||||
|
} = hap.value;
|
||||||
|
|
||||||
|
const device = getDevice(midiport, WebMidi.outputs);
|
||||||
|
if (!device) {
|
||||||
|
logger(
|
||||||
|
`[midi] midiport "${midiport}" not found! available: ${WebMidi.outputs.map((output) => `'${output.name}'`).join(', ')}`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
velocity = gain * velocity;
|
velocity = gain * velocity;
|
||||||
|
// if midimap is set, send a cc messages from defined controls
|
||||||
|
if (midicontrolMap.has(midimap)) {
|
||||||
|
const ccs = mapCC(midicontrolMap.get(midimap), hap.value);
|
||||||
|
ccs.forEach(({ ccn, ccv }) => sendCC(ccn, ccv, device, midichan, timeOffsetString));
|
||||||
|
}
|
||||||
|
|
||||||
// note off messages will often a few ms arrive late, try to prevent glitching by subtracting from the duration length
|
// note off messages will often a few ms arrive late, try to prevent glitching by subtracting from the duration length
|
||||||
const duration = (hap.duration.valueOf() / cps) * 1000 - 10;
|
const duration = (hap.duration.valueOf() / cps) * 1000 - 10;
|
||||||
@ -138,14 +268,7 @@ Pattern.prototype.midi = function (output) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (ccv !== undefined && ccn !== undefined) {
|
if (ccv !== undefined && ccn !== undefined) {
|
||||||
if (typeof ccv !== 'number' || ccv < 0 || ccv > 1) {
|
sendCC(ccn, ccv, device, midichan, timeOffsetString);
|
||||||
throw new Error('expected ccv to be a number between 0 and 1');
|
|
||||||
}
|
|
||||||
if (!['string', 'number'].includes(typeof ccn)) {
|
|
||||||
throw new Error('expected ccn to be a number or a string');
|
|
||||||
}
|
|
||||||
const scaled = Math.round(ccv * 127);
|
|
||||||
device.sendControlChange(ccn, scaled, midichan, { time: timeOffsetString });
|
|
||||||
}
|
}
|
||||||
if (hap.whole.begin + 0 === 0) {
|
if (hap.whole.begin + 0 === 0) {
|
||||||
// we need to start here because we have the timing info
|
// we need to start here because we have the timing info
|
||||||
|
|||||||
@ -18,6 +18,8 @@ const skippedExamples = [
|
|||||||
'accelerationZ',
|
'accelerationZ',
|
||||||
'accelerationY',
|
'accelerationY',
|
||||||
'accelerationX',
|
'accelerationX',
|
||||||
|
'defaultmidimap',
|
||||||
|
'midimaps',
|
||||||
];
|
];
|
||||||
|
|
||||||
describe('runs examples', () => {
|
describe('runs examples', () => {
|
||||||
|
|||||||
@ -46,6 +46,16 @@ But you can also control cc messages separately like this:
|
|||||||
$: ccv(sine.segment(16).slow(4)).ccn(74).midi()`}
|
$: ccv(sine.segment(16).slow(4)).ccn(74).midi()`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
Instead of setting `ccn` and `ccv` directly, you can also create mappings with `midimaps`:
|
||||||
|
|
||||||
|
## midimaps
|
||||||
|
|
||||||
|
<JsDoc client:idle name="midimaps" h={0} />
|
||||||
|
|
||||||
|
## defaultmidimap
|
||||||
|
|
||||||
|
<JsDoc client:idle name="defaultmidimap" h={0} />
|
||||||
|
|
||||||
# OSC/SuperDirt/StrudelDirt
|
# OSC/SuperDirt/StrudelDirt
|
||||||
|
|
||||||
In TidalCycles, sound is usually generated using [SuperDirt](https://github.com/musikinformatik/SuperDirt/), which runs inside SuperCollider. Strudel also supports using SuperDirt, although it requires installing some additional software.
|
In TidalCycles, sound is usually generated using [SuperDirt](https://github.com/musikinformatik/SuperDirt/), which runs inside SuperCollider. Strudel also supports using SuperDirt, although it requires installing some additional software.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user