mirror of
https://github.com/eliasstepanik/strudel-docker.git
synced 2026-01-11 13:48: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 { useState, useRef, useCallback, useMemo, useEffect } from 'react';
|
||||||
import { initAudioOnFirstClick } from '@strudel.cycles/webaudio';
|
import { Icon } from './Icon';
|
||||||
import { useEffect, useState } from 'react';
|
import { silence, getPunchcardPainter, noteToMidi } from '@strudel.cycles/core';
|
||||||
import { prebake } from '../repl/prebake';
|
import { transpiler } from '@strudel.cycles/transpiler';
|
||||||
import { themes, settings } from '../repl/themes.mjs';
|
import { getAudioContext, webaudioOutput } from '@strudel.cycles/webaudio';
|
||||||
import './MiniRepl.css';
|
import { StrudelMirror } from '@strudel/codemirror';
|
||||||
import { useSettings } from '../settings.mjs';
|
// import { prebake } from '@strudel/repl';
|
||||||
|
import { prebake } from '../repl/prebake.mjs';
|
||||||
|
import { loadModules } from '../repl/util.mjs';
|
||||||
import Claviature from '@components/Claviature';
|
import Claviature from '@components/Claviature';
|
||||||
|
|
||||||
let modules;
|
let prebaked, modulesLoading;
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
modules = evalScope(
|
prebaked = prebake();
|
||||||
controls,
|
modulesLoading = loadModules();
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MiniRepl({
|
export function MiniRepl({
|
||||||
tune,
|
tune: code,
|
||||||
drawTime,
|
hideHeader = false,
|
||||||
|
canvasHeight = 100,
|
||||||
|
onTrigger,
|
||||||
punchcard,
|
punchcard,
|
||||||
punchcardLabels = true,
|
punchcardLabels = true,
|
||||||
span = [0, 4],
|
|
||||||
canvasHeight = 100,
|
|
||||||
hideHeader,
|
|
||||||
claviature,
|
claviature,
|
||||||
claviatureLabels,
|
claviatureLabels,
|
||||||
}) {
|
}) {
|
||||||
const [Repl, setRepl] = useState();
|
const id = useMemo(() => s4(), []);
|
||||||
const { theme, keybindings, fontSize, fontFamily, isLineNumbersDisplayed, isActiveLineHighlighted } = useSettings();
|
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 [activeNotes, setActiveNotes] = useState([]);
|
||||||
useEffect(() => {
|
|
||||||
// we have to load this package on the client
|
const init = useCallback(({ code, shouldDraw }) => {
|
||||||
// because codemirror throws an error on the server
|
const drawContext = shouldDraw ? document.querySelector('#' + canvasId)?.getContext('2d') : null;
|
||||||
Promise.all([import('@strudel.cycles/react'), modules])
|
let onDraw;
|
||||||
.then(([res]) => setRepl(() => res.MiniRepl))
|
if (shouldDraw) {
|
||||||
.catch((err) => console.error(err));
|
onDraw = (haps, time, frame, painters) => {
|
||||||
}, []);
|
painters.length && drawContext?.clearRect(0, 0, drawContext.canvas.width * 2, drawContext.canvas.height * 2);
|
||||||
return Repl ? (
|
painters?.forEach((painter) => {
|
||||||
<div className="mb-4 mini-repl">
|
// ctx time haps drawTime paintOptions
|
||||||
<Repl
|
painter(drawContext, time, haps, drawTime, { clear: false });
|
||||||
tune={tune}
|
});
|
||||||
hideOutsideView={true}
|
};
|
||||||
drawTime={claviature ? [0, 0] : drawTime}
|
}
|
||||||
punchcard={punchcard}
|
|
||||||
punchcardLabels={punchcardLabels}
|
const editor = new StrudelMirror({
|
||||||
span={span}
|
id,
|
||||||
canvasHeight={canvasHeight}
|
defaultOutput: webaudioOutput,
|
||||||
theme={themes[theme]}
|
getTime: () => getAudioContext().currentTime,
|
||||||
hideHeader={hideHeader}
|
transpiler,
|
||||||
keybindings={keybindings}
|
autodraw: !!shouldDraw,
|
||||||
fontFamily={fontFamily}
|
root: containerRef.current,
|
||||||
fontSize={fontSize}
|
initialCode: '// LOADING',
|
||||||
isLineNumbersDisplayed={isLineNumbersDisplayed}
|
pattern: silence,
|
||||||
isActiveLineHighlighted={isActiveLineHighlighted}
|
drawTime,
|
||||||
onPaint={
|
onDraw,
|
||||||
claviature
|
editPattern: (pat, id) => {
|
||||||
? (ctx, time, haps, drawTime) => {
|
if (onTrigger) {
|
||||||
const active = haps
|
pat = pat.onTrigger(onTrigger, false);
|
||||||
.map((hap) => hap.value.note)
|
|
||||||
.filter(Boolean)
|
|
||||||
.map((n) => (typeof n === 'string' ? noteToMidi(n) : n));
|
|
||||||
setActiveNotes(active);
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
}
|
||||||
/>
|
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 && (
|
||||||
<Claviature
|
<Claviature
|
||||||
options={{
|
options={{
|
||||||
@ -90,7 +165,16 @@ export function MiniRepl({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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
|
<MiniRepl
|
||||||
client:idle
|
client:idle
|
||||||
tune={`const pattern = sequence(c3, [e3, g3])
|
tune={`const pattern = sequence("c3", ["e3", "g3"])
|
||||||
const events = pattern.queryArc(0, 1)
|
const events = pattern.queryArc(0, 1)
|
||||||
console.log(events.map((e) => e.show()))
|
console.log(events.map((e) => e.show()))
|
||||||
silence`}
|
silence`}
|
||||||
|
|||||||
@ -77,6 +77,9 @@ async function bufferToDataUrl(buf) {
|
|||||||
//open db and initialize it if necessary
|
//open db and initialize it if necessary
|
||||||
const openDB = (config, onOpened) => {
|
const openDB = (config, onOpened) => {
|
||||||
const { dbName, version, table, columns } = config;
|
const { dbName, version, table, columns } = config;
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!('indexedDB' in window)) {
|
if (!('indexedDB' in window)) {
|
||||||
console.log('IndexedDB is not supported.');
|
console.log('IndexedDB is not supported.');
|
||||||
return;
|
return;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user