Merge branch 'main' into patterns-tab
This commit is contained in:
commit
e72c9cbebf
49 changed files with 712 additions and 255 deletions
|
|
@ -381,6 +381,19 @@ const generic_params = [
|
|||
*/
|
||||
['coarse'],
|
||||
|
||||
/**
|
||||
* Allows you to set the output channels on the interface
|
||||
*
|
||||
* @name channels
|
||||
* @synonyms ch
|
||||
*
|
||||
* @param {number | Pattern} channels pattern the output channels
|
||||
* @example
|
||||
* note("e a d b g").channels("3:4")
|
||||
*
|
||||
*/
|
||||
['channels', 'ch'],
|
||||
|
||||
['phaserrate', 'phasr'], // superdirt only
|
||||
|
||||
/**
|
||||
|
|
@ -1214,6 +1227,16 @@ const generic_params = [
|
|||
* @name waveloss
|
||||
*/
|
||||
['waveloss'],
|
||||
/*
|
||||
* Noise crackle density
|
||||
*
|
||||
* @name density
|
||||
* @param {number | Pattern} density between 0 and x
|
||||
* @example
|
||||
* s("crackle*4").density("<0.01 0.04 0.2 0.5>".slow(4))
|
||||
*
|
||||
*/
|
||||
['density'],
|
||||
// TODO: midi effects?
|
||||
['dur'],
|
||||
// ['modwheel'],
|
||||
|
|
|
|||
|
|
@ -1985,7 +1985,7 @@ Pattern.prototype.hush = function () {
|
|||
* note("c d e g").palindrome()
|
||||
*/
|
||||
export const palindrome = register('palindrome', function (pat) {
|
||||
return pat.every(2, rev);
|
||||
return pat.lastOf(2, rev);
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
@ -2189,6 +2189,14 @@ export const duration = register('duration', function (value, pat) {
|
|||
return pat.withHapSpan((span) => new TimeSpan(span.begin, span.begin.add(value)));
|
||||
});
|
||||
|
||||
export const hsla = register('hsla', (h, s, l, a, pat) => {
|
||||
return pat.color(`hsla(${h}turn,${s * 100}%,${l * 100}%,${a})`);
|
||||
});
|
||||
|
||||
export const hsl = register('hsl', (h, s, l, pat) => {
|
||||
return pat.color(`hsl(${h}turn,${s * 100}%,${l * 100}%)`);
|
||||
});
|
||||
|
||||
/**
|
||||
* Sets the color of the hap in visualizations like pianoroll or highlighting.
|
||||
* @name color
|
||||
|
|
|
|||
|
|
@ -89,19 +89,22 @@ export function repl({
|
|||
allTransform = transform;
|
||||
return silence;
|
||||
};
|
||||
|
||||
for (let i = 1; i < 10; ++i) {
|
||||
Object.defineProperty(Pattern.prototype, `d${i}`, {
|
||||
get() {
|
||||
return this.p(i);
|
||||
},
|
||||
});
|
||||
Object.defineProperty(Pattern.prototype, `p${i}`, {
|
||||
get() {
|
||||
return this.p(i);
|
||||
},
|
||||
});
|
||||
Pattern.prototype[`q${i}`] = silence;
|
||||
try {
|
||||
for (let i = 1; i < 10; ++i) {
|
||||
Object.defineProperty(Pattern.prototype, `d${i}`, {
|
||||
get() {
|
||||
return this.p(i);
|
||||
},
|
||||
});
|
||||
Object.defineProperty(Pattern.prototype, `p${i}`, {
|
||||
get() {
|
||||
return this.p(i);
|
||||
},
|
||||
});
|
||||
Pattern.prototype[`q${i}`] = silence;
|
||||
}
|
||||
} catch (err) {
|
||||
// already defined..
|
||||
}
|
||||
|
||||
const fit = register('fit', (pat) =>
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import {
|
|||
slowcat,
|
||||
cat,
|
||||
sequence,
|
||||
palindrome,
|
||||
polymeter,
|
||||
polymeterSteps,
|
||||
polyrhythm,
|
||||
|
|
@ -571,6 +572,18 @@ describe('Pattern', () => {
|
|||
expect(sequence(1, 2, 3).firstCycle()).toStrictEqual(fastcat(1, 2, 3).firstCycle());
|
||||
});
|
||||
});
|
||||
describe('palindrome()', () => {
|
||||
it('Can create palindrome', () => {
|
||||
expect(
|
||||
fastcat('a', 'b', 'c')
|
||||
.palindrome()
|
||||
.fast(2)
|
||||
.firstCycle()
|
||||
.sort((a, b) => a.part.begin.sub(b.part.begin))
|
||||
.map((a) => a.value),
|
||||
).toStrictEqual(['a', 'b', 'c', 'c', 'b', 'a']);
|
||||
});
|
||||
});
|
||||
describe('polyrhythm()', () => {
|
||||
it('Can layer up cycles', () => {
|
||||
expect(polyrhythm(['a', 'b'], ['c']).firstCycle()).toStrictEqual(
|
||||
|
|
|
|||
|
|
@ -12,6 +12,12 @@ await initHydra();
|
|||
|
||||
Then you can use hydra below!
|
||||
|
||||
### options
|
||||
|
||||
You can also pass options to the `initHydra` function. These can be used to set [hydra options](https://github.com/hydra-synth/hydra-synth#api) + these strudel specific options:
|
||||
|
||||
- `feedStrudel`: sends the strudel canvas to `s0`. The strudel canvas is used to draw `pianoroll`, `spiral`, `scope` etc..
|
||||
|
||||
## Usage via npm
|
||||
|
||||
```sh
|
||||
|
|
|
|||
|
|
@ -1,14 +1,38 @@
|
|||
import { getDrawContext } from '@strudel.cycles/core';
|
||||
|
||||
export async function initHydra() {
|
||||
let latestOptions;
|
||||
|
||||
function appendCanvas(c) {
|
||||
const { canvas: testCanvas } = getDrawContext();
|
||||
c.canvas.id = 'hydra-canvas';
|
||||
c.canvas.style.position = 'fixed';
|
||||
c.canvas.style.top = '0px';
|
||||
testCanvas.after(c.canvas);
|
||||
return testCanvas;
|
||||
}
|
||||
|
||||
export async function initHydra(options = {}) {
|
||||
// reset if options have changed since last init
|
||||
if (latestOptions && JSON.stringify(latestOptions) !== JSON.stringify(options)) {
|
||||
document.getElementById('hydra-canvas').remove();
|
||||
}
|
||||
latestOptions = options;
|
||||
//load and init hydra
|
||||
if (!document.getElementById('hydra-canvas')) {
|
||||
const { canvas: testCanvas } = getDrawContext();
|
||||
await import('https://unpkg.com/hydra-synth');
|
||||
const hydraCanvas = testCanvas.cloneNode(true);
|
||||
hydraCanvas.id = 'hydra-canvas';
|
||||
testCanvas.after(hydraCanvas);
|
||||
new Hydra({ canvas: hydraCanvas, detectAudio: false });
|
||||
s0.init({ src: testCanvas });
|
||||
console.log('reinit..');
|
||||
const {
|
||||
src = 'https://unpkg.com/hydra-synth',
|
||||
feedStrudel = false,
|
||||
...hydraConfig
|
||||
} = { detectAudio: false, ...options };
|
||||
await import(src);
|
||||
const hydra = new Hydra(hydraConfig);
|
||||
if (feedStrudel) {
|
||||
const { canvas } = getDrawContext();
|
||||
canvas.style.display = 'none';
|
||||
hydra.synth.s0.init({ src: canvas });
|
||||
}
|
||||
appendCanvas(hydra);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,16 +2,23 @@ import { createRoot } from 'react-dom/client';
|
|||
import jsdoc from '../../../../doc.json';
|
||||
|
||||
const getDocLabel = (doc) => doc.name || doc.longname;
|
||||
const getDocSynonyms = (doc) => [getDocLabel(doc), ...(doc.synonyms || [])];
|
||||
const getInnerText = (html) => {
|
||||
var div = document.createElement('div');
|
||||
div.innerHTML = html;
|
||||
return div.textContent || div.innerText || '';
|
||||
};
|
||||
|
||||
export function Autocomplete({ doc }) {
|
||||
export function Autocomplete({ doc, label = getDocLabel(doc) }) {
|
||||
const synonyms = getDocSynonyms(doc).filter((a) => a !== label);
|
||||
return (
|
||||
<div className="prose dark:prose-invert max-h-[400px] overflow-auto">
|
||||
<h3 className="pt-0 mt-0">{getDocLabel(doc)}</h3>
|
||||
<h3 className="pt-0 mt-0">{label}</h3>{' '}
|
||||
{!!synonyms.length && (
|
||||
<span>
|
||||
Synonyms: <code>{synonyms.join(', ')}</code>
|
||||
</span>
|
||||
)}
|
||||
<div dangerouslySetInnerHTML={{ __html: doc.description }} />
|
||||
<ul>
|
||||
{doc.params?.map(({ name, type, description }, i) => (
|
||||
|
|
@ -48,18 +55,24 @@ const jsdocCompletions = jsdoc.docs
|
|||
!['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag)),
|
||||
)
|
||||
// https://codemirror.net/docs/ref/#autocomplete.Completion
|
||||
.map((doc) /*: Completion */ => ({
|
||||
label: getDocLabel(doc),
|
||||
// detail: 'xxx', // An optional short piece of information to show (with a different style) after the label.
|
||||
info: () => {
|
||||
const node = document.createElement('div');
|
||||
// if Autocomplete is non-interactive, it could also be rendered at build time..
|
||||
// .. using renderToStaticMarkup
|
||||
createRoot(node).render(<Autocomplete doc={doc} />);
|
||||
return node;
|
||||
},
|
||||
type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type
|
||||
}));
|
||||
.reduce(
|
||||
(acc, doc) /*: Completion */ =>
|
||||
acc.concat(
|
||||
[getDocLabel(doc), ...(doc.synonyms || [])].map((label) => ({
|
||||
label,
|
||||
// detail: 'xxx', // An optional short piece of information to show (with a different style) after the label.
|
||||
info: () => {
|
||||
const node = document.createElement('div');
|
||||
// if Autocomplete is non-interactive, it could also be rendered at build time..
|
||||
// .. using renderToStaticMarkup
|
||||
createRoot(node).render(<Autocomplete doc={doc} label={label} />);
|
||||
return node;
|
||||
},
|
||||
type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type
|
||||
})),
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
export const strudelAutocomplete = (context /* : CompletionContext */) => {
|
||||
let word = context.matchBefore(/\w*/);
|
||||
|
|
|
|||
|
|
@ -31,6 +31,9 @@ window.addEventListener(
|
|||
export const strudelTooltip = hoverTooltip(
|
||||
(view, pos, side) => {
|
||||
// Word selection from CodeMirror Hover Tooltip example https://codemirror.net/examples/tooltip/#hover-tooltips
|
||||
if (!ctrlDown) {
|
||||
return null;
|
||||
}
|
||||
let { from, to, text } = view.state.doc.lineAt(pos);
|
||||
let start = pos,
|
||||
end = pos;
|
||||
|
|
@ -47,11 +50,13 @@ export const strudelTooltip = hoverTooltip(
|
|||
// Get entry from Strudel documentation
|
||||
let entry = jsdoc.docs.filter((doc) => getDocLabel(doc) === word)[0];
|
||||
if (!entry) {
|
||||
return null;
|
||||
}
|
||||
if (!ctrlDown) {
|
||||
return null;
|
||||
// Try for synonyms
|
||||
entry = jsdoc.docs.filter((doc) => doc.synonyms && doc.synonyms.includes(word))[0];
|
||||
if (!entry) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
pos: start,
|
||||
end,
|
||||
|
|
@ -60,7 +65,7 @@ export const strudelTooltip = hoverTooltip(
|
|||
create(view) {
|
||||
let dom = document.createElement('div');
|
||||
dom.className = 'strudel-tooltip';
|
||||
createRoot(dom).render(<Autocomplete doc={entry} />);
|
||||
createRoot(dom).render(<Autocomplete doc={entry} label={word} />);
|
||||
return { dom };
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { getAudioContext } from './superdough.mjs';
|
|||
let noiseCache = {};
|
||||
|
||||
// lazy generates noise buffers and keeps them forever
|
||||
function getNoiseBuffer(type) {
|
||||
function getNoiseBuffer(type, density) {
|
||||
const ac = getAudioContext();
|
||||
if (noiseCache[type]) {
|
||||
return noiseCache[type];
|
||||
|
|
@ -34,17 +34,26 @@ function getNoiseBuffer(type) {
|
|||
output[i] = b0 + b1 + b2 + b3 + b4 + b5 + b6 + white * 0.5362;
|
||||
output[i] *= 0.11;
|
||||
b6 = white * 0.115926;
|
||||
} else if (type === 'crackle') {
|
||||
const probability = density * 0.01;
|
||||
if (Math.random() < probability) {
|
||||
output[i] = Math.random() * 2 - 1;
|
||||
} else {
|
||||
output[i] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
noiseCache[type] = noiseBuffer;
|
||||
|
||||
// Prevent caching to randomize crackles
|
||||
if (type !== 'crackle') noiseCache[type] = noiseBuffer;
|
||||
return noiseBuffer;
|
||||
}
|
||||
|
||||
// expects one of noises as type
|
||||
export function getNoiseOscillator(type = 'white', t) {
|
||||
export function getNoiseOscillator(type = 'white', t, density = 0.02) {
|
||||
const ac = getAudioContext();
|
||||
const o = ac.createBufferSource();
|
||||
o.buffer = getNoiseBuffer(type);
|
||||
o.buffer = getNoiseBuffer(type, density);
|
||||
o.loop = true;
|
||||
o.start(t);
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -27,28 +27,16 @@ export function getSound(s) {
|
|||
export const resetLoadedSounds = () => soundMap.set({});
|
||||
|
||||
let audioContext;
|
||||
|
||||
export const getAudioContext = () => {
|
||||
if (!audioContext) {
|
||||
audioContext = new AudioContext();
|
||||
const maxChannelCount = audioContext.destination.maxChannelCount;
|
||||
audioContext.destination.channelCount = maxChannelCount;
|
||||
}
|
||||
return audioContext;
|
||||
};
|
||||
|
||||
let destination;
|
||||
const getDestination = () => {
|
||||
const ctx = getAudioContext();
|
||||
if (!destination) {
|
||||
destination = ctx.createGain();
|
||||
destination.connect(ctx.destination);
|
||||
}
|
||||
return destination;
|
||||
};
|
||||
|
||||
export const panic = () => {
|
||||
getDestination().gain.linearRampToValueAtTime(0, getAudioContext().currentTime + 0.01);
|
||||
destination = null;
|
||||
};
|
||||
|
||||
let workletsLoading;
|
||||
|
||||
function loadWorklets() {
|
||||
|
|
@ -95,6 +83,39 @@ export async function initAudioOnFirstClick(options) {
|
|||
let delays = {};
|
||||
const maxfeedback = 0.98;
|
||||
|
||||
let channelMerger, destinationGain;
|
||||
|
||||
// input: AudioNode, channels: ?Array<int>
|
||||
export const connectToDestination = (input, channels = [0, 1]) => {
|
||||
const ctx = getAudioContext();
|
||||
if (channelMerger == null) {
|
||||
channelMerger = new ChannelMergerNode(ctx, { numberOfInputs: ctx.destination.channelCount });
|
||||
destinationGain = new GainNode(ctx);
|
||||
channelMerger.connect(destinationGain);
|
||||
destinationGain.connect(ctx.destination);
|
||||
}
|
||||
//This upmix can be removed if correct channel counts are set throughout the app,
|
||||
// and then strudel could theoretically support surround sound audio files
|
||||
const stereoMix = new StereoPannerNode(ctx);
|
||||
input.connect(stereoMix);
|
||||
|
||||
const splitter = new ChannelSplitterNode(ctx, {
|
||||
numberOfOutputs: stereoMix.channelCount,
|
||||
});
|
||||
stereoMix.connect(splitter);
|
||||
channels.forEach((ch, i) => {
|
||||
splitter.connect(channelMerger, i % stereoMix.channelCount, clamp(ch, 0, ctx.destination.channelCount - 1));
|
||||
});
|
||||
};
|
||||
|
||||
export const panic = () => {
|
||||
if (destinationGain == null) {
|
||||
return;
|
||||
}
|
||||
destinationGain.gain.linearRampToValueAtTime(0, getAudioContext().currentTime + 0.01);
|
||||
destinationGain = null;
|
||||
};
|
||||
|
||||
function getDelay(orbit, delaytime, delayfeedback, t) {
|
||||
if (delayfeedback > maxfeedback) {
|
||||
//logger(`delayfeedback was clamped to ${maxfeedback} to save your ears`);
|
||||
|
|
@ -104,7 +125,7 @@ function getDelay(orbit, delaytime, delayfeedback, t) {
|
|||
const ac = getAudioContext();
|
||||
const dly = ac.createFeedbackDelay(1, delaytime, delayfeedback);
|
||||
dly.start?.(t); // for some reason, this throws when audion extension is installed..
|
||||
dly.connect(getDestination());
|
||||
connectToDestination(dly, [0, 1]);
|
||||
delays[orbit] = dly;
|
||||
}
|
||||
delays[orbit].delayTime.value !== delaytime && delays[orbit].delayTime.setValueAtTime(delaytime, t);
|
||||
|
|
@ -163,7 +184,7 @@ function getReverb(orbit, duration, fade, lp, dim, ir) {
|
|||
if (!reverbs[orbit]) {
|
||||
const ac = getAudioContext();
|
||||
const reverb = ac.createReverb(duration, fade, lp, dim, ir);
|
||||
reverb.connect(getDestination());
|
||||
connectToDestination(reverb, [0, 1]);
|
||||
reverbs[orbit] = reverb;
|
||||
}
|
||||
if (
|
||||
|
|
@ -241,6 +262,7 @@ export const superdough = async (value, deadline, hapDuration) => {
|
|||
source,
|
||||
gain = 0.8,
|
||||
postgain = 1,
|
||||
density = 0.03,
|
||||
// filters
|
||||
ftype = '12db',
|
||||
fanchor = 0.5,
|
||||
|
|
@ -268,7 +290,7 @@ export const superdough = async (value, deadline, hapDuration) => {
|
|||
bpsustain = 1,
|
||||
bprelease = 0.01,
|
||||
bandq = 1,
|
||||
|
||||
channels = [1, 2],
|
||||
//phaser
|
||||
phaser,
|
||||
phaserdepth = 0.75,
|
||||
|
|
@ -301,6 +323,10 @@ export const superdough = async (value, deadline, hapDuration) => {
|
|||
compressorRelease,
|
||||
} = value;
|
||||
gain = nanFallback(gain, 1);
|
||||
|
||||
//music programs/audio gear usually increments inputs/outputs from 1, so imitate that behavior
|
||||
channels = (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1);
|
||||
|
||||
gain *= velocity; // legacy fix for velocity
|
||||
let toDisconnect = []; // audio nodes that will be disconnected when the source has ended
|
||||
const onended = () => {
|
||||
|
|
@ -434,9 +460,9 @@ export const superdough = async (value, deadline, hapDuration) => {
|
|||
}
|
||||
|
||||
// last gain
|
||||
const post = gainNode(postgain);
|
||||
const post = new GainNode(ac, { gain: postgain });
|
||||
chain.push(post);
|
||||
post.connect(getDestination());
|
||||
connectToDestination(post, channels);
|
||||
|
||||
// delay
|
||||
let delaySend;
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ const fm = (osc, harmonicityRatio, modulationIndex, wave = 'sine') => {
|
|||
};
|
||||
|
||||
const waveforms = ['sine', 'square', 'triangle', 'sawtooth'];
|
||||
const noises = ['pink', 'white', 'brown'];
|
||||
const noises = ['pink', 'white', 'brown', 'crackle'];
|
||||
|
||||
export function registerSynthSounds() {
|
||||
[...waveforms, ...noises].forEach((s) => {
|
||||
|
|
@ -36,7 +36,8 @@ export function registerSynthSounds() {
|
|||
if (waveforms.includes(s)) {
|
||||
sound = getOscillator(s, t, value);
|
||||
} else {
|
||||
sound = getNoiseOscillator(s, t);
|
||||
let { density } = value;
|
||||
sound = getNoiseOscillator(s, t, density);
|
||||
}
|
||||
|
||||
let { node: o, stop, triggerRelease } = sound;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue