mirror of
https://codeberg.org/uzu/strudel
synced 2026-10-07 09:28:07 -04:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 35bfd8b3ff | |||
| c2720a5738 | |||
| 75db9ff16e | |||
| 31cacc3c29 |
@@ -3076,15 +3076,3 @@ export const bmod = (config) => pure({}).bmod(config);
|
|||||||
export const { transient } = registerControl(['transient', 'transsustain']);
|
export const { transient } = registerControl(['transient', 'transsustain']);
|
||||||
|
|
||||||
export const { FXrelease, FXrel, FXr, fxr } = registerControl('FXrelease', 'FXrel', 'FXr', 'fxr');
|
export const { FXrelease, FXrel, FXr, fxr } = registerControl('FXrelease', 'FXrel', 'FXr', 'fxr');
|
||||||
|
|
||||||
/**
|
|
||||||
* Input audio device to be used as a sound source in a pattern
|
|
||||||
*
|
|
||||||
* @name input
|
|
||||||
* @param {string | number | Pattern} input Name or index of the input audio device to use
|
|
||||||
* @example
|
|
||||||
* // Please be careful about feedback!
|
|
||||||
* input("1 2").gain(0.5)
|
|
||||||
* .stretch(0.5).lfo({ s: 0.5 })
|
|
||||||
*/
|
|
||||||
export const { input } = registerControl('input');
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
} from './util.mjs';
|
} from './util.mjs';
|
||||||
import drawLine from './drawLine.mjs';
|
import drawLine from './drawLine.mjs';
|
||||||
import { errorLogger, logger } from './logger.mjs';
|
import { errorLogger, logger } from './logger.mjs';
|
||||||
|
import { strudelScope } from './evaluate.mjs';
|
||||||
|
|
||||||
let stringParser;
|
let stringParser;
|
||||||
|
|
||||||
@@ -1683,7 +1684,9 @@ export function register(name, func, patternify = true, preserveSteps = false, j
|
|||||||
|
|
||||||
// toplevel functions get curried as well as patternified
|
// toplevel functions get curried as well as patternified
|
||||||
// because pfunc uses spread args, we need to state the arity explicitly!
|
// because pfunc uses spread args, we need to state the arity explicitly!
|
||||||
return curry(pfunc, null, arity);
|
const curried = curry(pfunc, null, arity);
|
||||||
|
strudelScope[name] = curried;
|
||||||
|
return curried;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Like register, but defaults to stepJoin
|
// Like register, but defaults to stepJoin
|
||||||
|
|||||||
@@ -14,11 +14,9 @@ import {
|
|||||||
createFilter,
|
createFilter,
|
||||||
effectSend,
|
effectSend,
|
||||||
gainNode,
|
gainNode,
|
||||||
getADSRValues,
|
|
||||||
getCompressor,
|
getCompressor,
|
||||||
getDistortion,
|
getDistortion,
|
||||||
getLfo,
|
getLfo,
|
||||||
getParamADSR,
|
|
||||||
getWorklet,
|
getWorklet,
|
||||||
releaseAudioNode,
|
releaseAudioNode,
|
||||||
webAudioTimeout,
|
webAudioTimeout,
|
||||||
@@ -178,19 +176,6 @@ export const getAudioDevices = async () => {
|
|||||||
return devicesMap;
|
return devicesMap;
|
||||||
};
|
};
|
||||||
|
|
||||||
let AUDIO_INPUTS;
|
|
||||||
export const getAudioInputs = async () => {
|
|
||||||
await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
||||||
let mediaDevices = await navigator.mediaDevices.enumerateDevices();
|
|
||||||
mediaDevices = mediaDevices.filter((device) => device.kind === 'audioinput' && device.deviceId !== 'default');
|
|
||||||
const devicesMap = new Map();
|
|
||||||
devicesMap.set(DEFAULT_AUDIO_DEVICE_NAME, '');
|
|
||||||
mediaDevices.forEach((device) => {
|
|
||||||
devicesMap.set(device.label, device.deviceId);
|
|
||||||
});
|
|
||||||
return devicesMap;
|
|
||||||
};
|
|
||||||
|
|
||||||
let defaultDefaultValues = {
|
let defaultDefaultValues = {
|
||||||
s: 'triangle',
|
s: 'triangle',
|
||||||
gain: 0.8,
|
gain: 0.8,
|
||||||
@@ -269,7 +254,6 @@ export function loadWorklets() {
|
|||||||
return workletsLoading;
|
return workletsLoading;
|
||||||
}
|
}
|
||||||
|
|
||||||
let AUDIO_OUTPUTS;
|
|
||||||
// this function should be called on first user interaction (to avoid console warning)
|
// this function should be called on first user interaction (to avoid console warning)
|
||||||
export async function initAudio(options = {}) {
|
export async function initAudio(options = {}) {
|
||||||
const {
|
const {
|
||||||
@@ -290,8 +274,8 @@ export async function initAudio(options = {}) {
|
|||||||
|
|
||||||
if (audioDeviceName != null && audioDeviceName != DEFAULT_AUDIO_DEVICE_NAME) {
|
if (audioDeviceName != null && audioDeviceName != DEFAULT_AUDIO_DEVICE_NAME) {
|
||||||
try {
|
try {
|
||||||
AUDIO_OUTPUTS = await getAudioDevices();
|
const devices = await getAudioDevices();
|
||||||
const id = AUDIO_OUTPUTS.get(audioDeviceName);
|
const id = devices.get(audioDeviceName);
|
||||||
const isValidID = (id ?? '').length > 0;
|
const isValidID = (id ?? '').length > 0;
|
||||||
if (audioCtx.sinkId !== id && isValidID) {
|
if (audioCtx.sinkId !== id && isValidID) {
|
||||||
await audioCtx.setSinkId(id);
|
await audioCtx.setSinkId(id);
|
||||||
@@ -332,51 +316,6 @@ export async function initAudioOnFirstClick(options) {
|
|||||||
return audioReady;
|
return audioReady;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CACHED_INPUTS = {};
|
|
||||||
export async function getInput(input) {
|
|
||||||
let sourceNode = CACHED_INPUTS[input];
|
|
||||||
if (sourceNode === undefined) {
|
|
||||||
const ac = getAudioContext();
|
|
||||||
if (!AUDIO_INPUTS) {
|
|
||||||
AUDIO_INPUTS = await getAudioInputs();
|
|
||||||
}
|
|
||||||
const available = Array.from(AUDIO_INPUTS.keys());
|
|
||||||
// Convert numerical inputs to their corresponding device name
|
|
||||||
const deviceName = typeof input === 'number' ? available[input] : input;
|
|
||||||
const inputId = AUDIO_INPUTS.get(deviceName);
|
|
||||||
if (inputId === undefined) {
|
|
||||||
throw new Error(`[superdough] input "${input}" not found. Available inputs: ${available.join(', ')}`);
|
|
||||||
}
|
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({
|
|
||||||
audio: {
|
|
||||||
deviceId: { exact: inputId },
|
|
||||||
channelCount: { ideal: 2 },
|
|
||||||
sampleRate: { ideal: ac.sampleRate },
|
|
||||||
echoCancellation: false,
|
|
||||||
noiseSuppression: false,
|
|
||||||
autoGainControl: false,
|
|
||||||
latency: { ideal: 0.01 },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
sourceNode = ac.createMediaStreamSource(stream);
|
|
||||||
CACHED_INPUTS[input] = sourceNode;
|
|
||||||
}
|
|
||||||
return sourceNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getInputHandle(input, value, start, end) {
|
|
||||||
const inputNode = await getInput(input);
|
|
||||||
const envGain = gainNode(0);
|
|
||||||
inputNode.connect(envGain);
|
|
||||||
const [attack, decay, sustain, release] = getADSRValues(
|
|
||||||
[value.attack, value.decay, value.sustain, value.release],
|
|
||||||
'linear',
|
|
||||||
[0.001, 0.05, 0.6, 0.01],
|
|
||||||
);
|
|
||||||
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, start, end, 'linear');
|
|
||||||
return { inputNode, envGain };
|
|
||||||
}
|
|
||||||
|
|
||||||
let controller;
|
let controller;
|
||||||
export function getSuperdoughAudioController() {
|
export function getSuperdoughAudioController() {
|
||||||
if (controller == null) {
|
if (controller == null) {
|
||||||
@@ -469,9 +408,9 @@ function mapChannelNumbers(channels) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Chain {
|
class Chain {
|
||||||
constructor() {
|
constructor(head) {
|
||||||
this.audioNodes = [];
|
this.audioNodes = [head];
|
||||||
this.tails = [];
|
this.tails = [head];
|
||||||
}
|
}
|
||||||
connect(...nodes) {
|
connect(...nodes) {
|
||||||
nodes.forEach((node) => {
|
nodes.forEach((node) => {
|
||||||
@@ -558,7 +497,6 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
fft = getDefaultValue('fft'), // fftSize 0 - 10
|
fft = getDefaultValue('fft'), // fftSize 0 - 10
|
||||||
FX = [],
|
FX = [],
|
||||||
FXrelease,
|
FXrelease,
|
||||||
input,
|
|
||||||
} = value;
|
} = value;
|
||||||
|
|
||||||
delaytime = delaytime ?? cycleToSeconds(delaysync, cps);
|
delaytime = delaytime ?? cycleToSeconds(delaysync, cps);
|
||||||
@@ -601,28 +539,11 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
value.s = s;
|
value.s = s;
|
||||||
}
|
}
|
||||||
|
|
||||||
const chain = new Chain(); // connection manager which tracks audio nodes for releasing
|
|
||||||
|
|
||||||
// get source AudioNode
|
// get source AudioNode
|
||||||
let sourceNode;
|
let sourceNode;
|
||||||
if (source) {
|
if (source) {
|
||||||
sourceNode = source(t, value, hapDuration, cps);
|
sourceNode = source(t, value, hapDuration, cps);
|
||||||
nodes.main['source'] = [sourceNode];
|
nodes.main['source'] = [sourceNode];
|
||||||
} else if (input) {
|
|
||||||
const { inputNode, envGain } = await getInputHandle(input, value, t, end);
|
|
||||||
sourceNode = envGain;
|
|
||||||
webAudioTimeout(
|
|
||||||
ac,
|
|
||||||
() => {
|
|
||||||
chain.releaseNodes();
|
|
||||||
activeSoundSources.delete(chainID);
|
|
||||||
// We disconnect inputNode instead of releasing it
|
|
||||||
// because we want inputs to persist
|
|
||||||
inputNode.disconnect(envGain);
|
|
||||||
},
|
|
||||||
t,
|
|
||||||
endWithRelease,
|
|
||||||
);
|
|
||||||
} else if (getSound(s)) {
|
} else if (getSound(s)) {
|
||||||
const { onTrigger } = getSound(s);
|
const { onTrigger } = getSound(s);
|
||||||
|
|
||||||
@@ -658,7 +579,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
chain.connect(sourceNode);
|
const chain = new Chain(sourceNode); // connection manager which tracks audio nodes for releasing
|
||||||
FX = [...FX, value]; // run through the FX chain and then run through all FX outside of it as well
|
FX = [...FX, value]; // run through the FX chain and then run through all FX outside of it as well
|
||||||
for (let [idx, fx] of Object.entries(FX)) {
|
for (let [idx, fx] of Object.entries(FX)) {
|
||||||
const key = idx == FX.length - 1 ? 'main' : idx;
|
const key = idx == FX.length - 1 ? 'main' : idx;
|
||||||
|
|||||||
@@ -5484,19 +5484,6 @@ exports[`runs examples > example "inhabit" example index 1 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "input" example index 0 1`] = `
|
|
||||||
[
|
|
||||||
"[ 0/1 → 1/2 | input:1 gain:0.5 stretch:0.5 lfo:{0:{control:stretch sync:0.5} __ids:{}} ]",
|
|
||||||
"[ 1/2 → 1/1 | input:2 gain:0.5 stretch:0.5 lfo:{0:{control:stretch sync:0.5} __ids:{}} ]",
|
|
||||||
"[ 1/1 → 3/2 | input:1 gain:0.5 stretch:0.5 lfo:{0:{control:stretch sync:0.5} __ids:{}} ]",
|
|
||||||
"[ 3/2 → 2/1 | input:2 gain:0.5 stretch:0.5 lfo:{0:{control:stretch sync:0.5} __ids:{}} ]",
|
|
||||||
"[ 2/1 → 5/2 | input:1 gain:0.5 stretch:0.5 lfo:{0:{control:stretch sync:0.5} __ids:{}} ]",
|
|
||||||
"[ 5/2 → 3/1 | input:2 gain:0.5 stretch:0.5 lfo:{0:{control:stretch sync:0.5} __ids:{}} ]",
|
|
||||||
"[ 3/1 → 7/2 | input:1 gain:0.5 stretch:0.5 lfo:{0:{control:stretch sync:0.5} __ids:{}} ]",
|
|
||||||
"[ 7/2 → 4/1 | input:2 gain:0.5 stretch:0.5 lfo:{0:{control:stretch sync:0.5} __ids:{}} ]",
|
|
||||||
]
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`runs examples > example "inside" example index 0 1`] = `
|
exports[`runs examples > example "inside" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/8 | note:D3 ]",
|
"[ 0/1 → 1/8 | note:D3 ]",
|
||||||
|
|||||||
@@ -19,6 +19,14 @@ While there is no charge there are some caveats, e.g.:
|
|||||||
- the source code must stay free, i.e. you cannot distribute strudel or tidal as part of projects with incompatible licenses - see the [license](https://www.gnu.org/licenses/agpl-3.0.en.html) for details.
|
- the source code must stay free, i.e. you cannot distribute strudel or tidal as part of projects with incompatible licenses - see the [license](https://www.gnu.org/licenses/agpl-3.0.en.html) for details.
|
||||||
- the contributed examples and tracks are also separately licensed, and must not e.g. be used to train AI models without permission.
|
- the contributed examples and tracks are also separately licensed, and must not e.g. be used to train AI models without permission.
|
||||||
|
|
||||||
|
## How do I try out the latest features?
|
||||||
|
|
||||||
|
The main, stable strudel website is [strudel.cc](https://strudel.cc/). There is also [warm.strudel.cc](https://warm.strudel.cc), known as "warm strudel", which has the latest development features. You might find warm strudel has bug fixes and features that the main website doesn't, but it will often be less stable and probably not suitable for important performances.
|
||||||
|
|
||||||
|
Alternatively, you can run strudel locally to try out the latest features. You can find development-oriented [instructions for that here](https://codeberg.org/uzu/strudel/src/branch/main/CONTRIBUTING.md#project-setup).
|
||||||
|
|
||||||
|
You can see the [latest changes here](https://codeberg.org/uzu/strudel/pulls?q=&type=all&sort=recentupdate&state=closed&labels=&milestone=0&project=0&assignee=0&poster=0), as 'pull requests'.
|
||||||
|
|
||||||
## How to record or export audio?
|
## How to record or export audio?
|
||||||
|
|
||||||
Strudel is not a digital audio workstation and does not operate following the same principles shared by most traditional audio softwares. However, there are multiple ways to record the audio -- and video -- output of Strudel:
|
Strudel is not a digital audio workstation and does not operate following the same principles shared by most traditional audio softwares. However, there are multiple ways to record the audio -- and video -- output of Strudel:
|
||||||
|
|||||||
Reference in New Issue
Block a user