mirror of
https://codeberg.org/uzu/strudel
synced 2026-09-17 19:14:45 -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 { 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';
|
||||
import drawLine from './drawLine.mjs';
|
||||
import { errorLogger, logger } from './logger.mjs';
|
||||
import { strudelScope } from './evaluate.mjs';
|
||||
|
||||
let stringParser;
|
||||
|
||||
@@ -1683,7 +1684,9 @@ export function register(name, func, patternify = true, preserveSteps = false, j
|
||||
|
||||
// toplevel functions get curried as well as patternified
|
||||
// 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
|
||||
|
||||
@@ -14,11 +14,9 @@ import {
|
||||
createFilter,
|
||||
effectSend,
|
||||
gainNode,
|
||||
getADSRValues,
|
||||
getCompressor,
|
||||
getDistortion,
|
||||
getLfo,
|
||||
getParamADSR,
|
||||
getWorklet,
|
||||
releaseAudioNode,
|
||||
webAudioTimeout,
|
||||
@@ -178,19 +176,6 @@ export const getAudioDevices = async () => {
|
||||
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 = {
|
||||
s: 'triangle',
|
||||
gain: 0.8,
|
||||
@@ -269,7 +254,6 @@ export function loadWorklets() {
|
||||
return workletsLoading;
|
||||
}
|
||||
|
||||
let AUDIO_OUTPUTS;
|
||||
// this function should be called on first user interaction (to avoid console warning)
|
||||
export async function initAudio(options = {}) {
|
||||
const {
|
||||
@@ -290,8 +274,8 @@ export async function initAudio(options = {}) {
|
||||
|
||||
if (audioDeviceName != null && audioDeviceName != DEFAULT_AUDIO_DEVICE_NAME) {
|
||||
try {
|
||||
AUDIO_OUTPUTS = await getAudioDevices();
|
||||
const id = AUDIO_OUTPUTS.get(audioDeviceName);
|
||||
const devices = await getAudioDevices();
|
||||
const id = devices.get(audioDeviceName);
|
||||
const isValidID = (id ?? '').length > 0;
|
||||
if (audioCtx.sinkId !== id && isValidID) {
|
||||
await audioCtx.setSinkId(id);
|
||||
@@ -332,51 +316,6 @@ export async function initAudioOnFirstClick(options) {
|
||||
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;
|
||||
export function getSuperdoughAudioController() {
|
||||
if (controller == null) {
|
||||
@@ -469,9 +408,9 @@ function mapChannelNumbers(channels) {
|
||||
}
|
||||
|
||||
class Chain {
|
||||
constructor() {
|
||||
this.audioNodes = [];
|
||||
this.tails = [];
|
||||
constructor(head) {
|
||||
this.audioNodes = [head];
|
||||
this.tails = [head];
|
||||
}
|
||||
connect(...nodes) {
|
||||
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
|
||||
FX = [],
|
||||
FXrelease,
|
||||
input,
|
||||
} = value;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const chain = new Chain(); // connection manager which tracks audio nodes for releasing
|
||||
|
||||
// get source AudioNode
|
||||
let sourceNode;
|
||||
if (source) {
|
||||
sourceNode = source(t, value, hapDuration, cps);
|
||||
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)) {
|
||||
const { onTrigger } = getSound(s);
|
||||
|
||||
@@ -658,7 +579,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
||||
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
|
||||
for (let [idx, fx] of Object.entries(FX)) {
|
||||
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`] = `
|
||||
[
|
||||
"[ 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 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?
|
||||
|
||||
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