mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-22 13:13:10 -04:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 947dd76823 | |||
| 6e58c973af | |||
| 2473a5391d | |||
| 4e17cfbdd6 | |||
| 83c7e63432 | |||
| 909e0154fe | |||
| ef4e21ac40 | |||
| 3c1a8c8bdb | |||
| b7e941c649 | |||
| 9c5c71c31a | |||
| af53bab259 | |||
| 0065db8569 | |||
| 6f703e48e7 | |||
| 1ae773acc4 | |||
| 120f89d57f | |||
| 99e14dae5c | |||
| ac582b4d40 | |||
| 28eedae730 | |||
| e075095e8b | |||
| 84e3f12171 | |||
| 260c6e0783 | |||
| bc3481822c |
@@ -2320,6 +2320,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);
|
||||
|
||||
@@ -84,18 +84,9 @@ Pattern.prototype.onPaint = function (painter) {
|
||||
state.controls.painters = [];
|
||||
}
|
||||
state.controls.painters.push(painter);
|
||||
return state;
|
||||
});
|
||||
};
|
||||
|
||||
// TODO - Why isn't this pure deep copy not working?
|
||||
// Pattern.prototype.onPaint = function (painter) {
|
||||
// return this.withState((state) => {
|
||||
// const painters = state.controls.painters ? [...state.controls.painters, painter] : [painter];
|
||||
// return new State(state.span, { ...state.controls, painters });
|
||||
// });
|
||||
// };
|
||||
|
||||
Pattern.prototype.getPainters = function () {
|
||||
let painters = [];
|
||||
this.queryArc(0, 0, { painters });
|
||||
|
||||
@@ -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:
|
||||
|
||||
+40
-7
@@ -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:
|
||||
@@ -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)) {
|
||||
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];
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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((hap, currentTime, cps, targetTime) => {
|
||||
if (!WebMidi.enabled) {
|
||||
logger('Midi not enabled');
|
||||
logger('[midi] Midi not enabled');
|
||||
return;
|
||||
}
|
||||
hap.ensureObjectValue();
|
||||
@@ -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
|
||||
|
||||
@@ -153,6 +153,7 @@ samples('github:tidalcycles/dirt-samples')
|
||||
|
||||
The format is `github:<user>/<repo>/<branch>`.
|
||||
|
||||
If `<repo>` and `<branch>` are not specified, they will default to `samples` and `main` respectively.
|
||||
It expects a `strudel.json` file to be present at the root of the given repository, which declares the sample paths in the repo.
|
||||
|
||||
The format is also expected to be the same as explained above.
|
||||
|
||||
@@ -121,13 +121,20 @@ function githubPath(base, subpath = '') {
|
||||
if (!base.startsWith('github:')) {
|
||||
throw new Error('expected "github:" at the start of pseudoUrl');
|
||||
}
|
||||
let [_, path] = base.split('github:');
|
||||
let path = base.slice('github:'.length);
|
||||
path = path.endsWith('/') ? path.slice(0, -1) : path;
|
||||
if (path.split('/').length === 2) {
|
||||
// assume main as default branch if none set
|
||||
path += '/main';
|
||||
|
||||
let components = path.split('/');
|
||||
let user = components[0];
|
||||
let repo = components.length >= 2 ? components[1] : 'samples';
|
||||
let branch = components.length >= 3 ? components[2] : 'main';
|
||||
let other = components.slice(3);
|
||||
if (subpath) {
|
||||
other.push(subpath);
|
||||
}
|
||||
return `https://raw.githubusercontent.com/${path}/${subpath}`;
|
||||
other = other.join('/');
|
||||
|
||||
return `https://raw.githubusercontent.com/${user}/${repo}/${branch}/${other}`;
|
||||
}
|
||||
|
||||
export const processSampleMap = (sampleMap, fn, baseUrl = sampleMap._base || '') => {
|
||||
|
||||
@@ -231,12 +231,12 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
|
||||
begin: t,
|
||||
end: envEnd,
|
||||
frequency,
|
||||
detune: value.detune,
|
||||
freqspread: value.detune,
|
||||
position: value.wt,
|
||||
warp: value.warp,
|
||||
warpMode: warpmode,
|
||||
voices: Math.max(value.unison ?? 1, 1),
|
||||
spread: value.spread,
|
||||
panspread: value.spread,
|
||||
phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0,
|
||||
},
|
||||
{ outputChannelCount: [2] },
|
||||
|
||||
@@ -1050,14 +1050,15 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
||||
return [
|
||||
{ name: 'begin', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY },
|
||||
{ name: 'end', defaultValue: 0, min: 0, max: Number.POSITIVE_INFINITY },
|
||||
{ name: 'frequency', defaultValue: 220, minValue: 0.01, maxValue: 20000 },
|
||||
{ name: 'detune', defaultValue: 0.18 },
|
||||
{ name: 'position', defaultValue: 0, minValue: 0, maxValue: 1 },
|
||||
{ name: 'warp', defaultValue: 0, minValue: 0, maxValue: 1 },
|
||||
{ name: 'frequency', defaultValue: 440, min: Number.EPSILON },
|
||||
{ name: 'detune', defaultValue: 0 },
|
||||
{ name: 'freqspread', defaultValue: 0.18, min: 0 },
|
||||
{ name: 'position', defaultValue: 0, min: 0, max: 1 },
|
||||
{ name: 'warp', defaultValue: 0, min: 0, max: 1 },
|
||||
{ name: 'warpMode', defaultValue: 0 },
|
||||
{ name: 'voices', defaultValue: 1, minValue: 1, maxValue: 32 },
|
||||
{ name: 'spread', defaultValue: 0.7, minValue: 0, maxValue: 1 },
|
||||
{ name: 'phaserand', defaultValue: 0, minValue: 0, maxValue: 1 },
|
||||
{ name: 'voices', defaultValue: 1, min: 1 },
|
||||
{ name: 'panspread', defaultValue: 0.7, min: 0, max: 1 },
|
||||
{ name: 'phaserand', defaultValue: 0, min: 0, max: 1 },
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1235,10 +1236,12 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
||||
_sampleFrame(frame, phase) {
|
||||
const len = frame.length;
|
||||
const pos = phase * len;
|
||||
const i = pos | 0;
|
||||
let i = pos | 0;
|
||||
if (i >= len) i = 0; // fast wrap
|
||||
const frac = pos - i;
|
||||
const a = frame[i];
|
||||
const i1 = i + 1 < len ? i + 1 : 0; // fast wrap
|
||||
let i1 = i + 1;
|
||||
if (i1 >= len) i1 = 0;
|
||||
const b = frame[i1];
|
||||
return a + (b - a) * frac;
|
||||
}
|
||||
@@ -1268,6 +1271,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
||||
}
|
||||
for (let i = 0; i < outL.length; i++) {
|
||||
const detune = pv(parameters.detune, i);
|
||||
const freqspread = pv(parameters.freqspread, i);
|
||||
const tablePos = clamp(pv(parameters.position, i), 0, 1);
|
||||
const idx = tablePos * (this.numFrames - 1);
|
||||
const fIdx = idx | 0;
|
||||
@@ -1276,9 +1280,9 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
||||
const warpMode = pv(parameters.warpMode, i);
|
||||
const voices = pv(parameters.voices, i);
|
||||
const phaseRand = clamp(pv(parameters.phaserand, i), 0, 1);
|
||||
const spread = voices > 1 ? clamp(pv(parameters.spread, i), 0, 1) : 0;
|
||||
const gain1 = Math.sqrt(0.5 - 0.5 * spread);
|
||||
const gain2 = Math.sqrt(0.5 + 0.5 * spread);
|
||||
const panspread = voices > 1 ? clamp(pv(parameters.panspread, i), 0, 1) : 0;
|
||||
const gain1 = Math.sqrt(0.5 - 0.5 * panspread);
|
||||
const gain2 = Math.sqrt(0.5 + 0.5 * panspread);
|
||||
let f = pv(parameters.frequency, i);
|
||||
f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune
|
||||
const normalizer = 1 / Math.sqrt(voices);
|
||||
@@ -1291,7 +1295,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
|
||||
gainL = gain2;
|
||||
gainR = gain1;
|
||||
}
|
||||
const fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, detune, n)); // voice detune
|
||||
const fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, freqspread, n)); // voice detune
|
||||
const dPhase = fVoice * this.invSR;
|
||||
const level = this._chooseMip(dPhase);
|
||||
const table = this.tables[level];
|
||||
|
||||
@@ -60,6 +60,23 @@ Strudel makes heavy use of chained functions. Here is a more sophisticated examp
|
||||
.room(0.5)`}
|
||||
/>
|
||||
|
||||
## Write your own chained function
|
||||
|
||||
You can write your own chained function using `register`. Here's the above chain but registered as a reusable, chained function.
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`const effectChain = register('effectChain', (pat) => pat
|
||||
.s("sawtooth")
|
||||
.cutoff(500)
|
||||
//.delay(0.5)
|
||||
.room(0.5)
|
||||
)
|
||||
note("a3 c#4 e4 a4").effectChain()`}
|
||||
/>
|
||||
|
||||
Try adding `.rev()` after `effectChain()` to hear further effects added.
|
||||
|
||||
# Comments
|
||||
|
||||
The `//` in the example above is a line comment, resulting in the `delay` function being ignored.
|
||||
|
||||
@@ -381,7 +381,7 @@ Sampler effects are functions that can be used to change the behaviour of sample
|
||||
|
||||
### scrub
|
||||
|
||||
<JsDoc client:idle name="Pattern.scrub" h={0} />{' '}
|
||||
<JsDoc client:idle name="Pattern.scrub" h={0} />
|
||||
|
||||
### speed
|
||||
|
||||
|
||||
Reference in New Issue
Block a user