Compare commits

..

2 Commits

Author SHA1 Message Date
Jade (Rose) Rowland 93eff7e70f merge main 2025-10-13 01:13:12 -04:00
Jade (Rose) Rowland 8ce1b98e72 Merge branch 'main' into glossing-glossing/distortion-modes 2025-10-13 01:10:02 -04:00
11 changed files with 29 additions and 117 deletions
-17
View File
@@ -2320,23 +2320,6 @@ 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);
-8
View File
@@ -8,14 +8,6 @@ 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:
+7 -40
View File
@@ -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, hasControlName, getControlName, registerControl } from '@strudel/core';
import { noteToMidi, getControlName } from '@strudel/core';
import { Note } from 'webmidi';
// if you use WebMidi from outside of this package, make sure to import that instance:
@@ -100,34 +100,10 @@ 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 };
}
// 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];
return [getControlName(key), mapping];
}),
);
}
@@ -186,14 +162,7 @@ export async function midimaps(map) {
map = await loadCache[map];
}
if (typeof map === 'object') {
Object.entries(map).forEach(([name, mapping]) => {
try {
midicontrolMap.set(name, unifyMapping(mapping));
} catch (err) {
logger(`[midi] Error setting midimap '${name}': ${err.message}`);
throw err;
}
});
Object.entries(map).forEach(([name, mapping]) => midicontrolMap.set(name, unifyMapping(mapping)));
}
}
@@ -355,18 +324,18 @@ Pattern.prototype.midi = function (midiport, options = {}) {
const device = getDevice(midiConfig.midiport, outputs);
const otherOutputs = outputs.filter((o) => o.name !== device.name);
logger(
`[midi] Midi enabled! Using "${device.name}". ${
`Midi enabled! Using "${device.name}". ${
otherOutputs?.length ? `Also available: ${getMidiDeviceNamesString(otherOutputs)}` : ''
}`,
);
},
onDisconnected: ({ outputs }) =>
logger(`[midi] Midi device disconnected! Available: ${getMidiDeviceNamesString(outputs)}`),
logger(`Midi device disconnected! Available: ${getMidiDeviceNamesString(outputs)}`),
});
return this.onTrigger((hap, currentTime, cps, targetTime) => {
if (!WebMidi.enabled) {
logger('[midi] Midi not enabled');
logger('Midi not enabled');
return;
}
hap.ensureObjectValue();
@@ -414,9 +383,7 @@ 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
throw new Error(
`[midimap] midimap "${midimap}" not found! Available maps: ${[...midicontrolMap.keys()].join(', ')}`,
);
logger(`[midi] midimap "${midimap}" not found! Available maps: ${[...midicontrolMap.keys()].join(', ')}`);
}
// Handle note
View File
-1
View File
@@ -153,7 +153,6 @@ 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.
+5 -12
View File
@@ -121,20 +121,13 @@ function githubPath(base, subpath = '') {
if (!base.startsWith('github:')) {
throw new Error('expected "github:" at the start of pseudoUrl');
}
let path = base.slice('github:'.length);
let [_, path] = base.split('github:');
path = path.endsWith('/') ? path.slice(0, -1) : path;
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);
if (path.split('/').length === 2) {
// assume main as default branch if none set
path += '/main';
}
other = other.join('/');
return `https://raw.githubusercontent.com/${user}/${repo}/${branch}/${other}`;
return `https://raw.githubusercontent.com/${path}/${subpath}`;
}
export const processSampleMap = (sampleMap, fn, baseUrl = sampleMap._base || '') => {
+2 -2
View File
@@ -231,12 +231,12 @@ export async function onTriggerSynth(t, value, onended, tables, cps, frameLen) {
begin: t,
end: envEnd,
frequency,
freqspread: value.detune,
detune: value.detune,
position: value.wt,
warp: value.warp,
warpMode: warpmode,
voices: Math.max(value.unison ?? 1, 1),
panspread: value.spread,
spread: value.spread,
phaserand: (value.wtphaserand ?? value.unison > 1) ? 1 : 0,
},
{ outputChannelCount: [2] },
+13 -17
View File
@@ -1050,15 +1050,14 @@ 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: 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: '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: 'warpMode', defaultValue: 0 },
{ name: 'voices', defaultValue: 1, min: 1 },
{ name: 'panspread', defaultValue: 0.7, min: 0, max: 1 },
{ name: 'phaserand', defaultValue: 0, min: 0, max: 1 },
{ 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 },
];
}
@@ -1236,12 +1235,10 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
_sampleFrame(frame, phase) {
const len = frame.length;
const pos = phase * len;
let i = pos | 0;
if (i >= len) i = 0; // fast wrap
const i = pos | 0;
const frac = pos - i;
const a = frame[i];
let i1 = i + 1;
if (i1 >= len) i1 = 0;
const i1 = i + 1 < len ? i + 1 : 0; // fast wrap
const b = frame[i1];
return a + (b - a) * frac;
}
@@ -1271,7 +1268,6 @@ 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;
@@ -1280,9 +1276,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 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);
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);
let f = pv(parameters.frequency, i);
f = applySemitoneDetuneToFrequency(f, detune / 100); // overall detune
const normalizer = 1 / Math.sqrt(voices);
@@ -1295,7 +1291,7 @@ class WavetableOscillatorProcessor extends AudioWorkletProcessor {
gainL = gain2;
gainR = gain1;
}
const fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, freqspread, n)); // voice detune
const fVoice = applySemitoneDetuneToFrequency(f, getUnisonDetune(voices, detune, n)); // voice detune
const dPhase = fVoice * this.invSR;
const level = this._chooseMip(dPhase);
const table = this.tables[level];
-17
View File
@@ -60,23 +60,6 @@ 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.
+1 -1
View File
@@ -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
@@ -117,7 +117,7 @@ export function SettingsTab({ started }) {
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
return (
<div className="text-foreground p-4 space-y-4 w-full" style={{ fontFamily }}>
{canChangeAudioDevice && (
<FormItem label="Audio Output Device">
<AudioDeviceSelector
isDisabled={started}
@@ -132,7 +132,6 @@ export function SettingsTab({ started }) {
}}
/>
</FormItem>
)}
<FormItem label="Audio Engine Target">
<AudioEngineTargetSelector
target={audioEngineTarget}