Merge branch 'main' into patterns-tab

This commit is contained in:
Felix Roos 2023-12-07 17:33:07 +01:00
commit e72c9cbebf
49 changed files with 712 additions and 255 deletions

View File

@ -20,4 +20,5 @@ vite.config.js
**/dist **/dist
/src-tauri/target/**/* /src-tauri/target/**/*
reverbGen.mjs reverbGen.mjs
hydra.mjs hydra.mjs
jsdoc-synonyms.js

View File

@ -21,10 +21,10 @@ jobs:
name: github-pages name: github-pages
url: ${{ steps.deployment.outputs.page_url }} url: ${{ steps.deployment.outputs.page_url }}
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- uses: pnpm/action-setup@v2 - uses: pnpm/action-setup@v2
with: with:
version: 7 version: 8.11.0
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
node-version: 18 node-version: 18

View File

@ -10,10 +10,10 @@ jobs:
node-version: [18] node-version: [18]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- uses: pnpm/action-setup@v2 - uses: pnpm/action-setup@v2
with: with:
version: 7 version: 8.11.0
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}

85
.gitignore vendored
View File

@ -43,4 +43,87 @@ dev-dist
Dirt-Samples Dirt-Samples
tidal-drum-machines tidal-drum-machines
webaudiofontdata webaudiofontdata
src-tauri/target src-tauri/target
# BEGIN JetBrains -> END JetBrains
# for JetBrains IDE users, e.g. WebStorm. Source: https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
# END JetBrains -> BEGIN JetBrains

View File

@ -66,7 +66,7 @@ To get the project up and running for development, make sure you have installed:
- [git](https://git-scm.com/) - [git](https://git-scm.com/)
- [node](https://nodejs.org/en/) >= 18 - [node](https://nodejs.org/en/) >= 18
- [pnpm](https://pnpm.io/) (`npm i pnpm -g`) - [pnpm](https://pnpm.io/) (`curl -fsSL https://get.pnpm.io/install.sh | env PNPM_VERSION=8.11.0 sh -`)
then, do the following: then, do the following:

View File

@ -30,7 +30,6 @@ There are multiple npm packages you can use to use strudel, or only parts of it,
- [`midi`](./packages/midi): webmidi bindings - [`midi`](./packages/midi): webmidi bindings
- [`serial`](./packages/serial): webserial bindings - [`serial`](./packages/serial): webserial bindings
- [`tonal`](./packages/tonal): tonal functions - [`tonal`](./packages/tonal): tonal functions
- [`xen`](./packages/xen): microtonal / xenharmonic functions
- ... [and there are more](./packages/) - ... [and there are more](./packages/)
Click on the package names to find out more about each one. Click on the package names to find out more about each one.

17
jsdoc/jsdoc-synonyms.js Normal file
View File

@ -0,0 +1,17 @@
/*
jsdoc-synonyms.js - Add support for @synonym tag
Copyright (C) 2023 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/midi/midi.mjs>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
function defineTags(dictionary) {
dictionary.defineTag('synonyms', {
mustHaveValue: true,
onTagged: function (doclet, tag) {
doclet.synonyms_text = tag.value;
doclet.synonyms = doclet.synonyms_text.split(/[ ,]+/);
},
});
}
module.exports = { defineTags: defineTags };

View File

@ -3,7 +3,7 @@
"includePattern": ".+\\.(js(doc|x)?|mjs)$", "includePattern": ".+\\.(js(doc|x)?|mjs)$",
"excludePattern": "node_modules|shift-parser|shift-reducer|shift-traverser|dist" "excludePattern": "node_modules|shift-parser|shift-reducer|shift-traverser|dist"
}, },
"plugins": ["plugins/markdown"], "plugins": ["plugins/markdown", "jsdoc/jsdoc-synonyms"],
"opts": { "opts": {
"destination": "./out/", "destination": "./out/",
"recurse": true "recurse": true

View File

@ -65,7 +65,7 @@ async function getUndocumented(path, docs) {
} }
// read doc.json file // read doc.json file
const { docs } = JSON.parse(await readFile(resolve(__dirname, 'doc.json'), 'utf8')); const { docs } = JSON.parse(await readFile(resolve(__dirname, '..', 'doc.json'), 'utf8'));
const paths = dependencyTree.toList({ const paths = dependencyTree.toList({
filename: 'index.mjs', filename: 'index.mjs',
@ -76,7 +76,9 @@ const paths = dependencyTree.toList({
// const paths = ['../packages/core/pattern.mjs', '../packages/core/hap.mjs'].map((rel) => resolve(__dirname, rel)); // const paths = ['../packages/core/pattern.mjs', '../packages/core/hap.mjs'].map((rel) => resolve(__dirname, rel));
const undocumented = Object.fromEntries( const undocumented = Object.fromEntries(
await Promise.all(paths.map(async (path) => [path, await getUndocumented(path, docs)])), await Promise.all(
paths.map(async (path) => [path.replace(resolve(__dirname, '..'), ''), await getUndocumented(path, docs)]),
),
); );
console.log(JSON.stringify(undocumented, null, 2)); console.log(JSON.stringify(undocumented, null, 2));

View File

@ -18,12 +18,12 @@
"build": "npm run prebuild && cd website && npm run build", "build": "npm run prebuild && cd website && npm run build",
"preview": "cd website && npm run preview", "preview": "cd website && npm run preview",
"osc": "cd packages/osc && npm run server", "osc": "cd packages/osc && npm run server",
"jsdoc": "jsdoc packages/ -c jsdoc.config.json", "jsdoc": "jsdoc packages/ -c jsdoc/jsdoc.config.json",
"jsdoc-json": "jsdoc packages/ --template ./node_modules/jsdoc-json --destination doc.json -c jsdoc.config.json", "jsdoc-json": "jsdoc packages/ --template ./node_modules/jsdoc-json --destination doc.json -c jsdoc/jsdoc.config.json",
"lint": "eslint . --ext mjs,js --quiet", "lint": "eslint . --ext mjs,js --quiet",
"codeformat": "prettier --write .", "codeformat": "prettier --write .",
"format-check": "prettier --check .", "format-check": "prettier --check .",
"report-undocumented": "npm run jsdoc-json && node undocumented.mjs > undocumented.json", "report-undocumented": "npm run jsdoc-json && node jsdoc/undocumented.mjs > undocumented.json",
"check": "npm run format-check && npm run lint && npm run test", "check": "npm run format-check && npm run lint && npm run test",
"iclc": "cd paper && pandoc --template=pandoc/iclc.html --citeproc --number-sections iclc2023.md -o iclc2023.html && pandoc --template=pandoc/iclc.latex --citeproc --number-sections iclc2023.md -o iclc2023.pdf" "iclc": "cd paper && pandoc --template=pandoc/iclc.html --citeproc --number-sections iclc2023.md -o iclc2023.html && pandoc --template=pandoc/iclc.latex --citeproc --number-sections iclc2023.md -o iclc2023.pdf"
}, },

View File

@ -381,6 +381,19 @@ const generic_params = [
*/ */
['coarse'], ['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 ['phaserrate', 'phasr'], // superdirt only
/** /**
@ -1214,6 +1227,16 @@ const generic_params = [
* @name waveloss * @name waveloss
*/ */
['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? // TODO: midi effects?
['dur'], ['dur'],
// ['modwheel'], // ['modwheel'],

View File

@ -1985,7 +1985,7 @@ Pattern.prototype.hush = function () {
* note("c d e g").palindrome() * note("c d e g").palindrome()
*/ */
export const palindrome = register('palindrome', function (pat) { 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))); 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. * Sets the color of the hap in visualizations like pianoroll or highlighting.
* @name color * @name color

View File

@ -89,19 +89,22 @@ export function repl({
allTransform = transform; allTransform = transform;
return silence; return silence;
}; };
try {
for (let i = 1; i < 10; ++i) { for (let i = 1; i < 10; ++i) {
Object.defineProperty(Pattern.prototype, `d${i}`, { Object.defineProperty(Pattern.prototype, `d${i}`, {
get() { get() {
return this.p(i); return this.p(i);
}, },
}); });
Object.defineProperty(Pattern.prototype, `p${i}`, { Object.defineProperty(Pattern.prototype, `p${i}`, {
get() { get() {
return this.p(i); return this.p(i);
}, },
}); });
Pattern.prototype[`q${i}`] = silence; Pattern.prototype[`q${i}`] = silence;
}
} catch (err) {
// already defined..
} }
const fit = register('fit', (pat) => const fit = register('fit', (pat) =>

View File

@ -20,6 +20,7 @@ import {
slowcat, slowcat,
cat, cat,
sequence, sequence,
palindrome,
polymeter, polymeter,
polymeterSteps, polymeterSteps,
polyrhythm, polyrhythm,
@ -571,6 +572,18 @@ describe('Pattern', () => {
expect(sequence(1, 2, 3).firstCycle()).toStrictEqual(fastcat(1, 2, 3).firstCycle()); 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()', () => { describe('polyrhythm()', () => {
it('Can layer up cycles', () => { it('Can layer up cycles', () => {
expect(polyrhythm(['a', 'b'], ['c']).firstCycle()).toStrictEqual( expect(polyrhythm(['a', 'b'], ['c']).firstCycle()).toStrictEqual(

View File

@ -12,6 +12,12 @@ await initHydra();
Then you can use hydra below! 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 ## Usage via npm
```sh ```sh

View File

@ -1,14 +1,38 @@
import { getDrawContext } from '@strudel.cycles/core'; 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')) { if (!document.getElementById('hydra-canvas')) {
const { canvas: testCanvas } = getDrawContext(); console.log('reinit..');
await import('https://unpkg.com/hydra-synth'); const {
const hydraCanvas = testCanvas.cloneNode(true); src = 'https://unpkg.com/hydra-synth',
hydraCanvas.id = 'hydra-canvas'; feedStrudel = false,
testCanvas.after(hydraCanvas); ...hydraConfig
new Hydra({ canvas: hydraCanvas, detectAudio: false }); } = { detectAudio: false, ...options };
s0.init({ src: testCanvas }); 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);
} }
} }

View File

@ -2,16 +2,23 @@ import { createRoot } from 'react-dom/client';
import jsdoc from '../../../../doc.json'; import jsdoc from '../../../../doc.json';
const getDocLabel = (doc) => doc.name || doc.longname; const getDocLabel = (doc) => doc.name || doc.longname;
const getDocSynonyms = (doc) => [getDocLabel(doc), ...(doc.synonyms || [])];
const getInnerText = (html) => { const getInnerText = (html) => {
var div = document.createElement('div'); var div = document.createElement('div');
div.innerHTML = html; div.innerHTML = html;
return div.textContent || div.innerText || ''; 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 ( return (
<div className="prose dark:prose-invert max-h-[400px] overflow-auto"> <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 }} /> <div dangerouslySetInnerHTML={{ __html: doc.description }} />
<ul> <ul>
{doc.params?.map(({ name, type, description }, i) => ( {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)), !['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag)),
) )
// https://codemirror.net/docs/ref/#autocomplete.Completion // https://codemirror.net/docs/ref/#autocomplete.Completion
.map((doc) /*: Completion */ => ({ .reduce(
label: getDocLabel(doc), (acc, doc) /*: Completion */ =>
// detail: 'xxx', // An optional short piece of information to show (with a different style) after the label. acc.concat(
info: () => { [getDocLabel(doc), ...(doc.synonyms || [])].map((label) => ({
const node = document.createElement('div'); label,
// if Autocomplete is non-interactive, it could also be rendered at build time.. // detail: 'xxx', // An optional short piece of information to show (with a different style) after the label.
// .. using renderToStaticMarkup info: () => {
createRoot(node).render(<Autocomplete doc={doc} />); const node = document.createElement('div');
return node; // if Autocomplete is non-interactive, it could also be rendered at build time..
}, // .. using renderToStaticMarkup
type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type 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 */) => { export const strudelAutocomplete = (context /* : CompletionContext */) => {
let word = context.matchBefore(/\w*/); let word = context.matchBefore(/\w*/);

View File

@ -31,6 +31,9 @@ window.addEventListener(
export const strudelTooltip = hoverTooltip( export const strudelTooltip = hoverTooltip(
(view, pos, side) => { (view, pos, side) => {
// Word selection from CodeMirror Hover Tooltip example https://codemirror.net/examples/tooltip/#hover-tooltips // 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 { from, to, text } = view.state.doc.lineAt(pos);
let start = pos, let start = pos,
end = pos; end = pos;
@ -47,11 +50,13 @@ export const strudelTooltip = hoverTooltip(
// Get entry from Strudel documentation // Get entry from Strudel documentation
let entry = jsdoc.docs.filter((doc) => getDocLabel(doc) === word)[0]; let entry = jsdoc.docs.filter((doc) => getDocLabel(doc) === word)[0];
if (!entry) { if (!entry) {
return null; // Try for synonyms
} entry = jsdoc.docs.filter((doc) => doc.synonyms && doc.synonyms.includes(word))[0];
if (!ctrlDown) { if (!entry) {
return null; return null;
}
} }
return { return {
pos: start, pos: start,
end, end,
@ -60,7 +65,7 @@ export const strudelTooltip = hoverTooltip(
create(view) { create(view) {
let dom = document.createElement('div'); let dom = document.createElement('div');
dom.className = 'strudel-tooltip'; dom.className = 'strudel-tooltip';
createRoot(dom).render(<Autocomplete doc={entry} />); createRoot(dom).render(<Autocomplete doc={entry} label={word} />);
return { dom }; return { dom };
}, },
}; };

View File

@ -4,7 +4,7 @@ import { getAudioContext } from './superdough.mjs';
let noiseCache = {}; let noiseCache = {};
// lazy generates noise buffers and keeps them forever // lazy generates noise buffers and keeps them forever
function getNoiseBuffer(type) { function getNoiseBuffer(type, density) {
const ac = getAudioContext(); const ac = getAudioContext();
if (noiseCache[type]) { if (noiseCache[type]) {
return 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] = b0 + b1 + b2 + b3 + b4 + b5 + b6 + white * 0.5362;
output[i] *= 0.11; output[i] *= 0.11;
b6 = white * 0.115926; 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; return noiseBuffer;
} }
// expects one of noises as type // 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 ac = getAudioContext();
const o = ac.createBufferSource(); const o = ac.createBufferSource();
o.buffer = getNoiseBuffer(type); o.buffer = getNoiseBuffer(type, density);
o.loop = true; o.loop = true;
o.start(t); o.start(t);
return { return {

View File

@ -27,28 +27,16 @@ export function getSound(s) {
export const resetLoadedSounds = () => soundMap.set({}); export const resetLoadedSounds = () => soundMap.set({});
let audioContext; let audioContext;
export const getAudioContext = () => { export const getAudioContext = () => {
if (!audioContext) { if (!audioContext) {
audioContext = new AudioContext(); audioContext = new AudioContext();
const maxChannelCount = audioContext.destination.maxChannelCount;
audioContext.destination.channelCount = maxChannelCount;
} }
return audioContext; 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; let workletsLoading;
function loadWorklets() { function loadWorklets() {
@ -95,6 +83,39 @@ export async function initAudioOnFirstClick(options) {
let delays = {}; let delays = {};
const maxfeedback = 0.98; 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) { function getDelay(orbit, delaytime, delayfeedback, t) {
if (delayfeedback > maxfeedback) { if (delayfeedback > maxfeedback) {
//logger(`delayfeedback was clamped to ${maxfeedback} to save your ears`); //logger(`delayfeedback was clamped to ${maxfeedback} to save your ears`);
@ -104,7 +125,7 @@ function getDelay(orbit, delaytime, delayfeedback, t) {
const ac = getAudioContext(); const ac = getAudioContext();
const dly = ac.createFeedbackDelay(1, delaytime, delayfeedback); const dly = ac.createFeedbackDelay(1, delaytime, delayfeedback);
dly.start?.(t); // for some reason, this throws when audion extension is installed.. 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] = dly;
} }
delays[orbit].delayTime.value !== delaytime && delays[orbit].delayTime.setValueAtTime(delaytime, t); 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]) { if (!reverbs[orbit]) {
const ac = getAudioContext(); const ac = getAudioContext();
const reverb = ac.createReverb(duration, fade, lp, dim, ir); const reverb = ac.createReverb(duration, fade, lp, dim, ir);
reverb.connect(getDestination()); connectToDestination(reverb, [0, 1]);
reverbs[orbit] = reverb; reverbs[orbit] = reverb;
} }
if ( if (
@ -241,6 +262,7 @@ export const superdough = async (value, deadline, hapDuration) => {
source, source,
gain = 0.8, gain = 0.8,
postgain = 1, postgain = 1,
density = 0.03,
// filters // filters
ftype = '12db', ftype = '12db',
fanchor = 0.5, fanchor = 0.5,
@ -268,7 +290,7 @@ export const superdough = async (value, deadline, hapDuration) => {
bpsustain = 1, bpsustain = 1,
bprelease = 0.01, bprelease = 0.01,
bandq = 1, bandq = 1,
channels = [1, 2],
//phaser //phaser
phaser, phaser,
phaserdepth = 0.75, phaserdepth = 0.75,
@ -301,6 +323,10 @@ export const superdough = async (value, deadline, hapDuration) => {
compressorRelease, compressorRelease,
} = value; } = value;
gain = nanFallback(gain, 1); 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 gain *= velocity; // legacy fix for velocity
let toDisconnect = []; // audio nodes that will be disconnected when the source has ended let toDisconnect = []; // audio nodes that will be disconnected when the source has ended
const onended = () => { const onended = () => {
@ -434,9 +460,9 @@ export const superdough = async (value, deadline, hapDuration) => {
} }
// last gain // last gain
const post = gainNode(postgain); const post = new GainNode(ac, { gain: postgain });
chain.push(post); chain.push(post);
post.connect(getDestination()); connectToDestination(post, channels);
// delay // delay
let delaySend; let delaySend;

View File

@ -22,7 +22,7 @@ const fm = (osc, harmonicityRatio, modulationIndex, wave = 'sine') => {
}; };
const waveforms = ['sine', 'square', 'triangle', 'sawtooth']; const waveforms = ['sine', 'square', 'triangle', 'sawtooth'];
const noises = ['pink', 'white', 'brown']; const noises = ['pink', 'white', 'brown', 'crackle'];
export function registerSynthSounds() { export function registerSynthSounds() {
[...waveforms, ...noises].forEach((s) => { [...waveforms, ...noises].forEach((s) => {
@ -36,7 +36,8 @@ export function registerSynthSounds() {
if (waveforms.includes(s)) { if (waveforms.includes(s)) {
sound = getOscillator(s, t, value); sound = getOscillator(s, t, value);
} else { } else {
sound = getNoiseOscillator(s, t); let { density } = value;
sound = getNoiseOscillator(s, t, density);
} }
let { node: o, stop, triggerRelease } = sound; let { node: o, stop, triggerRelease } = sound;

View File

@ -1038,6 +1038,31 @@ exports[`runs examples > example "ceil" example index 0 1`] = `
] ]
`; `;
exports[`runs examples > example "channels" example index 0 1`] = `
[
"[ 0/1 → 1/5 | note:e channels:[3 4] ]",
"[ 1/5 → 2/5 | note:a channels:[3 4] ]",
"[ 2/5 → 3/5 | note:d channels:[3 4] ]",
"[ 3/5 → 4/5 | note:b channels:[3 4] ]",
"[ 4/5 → 1/1 | note:g channels:[3 4] ]",
"[ 1/1 → 6/5 | note:e channels:[3 4] ]",
"[ 6/5 → 7/5 | note:a channels:[3 4] ]",
"[ 7/5 → 8/5 | note:d channels:[3 4] ]",
"[ 8/5 → 9/5 | note:b channels:[3 4] ]",
"[ 9/5 → 2/1 | note:g channels:[3 4] ]",
"[ 2/1 → 11/5 | note:e channels:[3 4] ]",
"[ 11/5 → 12/5 | note:a channels:[3 4] ]",
"[ 12/5 → 13/5 | note:d channels:[3 4] ]",
"[ 13/5 → 14/5 | note:b channels:[3 4] ]",
"[ 14/5 → 3/1 | note:g channels:[3 4] ]",
"[ 3/1 → 16/5 | note:e channels:[3 4] ]",
"[ 16/5 → 17/5 | note:a channels:[3 4] ]",
"[ 17/5 → 18/5 | note:d channels:[3 4] ]",
"[ 18/5 → 19/5 | note:b channels:[3 4] ]",
"[ 19/5 → 4/1 | note:g channels:[3 4] ]",
]
`;
exports[`runs examples > example "chooseCycles" example index 0 1`] = ` exports[`runs examples > example "chooseCycles" example index 0 1`] = `
[ [
"[ 0/1 → 1/4 | s:bd ]", "[ 0/1 → 1/4 | s:bd ]",
@ -3267,22 +3292,22 @@ exports[`runs examples > example "outside" example index 0 1`] = `
exports[`runs examples > example "palindrome" example index 0 1`] = ` exports[`runs examples > example "palindrome" example index 0 1`] = `
[ [
"[ 0/1 → 1/4 | note:g ]", "[ 0/1 → 1/4 | note:c ]",
"[ 1/4 → 1/2 | note:e ]", "[ 1/4 → 1/2 | note:d ]",
"[ 1/2 → 3/4 | note:d ]", "[ 1/2 → 3/4 | note:e ]",
"[ 3/4 → 1/1 | note:c ]", "[ 3/4 → 1/1 | note:g ]",
"[ 1/1 → 5/4 | note:c ]", "[ 1/1 → 5/4 | note:g ]",
"[ 5/4 → 3/2 | note:d ]", "[ 5/4 → 3/2 | note:e ]",
"[ 3/2 → 7/4 | note:e ]", "[ 3/2 → 7/4 | note:d ]",
"[ 7/4 → 2/1 | note:g ]", "[ 7/4 → 2/1 | note:c ]",
"[ 2/1 → 9/4 | note:g ]", "[ 2/1 → 9/4 | note:c ]",
"[ 9/4 → 5/2 | note:e ]", "[ 9/4 → 5/2 | note:d ]",
"[ 5/2 → 11/4 | note:d ]", "[ 5/2 → 11/4 | note:e ]",
"[ 11/4 → 3/1 | note:c ]", "[ 11/4 → 3/1 | note:g ]",
"[ 3/1 → 13/4 | note:c ]", "[ 3/1 → 13/4 | note:g ]",
"[ 13/4 → 7/2 | note:d ]", "[ 13/4 → 7/2 | note:e ]",
"[ 7/2 → 15/4 | note:e ]", "[ 7/2 → 15/4 | note:d ]",
"[ 15/4 → 4/1 | note:g ]", "[ 15/4 → 4/1 | note:c ]",
] ]
`; `;

View File

@ -8372,16 +8372,16 @@ exports[`renders tunes > tune: hyperpop 1`] = `
exports[`renders tunes > tune: juxUndTollerei 1`] = ` exports[`renders tunes > tune: juxUndTollerei 1`] = `
[ [
"[ 0/1 → 1/4 | note:bb3 s:sawtooth pan:0 cutoff:1188.2154262966046 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]", "[ 0/1 → 1/4 | note:c3 s:sawtooth pan:0 cutoff:1188.2154262966046 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
"[ 0/1 → 1/4 | note:c3 s:sawtooth pan:1 cutoff:1188.2154262966046 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]", "[ 0/1 → 1/4 | note:bb3 s:sawtooth pan:1 cutoff:1188.2154262966046 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
"[ 1/4 → 1/2 | note:g3 s:sawtooth pan:0 cutoff:1361.2562095290161 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]", "[ 1/4 → 1/2 | note:eb3 s:sawtooth pan:0 cutoff:1361.2562095290161 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
"[ 1/4 → 1/2 | note:eb3 s:sawtooth pan:1 cutoff:1361.2562095290161 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]", "[ 1/4 → 1/2 | note:g3 s:sawtooth pan:1 cutoff:1361.2562095290161 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
"[ 1/2 → 3/4 | note:eb3 s:sawtooth pan:0 cutoff:1524.257063143398 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]", "[ 1/2 → 3/4 | note:g3 s:sawtooth pan:0 cutoff:1524.257063143398 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
"[ 1/2 → 3/4 | note:g3 s:sawtooth pan:1 cutoff:1524.257063143398 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]", "[ 1/2 → 3/4 | note:eb3 s:sawtooth pan:1 cutoff:1524.257063143398 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
"[ (101/200 → 1/1) ⇝ 201/200 | note:65 s:triangle pan:0 cutoff:1601.4815730092653 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]", "[ (101/200 → 1/1) ⇝ 201/200 | note:55 s:triangle pan:0 cutoff:1601.4815730092653 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
"[ (101/200 → 1/1) ⇝ 201/200 | note:55 s:triangle pan:1 cutoff:1601.4815730092653 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]", "[ (101/200 → 1/1) ⇝ 201/200 | note:65 s:triangle pan:1 cutoff:1601.4815730092653 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
"[ 3/4 → 1/1 | note:c3 s:sawtooth pan:0 cutoff:1670.953955747281 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]", "[ 3/4 → 1/1 | note:bb3 s:sawtooth pan:0 cutoff:1670.953955747281 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
"[ 3/4 → 1/1 | note:bb3 s:sawtooth pan:1 cutoff:1670.953955747281 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]", "[ 3/4 → 1/1 | note:c3 s:sawtooth pan:1 cutoff:1670.953955747281 lpattack:0.2 lpenv:-2 decay:0.05 sustain:0 room:0.6 delay:0.5 delaytime:0.1 delayfeedback:0.4 ]",
] ]
`; `;

View File

@ -1,17 +1,17 @@
{ {
"/home/felix/projects/strudel/packages/core/fraction.mjs": [ "/packages/core/fraction.mjs": [
"gcd" "gcd"
], ],
"/home/felix/projects/strudel/packages/core/timespan.mjs": [ "/packages/core/timespan.mjs": [
"TimeSpan" "TimeSpan"
], ],
"/home/felix/projects/strudel/packages/core/hap.mjs": [ "/packages/core/hap.mjs": [
"Hap" "Hap"
], ],
"/home/felix/projects/strudel/packages/core/state.mjs": [ "/packages/core/state.mjs": [
"State" "State"
], ],
"/home/felix/projects/strudel/packages/core/util.mjs": [ "/packages/core/util.mjs": [
"isNoteWithOctave", "isNoteWithOctave",
"isNote", "isNote",
"tokenizeNote", "tokenizeNote",
@ -34,23 +34,31 @@
"mapArgs", "mapArgs",
"numeralArgs", "numeralArgs",
"parseFractional", "parseFractional",
"fractionalArgs" "fractionalArgs",
"splitAt",
"zipWith",
"clamp",
"sol2note"
], ],
"/home/felix/projects/strudel/packages/core/value.mjs": [ "/packages/core/value.mjs": [
"unionWithObj", "unionWithObj",
"valued", "valued",
"Value", "Value",
"map" "map"
], ],
"/home/felix/projects/strudel/packages/core/drawLine.mjs": [], "/packages/core/drawLine.mjs": [],
"/home/felix/projects/strudel/packages/core/logger.mjs": [ "/packages/core/logger.mjs": [
"logKey", "logKey",
"logger" "logger"
], ],
"/home/felix/projects/strudel/packages/core/pattern.mjs": [ "/packages/core/pattern.mjs": [
"setStringParser", "setStringParser",
"polyrhythm",
"pr",
"pm",
"isPattern", "isPattern",
"reify", "reify",
"fastcat",
"set", "set",
"keep", "keep",
"keepif", "keepif",
@ -74,22 +82,33 @@
"func", "func",
"compressSpan", "compressSpan",
"compressspan", "compressspan",
"fastgap",
"focusSpan", "focusSpan",
"focusspan", "focusspan",
"density",
"sparsity",
"zoomArc", "zoomArc",
"zoomarc", "zoomarc",
"inv",
"juxby",
"echowith",
"stutWith",
"stutwith",
"iterback",
"chunkback",
"bypass", "bypass",
"duration", "duration",
"color",
"colour", "colour",
"striate" "loopat",
"loopatcps"
], ],
"/home/felix/projects/strudel/packages/core/controls.mjs": [], "/packages/core/controls.mjs": [],
"/home/felix/projects/strudel/packages/core/euclid.mjs": [ "/packages/core/euclid.mjs": [
"bjork",
"euclidrot", "euclidrot",
"euclidLegatoRot" "euclidLegatoRot"
], ],
"/home/felix/projects/strudel/packages/core/signal.mjs": [ "/packages/core/signal.mjs": [
"steady", "steady",
"signal", "signal",
"isaw", "isaw",
@ -112,34 +131,37 @@
"degradeByWith", "degradeByWith",
"undegrade" "undegrade"
], ],
"/home/felix/projects/strudel/packages/core/speak.mjs": [ "/packages/core/speak.mjs": [
"speak" "speak"
], ],
"/home/felix/projects/strudel/packages/core/evaluate.mjs": [ "/packages/core/evaluate.mjs": [
"evalScope", "evalScope",
"evaluate" "evaluate"
], ],
"/home/felix/projects/strudel/packages/core/zyklus.mjs": [], "/packages/core/zyklus.mjs": [],
"/home/felix/projects/strudel/packages/core/cyclist.mjs": [ "/packages/core/cyclist.mjs": [
"Cyclist" "Cyclist"
], ],
"/home/felix/projects/strudel/packages/core/time.mjs": [ "/packages/core/time.mjs": [
"getTime", "getTime",
"setTime" "setTime"
], ],
"/home/felix/projects/strudel/packages/core/repl.mjs": [ "/packages/core/repl.mjs": [
"repl" "repl",
"getTrigger"
], ],
"/home/felix/projects/strudel/packages/core/draw.mjs": [ "/packages/core/draw.mjs": [
"getDrawContext", "getDrawContext",
"cleanupDraw" "cleanupDraw",
"Framer",
"Drawer"
], ],
"/home/felix/projects/strudel/packages/core/animate.mjs": [ "/packages/core/animate.mjs": [
"x", "x",
"y", "y",
"w", "w",
"h", "h",
"a", "angle",
"r", "r",
"fill", "fill",
"smear", "smear",
@ -147,85 +169,121 @@
"moveXY", "moveXY",
"zoomIn" "zoomIn"
], ],
"/home/felix/projects/strudel/packages/core/pianoroll.mjs": [ "/packages/core/pianoroll.mjs": [
"pianoroll" "getDrawOptions",
"drawPianoroll"
], ],
"/home/felix/projects/strudel/packages/core/ui.mjs": [ "/packages/core/spiral.mjs": [],
"/packages/core/ui.mjs": [
"backgroundImage", "backgroundImage",
"cleanupUi" "cleanupUi"
], ],
"/home/felix/projects/strudel/packages/core/gist.js": [], "/packages/core/gist.js": [],
"/home/felix/projects/strudel/packages/core/index.mjs": [], "/packages/core/index.mjs": [],
"/home/felix/projects/strudel/packages/midi/midi.mjs": [ "/packages/csound/index.mjs": [
"loadCSound",
"loadcsound",
"loadCsound",
"csound",
"loadOrc"
],
"/packages/desktopbridge/utils.mjs": [
"Invoke",
"isTauri"
],
"/packages/desktopbridge/midibridge.mjs": [],
"/packages/desktopbridge/oscbridge.mjs": [],
"/packages/desktopbridge/index.mjs": [],
"/packages/midi/midi.mjs": [
"WebMidi", "WebMidi",
"enableWebMidi" "enableWebMidi",
"midin"
], ],
"/home/felix/projects/strudel/packages/midi/index.mjs": [], "/packages/midi/index.mjs": [],
"/home/felix/projects/strudel/packages/mini/krill-parser.js": [], "/packages/mini/krill-parser.js": [],
"/home/felix/projects/strudel/packages/mini/mini.mjs": [ "/packages/mini/mini.mjs": [
"patternifyAST", "patternifyAST",
"getLeafLocation",
"mini2ast",
"getLeaves",
"getLeafLocations",
"mini", "mini",
"m",
"h", "h",
"minify" "minify",
"miniAllStrings"
], ],
"/home/felix/projects/strudel/packages/mini/index.mjs": [], "/packages/mini/index.mjs": [],
"/home/felix/projects/strudel/packages/soundfonts/fontloader.mjs": [ "/packages/soundfonts/gm.mjs": [],
"/packages/soundfonts/fontloader.mjs": [
"getFontBufferSource", "getFontBufferSource",
"getFontPitch" "getFontPitch",
"registerSoundfonts"
], ],
"/home/felix/projects/strudel/packages/soundfonts/list.mjs": [ "/packages/soundfonts/list.mjs": [
"instruments", "instruments",
"drums", "drums",
"instrumentNames" "instrumentNames"
], ],
"/home/felix/projects/strudel/packages/soundfonts/sfumato.mjs": [ "/packages/soundfonts/sfumato.mjs": [
"loadSoundfont" "loadSoundfont"
], ],
"/home/felix/projects/strudel/packages/soundfonts/index.mjs": [], "/packages/soundfonts/index.mjs": [],
"/home/felix/projects/strudel/packages/tonal/tonal.mjs": [], "/packages/tonal/tonal.mjs": [],
"/home/felix/projects/strudel/packages/tonal/voicings.mjs": [ "/packages/tonal/tonleiter.mjs": [
"voicingRegistry", "pc2chroma",
"setVoicingRange" "rotateChroma",
"chroma2pc",
"tokenizeChord",
"note2pc",
"note2oct",
"note2chroma",
"midi2chroma",
"pitch2chroma",
"step2semitones",
"x2midi",
"scaleStep",
"renderVoicing",
"accidentalOffset",
"Step",
"Note"
], ],
"/home/felix/projects/strudel/packages/tonal/index.mjs": [], "/packages/tonal/ireal.mjs": [
"/home/felix/projects/strudel/packages/transpiler/transpiler.mjs": [ "simple",
"complex"
],
"/packages/tonal/voicings.mjs": [
"voicingRegistry",
"setVoicingRange",
"registerVoicings",
"voicingAlias"
],
"/packages/tonal/index.mjs": [],
"/packages/transpiler/transpiler.mjs": [
"transpiler" "transpiler"
], ],
"/home/felix/projects/strudel/packages/transpiler/index.mjs": [ "/packages/transpiler/index.mjs": [
"evaluate" "evaluate"
], ],
"/home/felix/projects/strudel/packages/webaudio/feedbackdelay.mjs": [], "/packages/webaudio/webaudio.mjs": [
"/home/felix/projects/strudel/packages/webaudio/reverb.mjs": [], "webaudioOutputTrigger",
"/home/felix/projects/strudel/packages/webaudio/sampler.mjs": [
"getCachedBuffer",
"getSampleBufferSource",
"loadBuffer",
"reverseBuffer",
"getLoadedBuffer",
"resetLoadedSamples",
"getLoadedSamples"
],
"/home/felix/projects/strudel/packages/webaudio/vowel.mjs": [
"vowelFormant"
],
"/home/felix/projects/strudel/packages/webaudio/webaudio.mjs": [
"getAudioContext",
"panic",
"initAudio",
"initAudioOnFirstClick",
"webaudioOutput", "webaudioOutput",
"webaudioOutputTrigger" "webaudioScheduler"
], ],
"/home/felix/projects/strudel/packages/webaudio/index.mjs": [], "/packages/webaudio/scope.mjs": [
"/home/felix/projects/strudel/packages/xen/xen.mjs": [ "drawTimeScope",
"drawFrequencyScope"
],
"/packages/webaudio/index.mjs": [],
"/packages/xen/xen.mjs": [
"edo", "edo",
"xen", "xen",
"tuning" "tuning"
], ],
"/home/felix/projects/strudel/packages/xen/tunejs.js": [], "/packages/xen/tunejs.js": [],
"/home/felix/projects/strudel/packages/xen/tune.mjs": [ "/packages/xen/tune.mjs": [
"tune" "tune"
], ],
"/home/felix/projects/strudel/packages/xen/index.mjs": [], "/packages/xen/index.mjs": [],
"/home/felix/projects/strudel/index.mjs": [] "/index.mjs": []
} }

View File

@ -13,24 +13,39 @@ import AstroPWA from '@vite-pwa/astro';
const site = `https://strudel.cc/`; // root url without a path const site = `https://strudel.cc/`; // root url without a path
const base = '/'; // base path of the strudel site const base = '/'; // base path of the strudel site
const baseNoTrailing = base.endsWith('/') ? base.slice(0, -1) : base;
// this rehype plugin converts relative anchor links to absolute ones // this rehype plugin fixes relative links
// it wokrs by prepending the absolute page path to anchor links // it works by prepending the base + page path to anchor links
// example: #gain -> /learn/effects/#gain // and by prepending the base path to other relative links starting with /
// this is necessary when using a base href like <base href={base} /> // this is necessary when using a base href like <base href={base} />
// in this setup, relative anchor links will always link to base, instead of the current page // examples with base as "mybase":
function absoluteAnchors() { // #gain -> /mybase/learn/effects/#gain
// /some/page -> /mybase/some/page
function relativeURLFix() {
return (tree, file) => { return (tree, file) => {
const chunks = file.history[0].split('/src/pages/'); // file.history[0] is the file path const chunks = file.history[0].split('/src/pages/'); // file.history[0] is the file path
const path = chunks[chunks.length - 1].slice(0, -4); // only path inside src/pages, without .mdx const path = chunks[chunks.length - 1].slice(0, -4); // only path inside src/pages, without .mdx
return rehypeUrls((url) => { return rehypeUrls((url) => {
if (!url.href.startsWith('#')) { let newHref = baseNoTrailing;
if (url.href.startsWith('#')) {
// special case: a relative anchor link to the current page
newHref += `/${path}/${url.href}`;
} else if (url.href.startsWith('/')) {
// any other relative url starting with /
newHref += url.pathname;
if (url.pathname.indexOf('.') == -1) {
// append trailing slash to resource only if there is no file extension
newHref += url.pathname.endsWith('/') ? '' : '/';
}
newHref += url.search || '';
newHref += url.hash || '';
} else {
// leave this URL alone
return; return;
} }
const baseWithSlash = base.endsWith('/') ? base : base + '/'; // console.log(url.href + ' -> ', newHref);
const absoluteUrl = baseWithSlash + path + url.href; return newHref;
// console.log(url.href + ' -> ', absoluteUrl);
return absoluteUrl;
})(tree); })(tree);
}; };
} }
@ -40,7 +55,7 @@ const options = {
remarkToc, remarkToc,
// E.g. `remark-frontmatter` // E.g. `remark-frontmatter`
], ],
rehypePlugins: [rehypeSlug, [rehypeAutolinkHeadings, { behavior: 'append' }], absoluteAnchors], rehypePlugins: [rehypeSlug, [rehypeAutolinkHeadings, { behavior: 'append' }], relativeURLFix],
}; };
// https://astro.build/config // https://astro.build/config

View File

@ -3,7 +3,7 @@ import { pwaInfo } from 'virtual:pwa-info';
import '../styles/index.css'; import '../styles/index.css';
const { BASE_URL } = import.meta.env; const { BASE_URL } = import.meta.env;
const base = BASE_URL; const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL;
--- ---
<!-- Global Metadata --> <!-- Global Metadata -->
@ -11,20 +11,20 @@ const base = BASE_URL;
<meta name="viewport" content="width=device-width" /> <meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} /> <meta name="generator" content={Astro.generator} />
<link rel="icon" type="image/svg+xml" href="favicon.ico" /> <link rel="icon" type="image/svg+xml" href={`${baseNoTrailing}/favicon.ico`} />
<meta <meta
name="description" name="description"
content="Strudel is a music live coding environment for the browser, porting the TidalCycles pattern language to JavaScript." content="Strudel is a music live coding environment for the browser, porting the TidalCycles pattern language to JavaScript."
/> />
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href={`${baseNoTrailing}/favicon.ico`} />
<link rel="apple-touch-icon" href="/icons/apple-icon-180.png" sizes="180x180" /> <link rel="apple-touch-icon" href={`${baseNoTrailing}/icons/apple-icon-180.png`} sizes="180x180" />
<meta name="theme-color" content="#222222" /> <meta name="theme-color" content="#222222" />
<base href={base} /> <base href={BASE_URL} />
<!-- Scrollable a11y code helper --> <!-- Scrollable a11y code helper -->
<script src="./make-scrollable-code-focusable.js" is:inline></script> <script src{`${baseNoTrailing}/make-scrollable-code-focusable.js`} is:inline></script>
<script src="/src/pwa.ts"></script> <script src="/src/pwa.ts"></script>
<!-- this does not work for some reason: --> <!-- this does not work for some reason: -->

View File

@ -16,6 +16,9 @@ const { currentPage } = Astro.props as Props;
// const lang = getLanguageFromURL(currentPage); // const lang = getLanguageFromURL(currentPage);
const langCode = 'en'; // getLanguageFromURL(currentPage); const langCode = 'en'; // getLanguageFromURL(currentPage);
const sidebar = SIDEBAR[langCode]; const sidebar = SIDEBAR[langCode];
const { BASE_URL } = import.meta.env;
const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL;
--- ---
<nav <nav
@ -23,7 +26,7 @@ const sidebar = SIDEBAR[langCode];
title="Top Navigation" title="Top Navigation"
> >
<div class="flex overflow-visible items-center grow" style="overflow:visible"> <div class="flex overflow-visible items-center grow" style="overflow:visible">
<a href="/" class="flex items-center text-2xl space-x-2"> <a href={`${baseNoTrailing}/`} class="flex items-center text-2xl space-x-2">
<h1 class="font-bold flex space-x-2 items-baseline text-xl"> <h1 class="font-bold flex space-x-2 items-baseline text-xl">
<span>🌀</span> <span>🌀</span>
<div class="flex space-x-1 items-baseline"> <div class="flex space-x-1 items-baseline">
@ -35,9 +38,9 @@ const sidebar = SIDEBAR[langCode];
</div> </div>
{/* KNOWN_LANGUAGE_CODES.length > 1 && <LanguageSelect lang={lang} client:idle /> */} {/* KNOWN_LANGUAGE_CODES.length > 1 && <LanguageSelect lang={lang} client:idle /> */}
<div class="search-item h-10"> <div class="search-item h-10">
<!-- <Search client:idle /> --> <Search client:idle />
</div> </div>
<a href="./" class="hidden md:flex cursor-pointer items-center space-x-1" <a href={`${baseNoTrailing}/`} class="hidden md:flex cursor-pointer items-center space-x-1"
><CommandLineIcon className="w-5 h-5" /><span>go to REPL</span> ><CommandLineIcon className="w-5 h-5" /><span>go to REPL</span>
</a> </a>
<div class="md:hidden"> <div class="md:hidden">
@ -48,8 +51,8 @@ const sidebar = SIDEBAR[langCode];
<style> <style>
/** Style Algolia */ /** Style Algolia */
:root { :root {
--docsearch-primary-color: var(--theme-accent); --docsearch-primary-color: var(--lineHighlight);
--docsearch-logo-color: var(--theme-text); --docsearch-logo-color: var(--foreground);
} }
.search-item { .search-item {

View File

@ -1,7 +1,28 @@
/** Style Algolia */ /** Style Algolia */
:root { :root {
--docsearch-primary-color: var(--theme-accent); --docsearch-primary-color: var(--lineHighlight);
--docsearch-logo-color: var(--theme-text); --docsearch-logo-color: var(--foreground);
--docsearch-container-background: rgba(110, 110, 110, 0.8);
--docsearch-modal-shadow: none;
--docsearch-text-color: var(--foreground);
--docsearch-highlight-color: var(--foreground);
--docsearch-searchbox-background: var(--lineBackground);
--docsearch-searchbox-focus-background: var(--lineBackground);
--docsearch-searchbox-shadow: inset 0 0 0 2px var(--lineHighlight);
--docsearch-hit-background: var(--background);
--docsearch-hit-active-color: var(--background);
--docsearch-hit-color: var(--foreground);
--docsearch-hit-shadow: 0 1px 3px 0 var(--foreground);
--docsearch-footer-shadow: none;
--docsearch-footer-background: var(--gutterBackground);
--docsearch-modal-background: var(--background);
--docsearch-muted-color: color-mix(in srgb, var(--foreground), #fff 30%);
--docsearch-key-gradient: var(--foreground);
--docsearch-key-shadow: inset 0 -2px 0 0 var(--gutterForeground), inset 0 0 1px 1px var(--foreground),
0 1px 2px 1px var(--gutterBackground);
}
.dark {
--docsearch-muted-color: color-mix(in srgb, var(--foreground), #000 30%);
} }
.search-input { .search-input {
flex-grow: 1; flex-grow: 1;
@ -14,9 +35,9 @@
font-size: 1rem; font-size: 1rem;
font-family: inherit; font-family: inherit;
line-height: inherit; line-height: inherit;
background-color: var(--theme-divider); background-color: var(--lineBackground);
border-color: var(--theme-divider); border-color: var(--lineBackground);
color: var(--theme-text-light); color: var(--foreground);
border-style: solid; border-style: solid;
border-width: 1px; border-width: 1px;
border-radius: 0.25rem; border-radius: 0.25rem;
@ -29,15 +50,15 @@
} }
.search-input:hover, .search-input:hover,
.search-input:focus { .search-input:focus {
color: var(--theme-text); color: var(--foreground);
border-color: var(--theme-text-light); border-color: var(--foreground);
} }
.search-input:hover::placeholder, .search-input:hover::placeholder,
.search-input:focus::placeholder { .search-input:focus::placeholder {
color: var(--theme-text-light); color: var(--foreground);
} }
.search-input::placeholder { .search-input::placeholder {
color: var(--theme-text-light); color: var(--foreground);
} }
.search-hint { .search-hint {
position: absolute; position: absolute;
@ -48,11 +69,13 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
letter-spacing: 0.125em; letter-spacing: 0.125em;
font-size: 13px; font-size: 12px;
font-family: var(--font-mono); font-family: var(--font-mono);
pointer-events: none; pointer-events: none;
border-color: var(--theme-text-lighter); border-color: transparent;
color: var(--theme-text-light); color: var(--background);
background-color: var(--foreground);
box-shadow: var(--docsearch-key-shadow);
border-style: solid; border-style: solid;
border-width: 1px; border-width: 1px;
border-radius: 0.25rem; border-radius: 0.25rem;
@ -64,12 +87,40 @@
display: flex; display: flex;
} }
} }
.search-button {
background-color: var(--lineBackground) !important;
color: var(--foreground);
}
/* ------------------------------------------------------------ *\ /* ------------------------------------------------------------ *\
DocSearch (Algolia) DocSearch (Algolia)
\* ------------------------------------------------------------ */ \* ------------------------------------------------------------ */
.DocSearch-Form {
padding: 3px var(--docsearch-spacing);
}
.DocSearch-Modal .DocSearch-Hit a { .DocSearch-Modal .DocSearch-Hit a {
box-shadow: none; box-shadow: none;
border: 1px solid var(--theme-accent); border: none;
}
#docsearch-input.DocSearch-Input {
background-color: var(--lineBackground);
border-top: 3px solid var(--docsearch-searchbox-shadow);
border-bottom: 3px solid var(--docsearch-searchbox-shadow);
}
.DocSearch-Commands-Key {
color: var(--background);
border-radius: 0.25rem;
}
.DocSearch-SearchBar {
padding-bottom: 1px;
}
#docsearch-input.DocSearch-Input:focus {
box-shadow: none;
}
.DocSearch-Footer {
color: var(--docsearch-muted-color) !important;
}
.DocSearch-Logo svg .cls-1,
.DocSearch-Logo svg .cls-2 {
fill: var(--docsearch-logo-color);
} }

View File

@ -6,6 +6,8 @@ import './Search.css';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import * as docSearchReact from '@docsearch/react'; import * as docSearchReact from '@docsearch/react';
const { BASE_URL } = import.meta.env;
const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL;
/** FIXME: This is still kinda nasty, but DocSearch is not ESM ready. */ /** FIXME: This is still kinda nasty, but DocSearch is not ESM ready. */
const DocSearchModal = docSearchReact.DocSearchModal || (docSearchReact as any).default.DocSearchModal; const DocSearchModal = docSearchReact.DocSearchModal || (docSearchReact as any).default.DocSearchModal;
@ -43,7 +45,12 @@ export default function Search() {
return ( return (
<> <>
<button type="button" ref={searchButtonRef} onClick={onOpen} className="rounded-md bg-slate-900 w-full px-2"> <button
type="button"
ref={searchButtonRef}
onClick={onOpen}
className="rounded-md bg-slate-900 w-full px-2 search-button"
>
<svg width="24" height="24" fill="none"> <svg width="24" height="24" fill="none">
<path <path
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
@ -74,6 +81,9 @@ export default function Search() {
indexName={ALGOLIA.indexName} indexName={ALGOLIA.indexName}
appId={ALGOLIA.appId} appId={ALGOLIA.appId}
apiKey={ALGOLIA.apiKey} apiKey={ALGOLIA.apiKey}
getMissingResultsUrl={({ query }) => {
return `https://github.com/tidalcycles/strudel/issues/new?title=Missing doc for ${query}`;
}}
transformItems={(items) => { transformItems={(items) => {
return items.map((item) => { return items.map((item) => {
// We transform the absolute URL into a relative URL to // We transform the absolute URL into a relative URL to
@ -81,9 +91,13 @@ export default function Search() {
const a = document.createElement('a'); const a = document.createElement('a');
a.href = item.url; a.href = item.url;
const hash = a.hash === '#overview' ? '' : a.hash; const hash = a.hash === '#overview' ? '' : a.hash;
let pathname = a.pathname;
pathname = pathname.startsWith('/') ? pathname.slice(1) : pathname;
pathname = pathname.endsWith('/') ? pathname.slice(0, -1) : pathname;
const url = `${baseNoTrailing}/${pathname}/${hash}`;
return { return {
...item, ...item,
url: `${a.pathname}${hash}`, url,
}; };
}); });
}} }}

View File

@ -9,6 +9,7 @@ type Props = {
const { currentPage } = Astro.props as Props; const { currentPage } = Astro.props as Props;
const { BASE_URL } = import.meta.env; const { BASE_URL } = import.meta.env;
let currentPageMatch = currentPage.slice(BASE_URL.length, currentPage.endsWith('/') ? -1 : undefined); let currentPageMatch = currentPage.slice(BASE_URL.length, currentPage.endsWith('/') ? -1 : undefined);
const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL;
const langCode = getLanguageFromURL(currentPage) || 'en'; const langCode = getLanguageFromURL(currentPage) || 'en';
const sidebar = SIDEBAR[langCode]; const sidebar = SIDEBAR[langCode];
@ -23,7 +24,7 @@ const sidebar = SIDEBAR[langCode];
<h2>{header}</h2> <h2>{header}</h2>
<ul> <ul>
{children.map((child) => { {children.map((child) => {
const url = Astro.site?.pathname + child.link; const url = `${baseNoTrailing}/${child.link}${child.link.endsWith('/') ? '' : '/'}`;
return ( return (
<li class=""> <li class="">
<a <a

View File

@ -73,7 +73,7 @@ const TableOfContents: FC<{ headings: MarkdownHeading[]; currentPage: string }>
.map((heading, i) => ( .map((heading, i) => (
<li className={`w-full`} key={i}> <li className={`w-full`} key={i}>
<a <a
href={`${currentPage}#${heading.slug}`} href={`${currentPage}/#${heading.slug}`}
onClick={onLinkClick} onClick={onLinkClick}
className={`py-0.5 block cursor-pointer w-full border-l-4 border-lineHighlight hover:bg-lineHighlight ${ className={`py-0.5 block cursor-pointer w-full border-l-4 border-lineHighlight hover:bg-lineHighlight ${
['pl-4', 'pl-9', 'pl-12'][heading.depth - minDepth] ['pl-4', 'pl-9', 'pl-12'][heading.depth - minDepth]

View File

@ -2,19 +2,15 @@ import jsdoc from '../../../doc.json'; // doc.json is built with `npm run jsdoc-
const docs = jsdoc.docs.reduce((acc, obj) => Object.assign(acc, { [obj.longname]: obj }), {}); const docs = jsdoc.docs.reduce((acc, obj) => Object.assign(acc, { [obj.longname]: obj }), {});
import { MiniRepl } from './MiniRepl'; import { MiniRepl } from './MiniRepl';
const getTag = (title, item) => item.tags?.find((t) => t.title === title)?.text;
export function JsDoc({ name, h = 3, hideDescription, punchcard, canvasHeight }) { export function JsDoc({ name, h = 3, hideDescription, punchcard, canvasHeight }) {
const item = docs[name]; const item = docs[name];
if (!item) { if (!item) {
console.warn('Not found: ' + name); console.warn('Not found: ' + name);
return <div />; return <div />;
} }
const synonyms = getTag('synonyms', item)?.split(', ') || [];
const CustomHeading = `h${h}`; const CustomHeading = `h${h}`;
const description = const description =
item.description?.replaceAll(/\{@link ([a-zA-Z\.]+)?#?([a-zA-Z]*)\}/g, (_, a, b) => { item.description?.replaceAll(/\{@link ([a-zA-Z\.]+)?#?([a-zA-Z]*)\}/g, (_, a, b) => {
// console.log(_, 'a', a, 'b', b);
return `<a href="#${a.replaceAll('.', '').toLowerCase()}${b ? `-${b}` : ''}">${a}${b ? `#${b}` : ''}</a>`; return `<a href="#${a.replaceAll('.', '').toLowerCase()}${b ? `-${b}` : ''}">${a}${b ? `#${b}` : ''}</a>`;
}) || ''; }) || '';
return ( return (
@ -22,9 +18,9 @@ export function JsDoc({ name, h = 3, hideDescription, punchcard, canvasHeight })
{!!h && <CustomHeading>{item.longname}</CustomHeading>} {!!h && <CustomHeading>{item.longname}</CustomHeading>}
{!hideDescription && ( {!hideDescription && (
<> <>
{!!synonyms.length && ( {!!item.synonyms_text && (
<span> <span>
Synonyms: <code>{synonyms.join(', ')}</code> Synonyms: <code>{item.synonyms_text}</code>
</span> </span>
)} )}
<div dangerouslySetInnerHTML={{ __html: description }} /> <div dangerouslySetInnerHTML={{ __html: description }} />

View File

@ -7,7 +7,7 @@ import { MiniRepl } from '../../../docs/MiniRepl';
# Willkommen # Willkommen
<img src="/icons/strudel_icon.png" className="w-32 animate-pulse md:float-right ml-8" /> <div className="w-32 animate-pulse md:float-right ml-8">![Strudel Icon](/icons/strudel_icon.png)</div>
Willkommen zum Strudel Workshop! Willkommen zum Strudel Workshop!
Du hast den richtigen Ort gefunden wenn du lernen möchtest wie man mit Code Musik macht. Du hast den richtigen Ort gefunden wenn du lernen möchtest wie man mit Code Musik macht.

View File

@ -3,6 +3,9 @@ import * as tunes from '../../../src/repl/tunes.mjs';
import HeadCommon from '../../components/HeadCommon.astro'; import HeadCommon from '../../components/HeadCommon.astro';
import { getMetadata } from '../metadata_parser'; import { getMetadata } from '../metadata_parser';
const { BASE_URL } = import.meta.env;
const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL;
--- ---
<head> <head>
@ -12,11 +15,11 @@ import { getMetadata } from '../metadata_parser';
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-2 p-2 select-none"> <div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-2 p-2 select-none">
{ {
Object.entries(tunes).map(([name, tune]) => ( Object.entries(tunes).map(([name, tune]) => (
<a class="rounded-md bg-slate-900 hover:bg-slate-700 cursor-pointer relative" href={`./#${btoa(tune)}`}> <a class="rounded-md bg-slate-900 hover:bg-slate-700 cursor-pointer relative" href={`${baseNoTrailing}/#${btoa(tune)}`}>
<div class="absolute w-full h-full flex justify-center items-center"> <div class="absolute w-full h-full flex justify-center items-center">
<span class="bg-slate-800 p-2 rounded-md text-white">{getMetadata(tune)['title'] || name}</span> <span class="bg-slate-800 p-2 rounded-md text-white">{getMetadata(tune)['title'] || name}</span>
</div> </div>
<img src={`./img/example-${name}.png`} /> <img src={`${baseNoTrailing}/img/example-${name}.png`} />
</a> </a>
)) ))
} }

View File

@ -4,7 +4,7 @@ import { evaluate } from '@strudel.cycles/transpiler';
import '../../../../test/runtime.mjs'; import '../../../../test/runtime.mjs';
import * as tunes from '../../repl/tunes.mjs'; import * as tunes from '../../repl/tunes.mjs';
export async function get({ params, request }) { export async function GET({ params, request }) {
const { name } = params; const { name } = params;
const tune = tunes[name]; const tune = tunes[name];
const { pattern } = await evaluate(tune); const { pattern } = await evaluate(tune);
@ -13,10 +13,7 @@ export async function get({ params, request }) {
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
pianoroll({ time: 4, haps, ctx, playhead: 1, fold: 1, background: 'transparent', playheadColor: 'transparent' }); pianoroll({ time: 4, haps, ctx, playhead: 1, fold: 1, background: 'transparent', playheadColor: 'transparent' });
const buffer = canvas.toBuffer('image/png'); const buffer = canvas.toBuffer('image/png');
return { return new Response(buffer);
body: buffer,
encoding: 'binary',
};
} }
export function getStaticPaths() { export function getStaticPaths() {
return Object.keys(tunes).map((name) => ({ return Object.keys(tunes).map((name) => ({

View File

@ -145,7 +145,7 @@ In both cases, p4 is derived from the value of `freq` or `note`.
## Limitations / Future Plans ## Limitations / Future Plans
Apart from the above listed p values, no other parameter can be patterned so far. Apart from the above listed p values, no other parameter can be patterned so far.
This also means that [audio effects](./learn/effects) will not work. This also means that [audio effects](/learn/effects/) will not work.
In the future, the integration could be improved by passing all patterned control parameters to the csound instrument. In the future, the integration could be improved by passing all patterned control parameters to the csound instrument.
This could work by a unique [channel](https://kunstmusik.github.io/icsc2022-csound-web/tutorial2-interacting-with-csound/#step-4---writing-continuous-data-channels) This could work by a unique [channel](https://kunstmusik.github.io/icsc2022-csound-web/tutorial2-interacting-with-csound/#step-4---writing-continuous-data-channels)
for each value. Channels could be read [like this](https://github.com/csound/csound/blob/master/Android/CsoundForAndroid/CsoundAndroidExamples/src/main/res/raw/multitouch_xy.csd). for each value. Channels could be read [like this](https://github.com/csound/csound/blob/master/Android/CsoundForAndroid/CsoundAndroidExamples/src/main/res/raw/multitouch_xy.csd).

View File

@ -41,7 +41,7 @@ This interactive tutorial will guide you through the basics of Strudel.
To see and hear what Strudel can do, visit the [Strudel REPL](https://strudel.cc/) and click the Shuffle icon in the top menu bar. To see and hear what Strudel can do, visit the [Strudel REPL](https://strudel.cc/) and click the Shuffle icon in the top menu bar.
You can get a feel for Strudel by browsing and editing these examples and clicking the Refresh icon to update. You can get a feel for Strudel by browsing and editing these examples and clicking the Refresh icon to update.
You can also browse through the examples [here](./examples). You can also browse through the examples [here](/examples).
Alternatively, you can get a taste of what Strudel can do by clicking play on this track: Alternatively, you can get a taste of what Strudel can do by clicking play on this track:

View File

@ -41,6 +41,8 @@ note("[a,c,e,<a4 ab4 g4 gb4>,b4]/4").s("sawtooth").vib(2)
`} `}
/> />
## H patterns
There is a special function `H` that allows you to use a pattern as an input to hydra: There is a special function `H` that allows you to use a pattern as an input to hydra:
<MiniRepl <MiniRepl
@ -48,8 +50,50 @@ There is a special function `H` that allows you to use a pattern as an input to
tune={`await initHydra() tune={`await initHydra()
let pattern = "3 4 5 [6 7]*2" let pattern = "3 4 5 [6 7]*2"
shape(H(pattern)).out(o0) shape(H(pattern)).out(o0)
n(pattern).scale("A:minor").piano().room(1) n(pattern).scale("A:minor").piano().room(1)
`}
/>
## detectAudio
To use hydra audio capture, call `initHydra` with `{detectAudio:true}` configuration param:
<MiniRepl
client:only="react"
tune={`await initHydra({detectAudio:true})
let pattern = "<3 4 5 [6 7]*2>"
shape(H(pattern)).repeat()
.scrollY(
()=> a.fft[0]*.25
)
.add(src(o0).color(.71 ).scrollX(.005),.95)
.out(o0)
n(pattern).scale("A:minor").piano().room(1)
`} `}
/> />
You might now be able to see this properly here: [open in REPL](/#YXdhaXQgaW5pdEh5ZHJhKCkKbGV0IHBhdHRlcm4gPSAiMyA0IDUgWzYgN10qMiIKc2hhcGUoSChwYXR0ZXJuKSkub3V0KG8wKQpuKHBhdHRlcm4pLnNjYWxlKCJBOm1pbm9yIikucGlhbm8oKS5yb29tKDEpIA%3D%3D) You might now be able to see this properly here: [open in REPL](/#YXdhaXQgaW5pdEh5ZHJhKCkKbGV0IHBhdHRlcm4gPSAiMyA0IDUgWzYgN10qMiIKc2hhcGUoSChwYXR0ZXJuKSkub3V0KG8wKQpuKHBhdHRlcm4pLnNjYWxlKCJBOm1pbm9yIikucGlhbm8oKS5yb29tKDEpIA%3D%3D)
Similar to `detectAudio`, all the [available hydra options](https://github.com/hydra-synth/hydra-synth#api) can be passed to `initHydra`.
## feedStrudel
Using the `feedStrudel` option, you can transform strudel visualizations with hydra:
<MiniRepl
client:only="react"
tune={`await initHydra({feedStrudel:1})
//
src(s0).kaleid(H("<4 5 6>"))
.diff(osc(1,0.5,5))
.modulateScale(osc(2,-0.25,1))
.out()
//
stack(
s("bd*2,[hh:0:<.5 1>]*4,~ rim").bank("RolandTR909").speed(.9),
note("[<g1!3 <bb1 <f1 d1>>>]*3").s("sawtooth")
.room(.75).sometimes(add(note(12))).clip(.3)
.lpa(.05).lpenv(-4).lpf(2000).lpq(8).ftype('24db')
).fft(4)
.scope({pos:0,smear:.95})`}
/>

View File

@ -49,7 +49,7 @@ A standalone app has its own desktop / homescreen icon and launches in a separat
without the browser ui. without the browser ui.
<figure> <figure>
<img src="./pwa/strudel-macos.png" alt="Strudel on MacOS" /> ![Strudel on MacOS](/pwa/strudel-macos.png)
<figcaption>Strudel on MacOS</figcaption> <figcaption>Strudel on MacOS</figcaption>
</figure> </figure>
@ -67,7 +67,7 @@ Without a chromium based browser, you can use [nativefier](https://github.com/na
2. run `npx nativefier strudel.cc` 2. run `npx nativefier strudel.cc`
<figure> <figure>
<img src="./pwa/strudel-linux.png" alt="Strudel on Linux" /> ![Strudel on Linux](/pwa/strudel-linux.png)
<figcaption>Strudel on Linux</figcaption> <figcaption>Strudel on Linux</figcaption>
</figure> </figure>

View File

@ -19,7 +19,7 @@ You can learn more about both of these approaches in the pages [Synths](/learn/s
# Combining notes and sounds # Combining notes and sounds
In both of the above cases, we are no longer directly controlling the `note`/`freq` of the sound heard via `s`, as we were in the [Notes](/strudel/notes) page. In both of the above cases, we are no longer directly controlling the `note`/`freq` of the sound heard via `s`, as we were in the [Notes](/workshop/first-notes/) page.
So how can we both control the sound and the pitch? We can _combine_ `note`/`freq` with `s` to change the sound of our pitches: So how can we both control the sound and the pitch? We can _combine_ `note`/`freq` with `s` to change the sound of our pitches:

View File

@ -96,13 +96,13 @@ If you find a tidal control that's not on the list, please tell!
## Sound ## Sound
Tidal is commonly paired with Superdirt / Supercollider for sound generation. Tidal is commonly paired with Superdirt / Supercollider for sound generation.
While Strudel also has a way of [communicating with Superdirt](./learn/input-output), While Strudel also has a way of [communicating with Superdirt](/learn/input-output/),
it aims to provide a standalone live coding environment that runs entirely in the browser. it aims to provide a standalone live coding environment that runs entirely in the browser.
### Audio Effects ### Audio Effects
Many of SuperDirt's effects have been reimplemented in Strudel, using the Web Audio API. Many of SuperDirt's effects have been reimplemented in Strudel, using the Web Audio API.
You can find a [list of available effects here](./learn/effects). You can find a [list of available effects here](/learn/effects/).
### Sampler ### Sampler

View File

@ -42,6 +42,10 @@ Some amount of pink noise can also be added to any oscillator by using the `nois
<MiniRepl client:idle tune={`note("c3").noise("<0.1 0.25 0.5>").scope()`} /> <MiniRepl client:idle tune={`note("c3").noise("<0.1 0.25 0.5>").scope()`} />
You can also use the `crackle` type to play some subtle noise crackles. You can control noise amount by using the `density` parameter:
<MiniRepl client:idle tune={`s("crackle*4").density("<0.01 0.04 0.2 0.5>".slow(4)).scope()`} />
### Additive Synthesis ### Additive Synthesis
To tame the harsh sound of the basic waveforms, we can set the `n` control to limit the overtones of the waveform: To tame the harsh sound of the basic waveforms, we can set the `n` control to limit the overtones of the waveform:

View File

@ -110,4 +110,4 @@ Some of these have equivalent operators in the Mini Notation:
<JsDoc client:idle name="ribbon" h={0} /> <JsDoc client:idle name="ribbon" h={0} />
Apart from modifying time, there are ways to [Control Parameters](functions/value-modifiers). Apart from modifying time, there are ways to [Control Parameters](/functions/value-modifiers/).

View File

@ -5,6 +5,9 @@ import { Content } from '../../../../my-patterns/README.md';
import HeadCommon from '../../components/HeadCommon.astro'; import HeadCommon from '../../components/HeadCommon.astro';
const myPatterns = await getMyPatterns(); const myPatterns = await getMyPatterns();
const { BASE_URL } = import.meta.env;
const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL;
--- ---
<head> <head>
@ -23,12 +26,12 @@ const myPatterns = await getMyPatterns();
Object.entries(myPatterns).map(([name, tune]) => ( Object.entries(myPatterns).map(([name, tune]) => (
<a <a
class="rounded-md bg-slate-900 hover:bg-slate-700 cursor-pointer relative" class="rounded-md bg-slate-900 hover:bg-slate-700 cursor-pointer relative"
href={`./#${btoa(tune as string)}`} href={`${baseNoTrailing}/#${btoa(tune as string)}`}
> >
<div class="absolute w-full h-full flex justify-center items-center"> <div class="absolute w-full h-full flex justify-center items-center">
<span class="bg-slate-800 p-2 rounded-md text-white">{name}</span> <span class="bg-slate-800 p-2 rounded-md text-white">{name}</span>
</div> </div>
<img src={`./swatch/${name}.png`} /> <img src={`${baseNoTrailing}/swatch/${name}.png`} />
</a> </a>
)) ))
} }

View File

@ -159,7 +159,7 @@ Try adding more sounds inside a bracket!
</Box> </Box>
Similar to the whole sequence, the content of a sub-sequence will be squished to the its own length. Similar to the whole sequence, the content of a sub-sequence will be squished to its own length.
**Multiplication: Speed things up** **Multiplication: Speed things up**

View File

@ -7,7 +7,7 @@ import { MiniRepl } from '../../docs/MiniRepl';
# Welcome # Welcome
<img src="/icons/strudel_icon.png" className="w-32 animate-pulse md:float-right ml-8" /> <div className="w-32 animate-pulse md:float-right ml-8">![Strudel Icon](/icons/strudel_icon.png)</div>
Welcome to the Strudel documentation pages! Welcome to the Strudel documentation pages!
You've come to the right place if you want to learn how to make music with code. You've come to the right place if you want to learn how to make music with code.

View File

@ -9,6 +9,8 @@ import React, { useContext } from 'react';
import { useSettings, setIsZen } from '../settings.mjs'; import { useSettings, setIsZen } from '../settings.mjs';
// import { ReplContext } from './Repl'; // import { ReplContext } from './Repl';
import './Repl.css'; import './Repl.css';
const { BASE_URL } = import.meta.env;
const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL;
export function Header({ context }) { export function Header({ context }) {
const { const {
@ -123,7 +125,7 @@ export function Header({ context }) {
{!isEmbedded && ( {!isEmbedded && (
<a <a
title="learn" title="learn"
href="./workshop/getting-started" href={`${baseNoTrailing}/workshop/getting-started/`}
className={cx('hover:opacity-50 flex items-center space-x-1', !isEmbedded ? 'p-2' : 'px-2')} className={cx('hover:opacity-50 flex items-center space-x-1', !isEmbedded ? 'p-2' : 'px-2')}
> >
<AcademicCapIcon className="w-6 h-6" /> <AcademicCapIcon className="w-6 h-6" />

View File

@ -37,6 +37,11 @@ export function Reference() {
{visibleFunctions.map((entry, i) => ( {visibleFunctions.map((entry, i) => (
<section key={i}> <section key={i}>
<h3 id={`doc-${i}`}>{entry.name}</h3> <h3 id={`doc-${i}`}>{entry.name}</h3>
{!!entry.synonyms_text && (
<p>
Synonyms: <code>{entry.synonyms_text}</code>
</p>
)}
{/* <small>{entry.meta.filename}</small> */} {/* <small>{entry.meta.filename}</small> */}
<p dangerouslySetInnerHTML={{ __html: entry.description }}></p> <p dangerouslySetInnerHTML={{ __html: entry.description }}></p>
<ul> <ul>

View File

@ -3,6 +3,9 @@ import { registerSynthSounds, registerZZFXSounds, samples } from '@strudel.cycle
import './piano.mjs'; import './piano.mjs';
import './files.mjs'; import './files.mjs';
const { BASE_URL } = import.meta.env;
const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL;
export async function prebake() { export async function prebake() {
// https://archive.org/details/SalamanderGrandPianoV3 // https://archive.org/details/SalamanderGrandPianoV3
// License: CC-by http://creativecommons.org/licenses/by/3.0/ Author: Alexander Holm // License: CC-by http://creativecommons.org/licenses/by/3.0/ Author: Alexander Holm
@ -14,16 +17,16 @@ export async function prebake() {
// => getting "window is not defined", as soon as "@strudel.cycles/soundfonts" is imported statically // => getting "window is not defined", as soon as "@strudel.cycles/soundfonts" is imported statically
// seems to be a problem with soundfont2 // seems to be a problem with soundfont2
import('@strudel.cycles/soundfonts').then(({ registerSoundfonts }) => registerSoundfonts()), import('@strudel.cycles/soundfonts').then(({ registerSoundfonts }) => registerSoundfonts()),
samples(`./piano.json`, `./piano/`, { prebake: true }), samples(`${baseNoTrailing}/piano.json`, `${baseNoTrailing}/piano/`, { prebake: true }),
// https://github.com/sgossner/VCSL/ // https://github.com/sgossner/VCSL/
// https://api.github.com/repositories/126427031/contents/ // https://api.github.com/repositories/126427031/contents/
// LICENSE: CC0 general-purpose // LICENSE: CC0 general-purpose
samples(`./vcsl.json`, 'github:sgossner/VCSL/master/', { prebake: true }), samples(`${baseNoTrailing}/vcsl.json`, 'github:sgossner/VCSL/master/', { prebake: true }),
samples(`./tidal-drum-machines.json`, 'github:ritchse/tidal-drum-machines/main/machines/', { samples(`${baseNoTrailing}/tidal-drum-machines.json`, 'github:ritchse/tidal-drum-machines/main/machines/', {
prebake: true, prebake: true,
tag: 'drum-machines', tag: 'drum-machines',
}), }),
samples(`./EmuSP12.json`, `./EmuSP12/`, { prebake: true, tag: 'drum-machines' }), samples(`${baseNoTrailing}/EmuSP12.json`, `${baseNoTrailing}/EmuSP12/`, { prebake: true, tag: 'drum-machines' }),
samples( samples(
{ {
casio: ['casio/high.wav', 'casio/low.wav', 'casio/noise.wav'], casio: ['casio/high.wav', 'casio/low.wav', 'casio/noise.wav'],