mirror of
https://github.com/eliasstepanik/strudel-docker.git
synced 2026-01-11 05:38:34 +00:00
whole docs now run new repl
- move MicroRepl code to MiniRepl - fix a ssr bug
This commit is contained in:
parent
837f8f318d
commit
975a198ee9
@ -1,172 +0,0 @@
|
||||
import { useState, useRef, useCallback, useMemo, useEffect } from 'react';
|
||||
import { Icon } from './Icon';
|
||||
import { silence, getPunchcardPainter, noteToMidi } from '@strudel.cycles/core';
|
||||
import { transpiler } from '@strudel.cycles/transpiler';
|
||||
import { getAudioContext, webaudioOutput } from '@strudel.cycles/webaudio';
|
||||
import { StrudelMirror } from '@strudel/codemirror';
|
||||
import { prebake } from '@strudel/repl';
|
||||
import Claviature from '@components/Claviature';
|
||||
|
||||
export function MicroRepl({
|
||||
code,
|
||||
hideHeader = false,
|
||||
canvasHeight = 100,
|
||||
onTrigger,
|
||||
punchcard,
|
||||
punchcardLabels = true,
|
||||
claviature,
|
||||
claviatureLabels,
|
||||
}) {
|
||||
const id = useMemo(() => s4(), []);
|
||||
const canvasId = useMemo(() => `canvas-${id}`, [id]);
|
||||
const shouldDraw = !!punchcard || !!claviature;
|
||||
const shouldShowCanvas = !!punchcard;
|
||||
const drawTime = punchcard ? [0, 4] : [0, 0];
|
||||
const [activeNotes, setActiveNotes] = useState([]);
|
||||
|
||||
const init = useCallback(({ code, shouldDraw }) => {
|
||||
const drawContext = shouldDraw ? document.querySelector('#' + canvasId)?.getContext('2d') : null;
|
||||
let onDraw;
|
||||
if (shouldDraw) {
|
||||
onDraw = (haps, time, frame, painters) => {
|
||||
painters.length && drawContext?.clearRect(0, 0, drawContext.canvas.width * 2, drawContext.canvas.height * 2);
|
||||
painters?.forEach((painter) => {
|
||||
// ctx time haps drawTime paintOptions
|
||||
painter(drawContext, time, haps, drawTime, { clear: false });
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
const editor = new StrudelMirror({
|
||||
id,
|
||||
defaultOutput: webaudioOutput,
|
||||
getTime: () => getAudioContext().currentTime,
|
||||
transpiler,
|
||||
autodraw: !!shouldDraw,
|
||||
root: containerRef.current,
|
||||
initialCode: '// LOADING',
|
||||
pattern: silence,
|
||||
drawTime,
|
||||
onDraw,
|
||||
editPattern: (pat, id) => {
|
||||
if (onTrigger) {
|
||||
pat = pat.onTrigger(onTrigger, false);
|
||||
}
|
||||
if (claviature) {
|
||||
editor?.painters.push((ctx, time, haps, drawTime) => {
|
||||
const active = haps
|
||||
.map((hap) => hap.value.note)
|
||||
.filter(Boolean)
|
||||
.map((n) => (typeof n === 'string' ? noteToMidi(n) : n));
|
||||
setActiveNotes(active);
|
||||
});
|
||||
}
|
||||
if (punchcard) {
|
||||
editor?.painters.push(getPunchcardPainter({ labels: !!punchcardLabels }));
|
||||
}
|
||||
return pat;
|
||||
},
|
||||
prebake,
|
||||
onUpdateState: (state) => {
|
||||
setReplState({ ...state });
|
||||
},
|
||||
});
|
||||
// init settings
|
||||
editor.setCode(code);
|
||||
editorRef.current = editor;
|
||||
}, []);
|
||||
|
||||
const [replState, setReplState] = useState({});
|
||||
const { started, isDirty, error } = replState;
|
||||
const editorRef = useRef();
|
||||
const containerRef = useRef();
|
||||
const [client, setClient] = useState(false);
|
||||
useEffect(() => {
|
||||
setClient(true);
|
||||
if (!editorRef.current) {
|
||||
setTimeout(() => {
|
||||
init({ code, shouldDraw });
|
||||
});
|
||||
}
|
||||
return () => {
|
||||
editorRef.current?.clear();
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!client) {
|
||||
return <pre>{code}</pre>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden rounded-t-md bg-background border border-lineHighlight">
|
||||
{!hideHeader && (
|
||||
<div className="flex justify-between bg-lineHighlight">
|
||||
<div className="flex">
|
||||
<button
|
||||
className={cx(
|
||||
'cursor-pointer w-16 flex items-center justify-center p-1 border-r border-lineHighlight text-foreground bg-lineHighlight hover:bg-background',
|
||||
started ? 'animate-pulse' : '',
|
||||
)}
|
||||
onClick={() => editorRef.current?.toggle()}
|
||||
>
|
||||
<Icon type={started ? 'stop' : 'play'} />
|
||||
</button>
|
||||
<button
|
||||
className={cx(
|
||||
'w-16 flex items-center justify-center p-1 text-foreground border-lineHighlight bg-lineHighlight',
|
||||
isDirty ? 'text-foreground hover:bg-background cursor-pointer' : 'opacity-50 cursor-not-allowed',
|
||||
)}
|
||||
onClick={() => editorRef.current?.evaluate()}
|
||||
>
|
||||
<Icon type="refresh" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="overflow-auto relative p-1">
|
||||
<div ref={containerRef}></div>
|
||||
{error && <div className="text-right p-1 text-md text-red-200">{error.message}</div>}
|
||||
</div>
|
||||
{shouldShowCanvas && (
|
||||
<canvas
|
||||
id={canvasId}
|
||||
className="w-full pointer-events-none border-t border-lineHighlight"
|
||||
height={canvasHeight}
|
||||
ref={(el) => {
|
||||
if (el && el.width !== el.clientWidth) {
|
||||
el.width = el.clientWidth;
|
||||
}
|
||||
}}
|
||||
></canvas>
|
||||
)}
|
||||
{/* !!log.length && (
|
||||
<div className="bg-gray-800 rounded-md p-2">
|
||||
{log.map(({ message }, i) => (
|
||||
<div key={i}>{message}</div>
|
||||
))}
|
||||
</div>
|
||||
) */}
|
||||
{claviature && (
|
||||
<Claviature
|
||||
options={{
|
||||
range: ['C2', 'C6'],
|
||||
scaleY: 0.75,
|
||||
colorize: [{ keys: activeNotes, color: 'steelblue' }],
|
||||
labels: claviatureLabels || {},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function cx(...classes) {
|
||||
// : Array<string | undefined>
|
||||
return classes.filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
function s4() {
|
||||
return Math.floor((1 + Math.random()) * 0x10000)
|
||||
.toString(16)
|
||||
.substring(1);
|
||||
}
|
||||
@ -1,84 +1,159 @@
|
||||
import { evalScope, controls, noteToMidi } from '@strudel.cycles/core';
|
||||
import { initAudioOnFirstClick } from '@strudel.cycles/webaudio';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { prebake } from '../repl/prebake';
|
||||
import { themes, settings } from '../repl/themes.mjs';
|
||||
import './MiniRepl.css';
|
||||
import { useSettings } from '../settings.mjs';
|
||||
import { useState, useRef, useCallback, useMemo, useEffect } from 'react';
|
||||
import { Icon } from './Icon';
|
||||
import { silence, getPunchcardPainter, noteToMidi } from '@strudel.cycles/core';
|
||||
import { transpiler } from '@strudel.cycles/transpiler';
|
||||
import { getAudioContext, webaudioOutput } from '@strudel.cycles/webaudio';
|
||||
import { StrudelMirror } from '@strudel/codemirror';
|
||||
// import { prebake } from '@strudel/repl';
|
||||
import { prebake } from '../repl/prebake.mjs';
|
||||
import { loadModules } from '../repl/util.mjs';
|
||||
import Claviature from '@components/Claviature';
|
||||
|
||||
let modules;
|
||||
let prebaked, modulesLoading;
|
||||
if (typeof window !== 'undefined') {
|
||||
modules = evalScope(
|
||||
controls,
|
||||
import('@strudel.cycles/core'),
|
||||
import('@strudel.cycles/tonal'),
|
||||
import('@strudel.cycles/mini'),
|
||||
import('@strudel.cycles/midi'),
|
||||
import('@strudel.cycles/xen'),
|
||||
import('@strudel.cycles/webaudio'),
|
||||
import('@strudel.cycles/osc'),
|
||||
import('@strudel.cycles/csound'),
|
||||
import('@strudel.cycles/soundfonts'),
|
||||
import('@strudel/hydra'),
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
initAudioOnFirstClick();
|
||||
prebake();
|
||||
prebaked = prebake();
|
||||
modulesLoading = loadModules();
|
||||
}
|
||||
|
||||
export function MiniRepl({
|
||||
tune,
|
||||
drawTime,
|
||||
tune: code,
|
||||
hideHeader = false,
|
||||
canvasHeight = 100,
|
||||
onTrigger,
|
||||
punchcard,
|
||||
punchcardLabels = true,
|
||||
span = [0, 4],
|
||||
canvasHeight = 100,
|
||||
hideHeader,
|
||||
claviature,
|
||||
claviatureLabels,
|
||||
}) {
|
||||
const [Repl, setRepl] = useState();
|
||||
const { theme, keybindings, fontSize, fontFamily, isLineNumbersDisplayed, isActiveLineHighlighted } = useSettings();
|
||||
const id = useMemo(() => s4(), []);
|
||||
const canvasId = useMemo(() => `canvas-${id}`, [id]);
|
||||
const shouldDraw = !!punchcard || !!claviature;
|
||||
const shouldShowCanvas = !!punchcard;
|
||||
const drawTime = punchcard ? [0, 4] : [0, 0];
|
||||
const [activeNotes, setActiveNotes] = useState([]);
|
||||
useEffect(() => {
|
||||
// we have to load this package on the client
|
||||
// because codemirror throws an error on the server
|
||||
Promise.all([import('@strudel.cycles/react'), modules])
|
||||
.then(([res]) => setRepl(() => res.MiniRepl))
|
||||
.catch((err) => console.error(err));
|
||||
}, []);
|
||||
return Repl ? (
|
||||
<div className="mb-4 mini-repl">
|
||||
<Repl
|
||||
tune={tune}
|
||||
hideOutsideView={true}
|
||||
drawTime={claviature ? [0, 0] : drawTime}
|
||||
punchcard={punchcard}
|
||||
punchcardLabels={punchcardLabels}
|
||||
span={span}
|
||||
canvasHeight={canvasHeight}
|
||||
theme={themes[theme]}
|
||||
hideHeader={hideHeader}
|
||||
keybindings={keybindings}
|
||||
fontFamily={fontFamily}
|
||||
fontSize={fontSize}
|
||||
isLineNumbersDisplayed={isLineNumbersDisplayed}
|
||||
isActiveLineHighlighted={isActiveLineHighlighted}
|
||||
onPaint={
|
||||
claviature
|
||||
? (ctx, time, haps, drawTime) => {
|
||||
const active = haps
|
||||
.map((hap) => hap.value.note)
|
||||
.filter(Boolean)
|
||||
.map((n) => (typeof n === 'string' ? noteToMidi(n) : n));
|
||||
setActiveNotes(active);
|
||||
}
|
||||
: undefined
|
||||
|
||||
const init = useCallback(({ code, shouldDraw }) => {
|
||||
const drawContext = shouldDraw ? document.querySelector('#' + canvasId)?.getContext('2d') : null;
|
||||
let onDraw;
|
||||
if (shouldDraw) {
|
||||
onDraw = (haps, time, frame, painters) => {
|
||||
painters.length && drawContext?.clearRect(0, 0, drawContext.canvas.width * 2, drawContext.canvas.height * 2);
|
||||
painters?.forEach((painter) => {
|
||||
// ctx time haps drawTime paintOptions
|
||||
painter(drawContext, time, haps, drawTime, { clear: false });
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
const editor = new StrudelMirror({
|
||||
id,
|
||||
defaultOutput: webaudioOutput,
|
||||
getTime: () => getAudioContext().currentTime,
|
||||
transpiler,
|
||||
autodraw: !!shouldDraw,
|
||||
root: containerRef.current,
|
||||
initialCode: '// LOADING',
|
||||
pattern: silence,
|
||||
drawTime,
|
||||
onDraw,
|
||||
editPattern: (pat, id) => {
|
||||
if (onTrigger) {
|
||||
pat = pat.onTrigger(onTrigger, false);
|
||||
}
|
||||
/>
|
||||
if (claviature) {
|
||||
editor?.painters.push((ctx, time, haps, drawTime) => {
|
||||
const active = haps
|
||||
.map((hap) => hap.value.note)
|
||||
.filter(Boolean)
|
||||
.map((n) => (typeof n === 'string' ? noteToMidi(n) : n));
|
||||
setActiveNotes(active);
|
||||
});
|
||||
}
|
||||
if (punchcard) {
|
||||
editor?.painters.push(getPunchcardPainter({ labels: !!punchcardLabels }));
|
||||
}
|
||||
return pat;
|
||||
},
|
||||
prebake: async () => Promise.all([modulesLoading, prebaked]),
|
||||
onUpdateState: (state) => {
|
||||
setReplState({ ...state });
|
||||
},
|
||||
});
|
||||
// init settings
|
||||
editor.setCode(code);
|
||||
editorRef.current = editor;
|
||||
}, []);
|
||||
|
||||
const [replState, setReplState] = useState({});
|
||||
const { started, isDirty, error } = replState;
|
||||
const editorRef = useRef();
|
||||
const containerRef = useRef();
|
||||
const [client, setClient] = useState(false);
|
||||
useEffect(() => {
|
||||
setClient(true);
|
||||
if (!editorRef.current) {
|
||||
setTimeout(() => {
|
||||
init({ code, shouldDraw });
|
||||
});
|
||||
}
|
||||
return () => {
|
||||
editorRef.current?.clear();
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!client) {
|
||||
return <pre>{code}</pre>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden rounded-t-md bg-background border border-lineHighlight">
|
||||
{!hideHeader && (
|
||||
<div className="flex justify-between bg-lineHighlight">
|
||||
<div className="flex">
|
||||
<button
|
||||
className={cx(
|
||||
'cursor-pointer w-16 flex items-center justify-center p-1 border-r border-lineHighlight text-foreground bg-lineHighlight hover:bg-background',
|
||||
started ? 'animate-pulse' : '',
|
||||
)}
|
||||
onClick={() => editorRef.current?.toggle()}
|
||||
>
|
||||
<Icon type={started ? 'stop' : 'play'} />
|
||||
</button>
|
||||
<button
|
||||
className={cx(
|
||||
'w-16 flex items-center justify-center p-1 text-foreground border-lineHighlight bg-lineHighlight',
|
||||
isDirty ? 'text-foreground hover:bg-background cursor-pointer' : 'opacity-50 cursor-not-allowed',
|
||||
)}
|
||||
onClick={() => editorRef.current?.evaluate()}
|
||||
>
|
||||
<Icon type="refresh" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="overflow-auto relative p-1">
|
||||
<div ref={containerRef}></div>
|
||||
{error && <div className="text-right p-1 text-md text-red-200">{error.message}</div>}
|
||||
</div>
|
||||
{shouldShowCanvas && (
|
||||
<canvas
|
||||
id={canvasId}
|
||||
className="w-full pointer-events-none border-t border-lineHighlight"
|
||||
height={canvasHeight}
|
||||
ref={(el) => {
|
||||
if (el && el.width !== el.clientWidth) {
|
||||
el.width = el.clientWidth;
|
||||
}
|
||||
}}
|
||||
></canvas>
|
||||
)}
|
||||
{/* !!log.length && (
|
||||
<div className="bg-gray-800 rounded-md p-2">
|
||||
{log.map(({ message }, i) => (
|
||||
<div key={i}>{message}</div>
|
||||
))}
|
||||
</div>
|
||||
) */}
|
||||
{claviature && (
|
||||
<Claviature
|
||||
options={{
|
||||
@ -90,7 +165,16 @@ export function MiniRepl({
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<pre>{tune}</pre>
|
||||
);
|
||||
}
|
||||
|
||||
function cx(...classes) {
|
||||
// : Array<string | undefined>
|
||||
return classes.filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
function s4() {
|
||||
return Math.floor((1 + Math.random()) * 0x10000)
|
||||
.toString(16)
|
||||
.substring(1);
|
||||
}
|
||||
|
||||
@ -1,312 +0,0 @@
|
||||
---
|
||||
title: Recipes
|
||||
layout: ../../layouts/MainLayout.astro
|
||||
---
|
||||
|
||||
import { MicroRepl } from '../../docs/MicroRepl';
|
||||
|
||||
# Recipes
|
||||
|
||||
This page shows possible ways to achieve common (or not so common) musical goals.
|
||||
There are often many ways to do a thing and there is no right or wrong.
|
||||
The fun part is that each representation will give you different impulses when improvising.
|
||||
|
||||
## Arpeggios
|
||||
|
||||
An arpeggio is when the notes of a chord are played in sequence.
|
||||
We can either write the notes by hand:
|
||||
|
||||
<MicroRepl
|
||||
client:visible
|
||||
code={`note("c eb g c4")
|
||||
.clip(2).s("gm_electric_guitar_clean")`}
|
||||
punchcard
|
||||
/>
|
||||
|
||||
...or use scales:
|
||||
|
||||
<MicroRepl
|
||||
client:visible
|
||||
code={`n("0 2 4 7").scale("C:minor")
|
||||
.clip(2).s("gm_electric_guitar_clean")`}
|
||||
punchcard
|
||||
/>
|
||||
|
||||
...or chord symbols:
|
||||
|
||||
<MicroRepl
|
||||
client:visible
|
||||
code={`n("0 1 2 3").chord("Cm").mode("above:c3").voicing()
|
||||
.clip(2).s("gm_electric_guitar_clean")`}
|
||||
punchcard
|
||||
/>
|
||||
|
||||
...using off:
|
||||
|
||||
<MicroRepl
|
||||
client:visible
|
||||
code={`"0"
|
||||
.off(1/3, add(2))
|
||||
.off(1/2, add(4))
|
||||
.n()
|
||||
.scale("C:minor")
|
||||
.s("gm_electric_guitar_clean")`}
|
||||
punchcard
|
||||
/>
|
||||
|
||||
## Chopping Breaks
|
||||
|
||||
A sample can be looped and chopped like this:
|
||||
|
||||
<MicroRepl
|
||||
client:visible
|
||||
code={`await samples('github:yaxu/clean-breaks/main')
|
||||
s("amen/8").fit().chop(16)`}
|
||||
punchcard
|
||||
/>
|
||||
|
||||
This fits the break into 8 cycles + chops it in 16 pieces.
|
||||
The chops are not audible yet, because we're not doing any manipulation.
|
||||
Let's add randmized doubling + reversing:
|
||||
|
||||
<MicroRepl
|
||||
client:visible
|
||||
code={`await samples('github:yaxu/clean-breaks/main')
|
||||
s("amen/8").fit().chop(16).cut(1)
|
||||
.sometimesBy(.5, ply(2))
|
||||
.sometimesBy(.25, mul(speed(-1)))`}
|
||||
punchcard
|
||||
/>
|
||||
|
||||
If we want to specify the order of samples, we can replace `chop` with `slice`:
|
||||
|
||||
<MicroRepl
|
||||
client:visible
|
||||
code={`await samples('github:yaxu/clean-breaks/main')
|
||||
s("amen/8").fit()
|
||||
.slice(8, "<0 1 2 3 4*2 5 6 [6 7]>")
|
||||
.cut(1).rarely(ply(2))`}
|
||||
punchcard
|
||||
/>
|
||||
|
||||
If we use `splice` instead of `slice`, the speed adjusts to the duration of the event:
|
||||
|
||||
<MicroRepl
|
||||
client:visible
|
||||
code={`await samples('github:yaxu/clean-breaks/main')
|
||||
s("amen")
|
||||
.splice(8, "<0 1 2 3 4*2 5 6 [6 7]>")
|
||||
.cut(1).rarely(ply(2))`}
|
||||
punchcard
|
||||
/>
|
||||
|
||||
Note that we don't need `fit`, because `splice` will do that by itself.
|
||||
|
||||
## Filter Envelopes
|
||||
|
||||
A minimal filter envelope looks like this:
|
||||
|
||||
<MicroRepl
|
||||
client:visible
|
||||
code={`note("g1 bb1 <c2 eb2> d2")
|
||||
.s("sawtooth")
|
||||
.lpf(400).lpa(.2).lpenv(4)
|
||||
.scope()`}
|
||||
/>
|
||||
|
||||
We can flip the envelope by setting `lpenv` negative + add some resonance `lpq`:
|
||||
|
||||
<MicroRepl
|
||||
client:visible
|
||||
code={`note("g1 bb1 <c2 eb2> d2")
|
||||
.s("sawtooth").lpq(8)
|
||||
.lpf(400).lpa(.2).lpenv(-4)
|
||||
.scope()`}
|
||||
/>
|
||||
|
||||
## Layering Sounds
|
||||
|
||||
We can layer sounds by separating them with ",":
|
||||
|
||||
<MicroRepl
|
||||
client:visible
|
||||
code={`note("<g1 bb1 d2 f1>")
|
||||
.s("sawtooth, square") // <------
|
||||
.scope()`}
|
||||
/>
|
||||
|
||||
We can control the gain of individual sounds like this:
|
||||
|
||||
<MicroRepl
|
||||
client:visible
|
||||
code={`note("<g1 bb1 d2 f1>")
|
||||
.s("sawtooth, square:0:.5") // <--- "name:number:gain"
|
||||
.scope()`}
|
||||
/>
|
||||
|
||||
For more control over each voice, we can use `layer`:
|
||||
|
||||
<MicroRepl
|
||||
client:visible
|
||||
code={`note("<g1 bb1 d2 f1>").layer(
|
||||
x=>x.s("sawtooth").vib(4),
|
||||
x=>x.s("square").add(note(12))
|
||||
).scope()`}
|
||||
/>
|
||||
|
||||
Here, we give the sawtooth a vibrato and the square is moved an octave up.
|
||||
With `layer`, you can use any pattern method available on each voice, so sky is the limit..
|
||||
|
||||
## Oscillator Detune
|
||||
|
||||
We can fatten a sound by adding a detuned version to itself:
|
||||
|
||||
<MicroRepl
|
||||
client:visible
|
||||
code={`note("<g1 bb1 d2 f1>")
|
||||
.add(note("0,.1")) // <------ chorus
|
||||
.s("sawtooth").scope()`}
|
||||
punchcard
|
||||
/>
|
||||
|
||||
Try out different values, or add another voice!
|
||||
|
||||
## Polyrhythms
|
||||
|
||||
Here is a simple example of a polyrhythm:
|
||||
|
||||
<MicroRepl client:visible code={`s("bd*2,hh*3")`} punchcard />
|
||||
|
||||
A polyrhythm is when 2 different tempos happen at the same time.
|
||||
|
||||
## Polymeter
|
||||
|
||||
This is a polymeter:
|
||||
|
||||
<MicroRepl client:visible code={`s("<bd rim>,<hh hh oh>").fast(2)`} punchcard />
|
||||
|
||||
A polymeter is when 2 different bar lengths play at the same tempo.
|
||||
|
||||
## Phasing
|
||||
|
||||
This is a phasing:
|
||||
|
||||
<MicroRepl client:visible code={`note("<C D G A Bb D C A G D Bb A>*[6,6.1]").piano()`} punchcard />
|
||||
|
||||
Phasing happens when the same sequence plays at slightly different tempos.
|
||||
|
||||
## Running through samples
|
||||
|
||||
Using `run` with `n`, we can rush through a sample bank:
|
||||
|
||||
<MicroRepl
|
||||
client:visible
|
||||
code={`await samples('github:Bubobubobubobubo/Dough-Fox/main')
|
||||
n(run(8)).s("ftabla")`}
|
||||
punchcard
|
||||
/>
|
||||
|
||||
This works great with sample banks that contain similar sounds, like in this case different recordings of a tabla.
|
||||
Often times, you'll hear the beginning of the phrase not where the pattern begins.
|
||||
In this case, I hear the beginning at the third sample, which can be accounted for with `early`.
|
||||
|
||||
<MicroRepl
|
||||
client:visible
|
||||
code={`await samples('github:Bubobubobubobubo/Dough-Fox/main')
|
||||
n(run(8)).s("ftabla").early(2/8)`}
|
||||
/>
|
||||
|
||||
Let's add some randomness:
|
||||
|
||||
<MicroRepl
|
||||
client:visible
|
||||
code={`await samples('github:Bubobubobubobubo/Dough-Fox/main')
|
||||
n(run(8)).s("ftabla").early(2/8)
|
||||
.sometimes(mul(speed(1.5)))`}
|
||||
/>
|
||||
|
||||
## Tape Warble
|
||||
|
||||
We can emulate a pitch warbling effect like this:
|
||||
|
||||
<MicroRepl
|
||||
client:visible
|
||||
code={`note("c4 bb f eb")
|
||||
.add(note(perlin.range(0,.5))) // <------ warble
|
||||
.clip(2).s("gm_electric_guitar_clean")`}
|
||||
/>
|
||||
|
||||
## Sound Duration
|
||||
|
||||
There are a number of ways to change the sound duration. Using clip:
|
||||
|
||||
<MicroRepl
|
||||
client:visible
|
||||
code={`note("f ab bb c")
|
||||
.clip("<2 1 .5 .25>/2")`}
|
||||
/>
|
||||
|
||||
The value of clip is relative to the duration of each event.
|
||||
We can also create overlaps using release:
|
||||
|
||||
<MicroRepl
|
||||
client:visible
|
||||
code={`note("f ab bb c")
|
||||
.release("<2 1 .5 .002>/2")`}
|
||||
/>
|
||||
|
||||
This will smoothly fade out each sound for the given number of seconds.
|
||||
We could also make the notes shorter with decay / sustain:
|
||||
|
||||
<MicroRepl
|
||||
client:visible
|
||||
code={`note("f ab bb c")
|
||||
.decay("<.2 .1 .02>/2").sustain(0)`}
|
||||
/>
|
||||
|
||||
For now, there is a limitation where decay values that exceed the event duration may cause little cracks, so use higher numbers with caution..
|
||||
|
||||
When using samples, we also have `.end` to cut relative to the sample length:
|
||||
|
||||
<MicroRepl client:visible code={`s("oh*4").end("<1 .5 .25 .1>")`} />
|
||||
|
||||
Compare that to clip:
|
||||
|
||||
<MicroRepl client:visible code={`s("oh*4").clip("<1 .5 .25 .1>")`} />
|
||||
|
||||
or decay / sustain
|
||||
|
||||
<MicroRepl client:visible code={`s("oh*4").decay("<.2 .12 .06 .01>").sustain(0)`} />
|
||||
|
||||
## Wavetable Synthesis
|
||||
|
||||
You can loop a sample with `loop` / `loopEnd`:
|
||||
|
||||
<MicroRepl client:visible code={`note("<c eb g f>").s("bd").loop(1).loopEnd(.05).gain(.2)`} />
|
||||
|
||||
This allows us to play the first 5% of the bass drum as a synth!
|
||||
To simplify loading wavetables, any sample that starts with `wt_` will be looped automatically:
|
||||
|
||||
<MicroRepl
|
||||
client:visible
|
||||
code={`await samples('github:bubobubobubobubo/dough-waveforms/main')
|
||||
note("c eb g bb").s("wt_dbass").clip(2)`}
|
||||
/>
|
||||
|
||||
Running through different wavetables can also give interesting variations:
|
||||
|
||||
<MicroRepl
|
||||
client:visible
|
||||
code={`await samples('github:bubobubobubobubo/dough-waveforms/main')
|
||||
note("c2*8").s("wt_dbass").n(run(8))`}
|
||||
/>
|
||||
|
||||
...adding a filter envelope + reverb:
|
||||
|
||||
<MicroRepl
|
||||
client:visible
|
||||
code={`await samples('github:bubobubobubobubo/dough-waveforms/main')
|
||||
note("c2*8").s("wt_dbass").n(run(8))
|
||||
.lpf(perlin.range(200,2000).slow(8))
|
||||
.lpenv(-3).lpa(.1).room(.5)`}
|
||||
/>
|
||||
@ -14,7 +14,7 @@ Example:
|
||||
|
||||
<MiniRepl
|
||||
client:idle
|
||||
tune={`const pattern = sequence(c3, [e3, g3])
|
||||
tune={`const pattern = sequence("c3", ["e3", "g3"])
|
||||
const events = pattern.queryArc(0, 1)
|
||||
console.log(events.map((e) => e.show()))
|
||||
silence`}
|
||||
|
||||
@ -77,6 +77,9 @@ async function bufferToDataUrl(buf) {
|
||||
//open db and initialize it if necessary
|
||||
const openDB = (config, onOpened) => {
|
||||
const { dbName, version, table, columns } = config;
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
if (!('indexedDB' in window)) {
|
||||
console.log('IndexedDB is not supported.');
|
||||
return;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user