Compare commits

..

29 Commits

Author SHA1 Message Date
Felix Roos 1f54d93a00 last minute edits 2023-04-19 08:20:21 +02:00
Felix Roos 284f6293a2 fix: colorable highlighting 2023-04-19 01:06:32 +02:00
alex a2b99f94bd slide tweaks 2023-04-18 08:49:28 +01:00
Felix Roos 8a0f9c6e36 add swatch slide 2023-04-17 00:31:38 +02:00
Felix Roos d38a8ab1fa more slides 2023-04-16 23:55:12 +02:00
Felix Roos 6a451be50c offline ready soundfonts 2023-04-16 23:54:06 +02:00
Felix Roos dac2996b3d Merge remote-tracking branch 'origin/main' into iclc-slides 2023-04-16 15:58:27 +02:00
Felix Roos d689d71ee6 make slides samples work offline 2023-04-16 15:57:40 +02:00
Felix Roos a4a6321afb move stuff around, add flow charts 2023-04-15 21:01:42 +02:00
Felix Roos 8b13badb67 add more slides 2023-04-15 14:15:02 +02:00
Felix Roos 15a6747b2c move stuff around + mini notation slide 2023-04-06 23:24:40 +02:00
Felix Roos a96ff66f63 remove padding from stepper 2023-04-06 22:44:14 +02:00
Felix Roos 123323e32b - add Stepper + use it everywhere
- rename files to make more sense
- add transpilation slide
2023-04-06 22:14:17 +02:00
Felix Roos 8ca850f9fe improve query demo + headings 2023-04-06 13:00:44 +02:00
Felix Roos d77725cf1d add events to audio slide 2023-04-06 12:34:34 +02:00
Felix Roos b016d115f5 - cover slide
- rename stuff
- make things more round
2023-04-06 11:55:53 +02:00
Felix Roos 1a57a86497 fix: mini repl logs 2023-04-06 11:55:03 +02:00
Felix Roos d433e68fb2 moar slides 2023-04-06 01:54:18 +02:00
Felix Roos c90bc80e6d add example tune 2023-04-05 23:48:14 +02:00
Felix Roos d3c4fd70dc slides: add missing syntax comparison labels 2023-04-05 23:47:53 +02:00
Felix Roos 87cab96893 also load tidal drum machines in slides 2023-04-05 23:47:26 +02:00
Felix Roos 0248d15470 only mount nearby slides 2023-04-05 23:47:04 +02:00
Felix Roos d4801c545e add syntax comparison slide 2023-04-05 23:26:07 +02:00
Felix Roos ce28acaf18 Merge branch 'main' into iclc-slides 2023-04-05 21:24:56 +02:00
Felix Roos bf0336404d add slides readme 2023-03-23 11:59:54 +01:00
Felix Roos 7c3d8e5c8d fix: import 2023-03-23 11:57:43 +01:00
Felix Roos 3dad55e266 Merge branch 'main' into iclc-slides 2023-03-23 11:56:14 +01:00
Felix Roos 3d9ac37efe add basic slides app 2023-03-19 22:08:43 +01:00
Felix Roos d4619362bd allow passing fontSize to MiniRepl 2023-03-19 22:04:17 +01:00
306 changed files with 19073 additions and 27739 deletions
-1
View File
@@ -18,4 +18,3 @@ vite.config.js
**/*.json **/*.json
**/dev-dist **/dev-dist
**/dist **/dist
/src-tauri/target/**/*
-62
View File
@@ -1,62 +0,0 @@
name: Tauri Builder
on:
push:
tags:
- "v*"
workflow_dispatch:
jobs:
release:
strategy:
fail-fast: false
matrix:
platform: [macos-latest, ubuntu-latest, windows-latest]
include:
- os: ubuntu-latest
rust_target: x86_64-unknown-linux-gnu
- os: macos-latest
rust_target: x86_64-apple-darwin
- os: macos-latest
rust_target: aarch64-apple-darwin
- os: windows-latest
rust_target: x86_64-pc-windows-msvc
runs-on: ${{ matrix.platform }}
steps:
- name: Checkout repository
uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: 8.6.2
- name: Node.js setup
uses: actions/setup-node@v3
with:
node-version: latest
# node-version-file: '.nvmrc'
- name: Install Rust (Stable)
run:
curl https://sh.rustup.rs -sSf | sh -s -- -y
- name: Install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf
- name: Install app dependencies from lockfile and build web
run: pnpm install
- name: Build the app
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
# tauri-action replaces \_\_VERSION\_\_ with the app version
tagName: ${{ github.ref_name }}
releaseName: "Strudel v__VERSION__"
releaseBody: |
See the assets to download this version and install.
releaseDraft: true
prerelease: false
-4
View File
@@ -40,7 +40,3 @@ repl/stats.html
coverage coverage
public/icons/apple-splash-* public/icons/apple-splash-*
dev-dist dev-dist
Dirt-Samples
tidal-drum-machines
webaudiofontdata
src-tauri/target
+1 -1
View File
@@ -31,7 +31,7 @@ Use one of the Communication Channels listed above.
## Improve the Docs ## Improve the Docs
If you find some weak spots in the [docs](https://strudel.tidalcycles.org/workshop/getting-started/), If you find some weak spots in the [docs](https://strudel.tidalcycles.org/learn/getting-started),
you can edit each file directly on github via the "Edit this page" link located in the right sidebar. you can edit each file directly on github via the "Edit this page" link located in the right sidebar.
## Propose a Feature ## Propose a Feature
+2 -2
View File
@@ -24,8 +24,8 @@ There are multiple npm packages you can use to use strudel, or only parts of it,
- [`core`](./packages/core/): tidal pattern engine - [`core`](./packages/core/): tidal pattern engine
- [`mini`](./packages/mini): mini notation parser + core binding - [`mini`](./packages/mini): mini notation parser + core binding
- [`transpiler`](./packages/transpiler): user code transpiler - [`eval`](./packages/eval): user code evaluator. syntax sugar + highlighting
- [`webaudio`](./packages/webaudio): webaudio output - [`tone`](./packages/tone): bindings for Tone.js instruments and effects
- [`osc`](./packages/osc): bindings to communicate via OSC - [`osc`](./packages/osc): bindings to communicate via OSC
- [`midi`](./packages/midi): webmidi bindings - [`midi`](./packages/midi): webmidi bindings
- [`serial`](./packages/serial): webserial bindings - [`serial`](./packages/serial): webserial bindings
@@ -22,3 +22,5 @@ dist-ssr
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
public/samples
+22
View File
@@ -0,0 +1,22 @@
# Usage
```sh
cd iclc2023/slides
pnpm i
pnpm dev
```
- ctrl+click or ctrl+(left|right) to switch slides
## Samples
To make sure all the samples are available, clone:
```sh
cd public/samples
git clone https://github.com/ritchse/tidal-drum-machines.git
git clone https://github.com/tidalcycles/Dirt-Samples.git
git clone https://github.com/felixroos/webaudiofontdata.git
```
or use symlinks..
+15
View File
@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<!-- <link rel="icon" type="image/svg+xml" href="/vite.svg" /> -->
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Slides</title>
</head>
<body style="background-color: black">
<div id="root">
<span style="color: white">loading... </span>
</div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
+40
View File
@@ -0,0 +1,40 @@
{
"name": "slides",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@mdx-js/rollup": "^2.3.0",
"@nanostores/persistent": "^0.7.0",
"@nanostores/react": "^0.4.1",
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/react": "workspace:*",
"@strudel.cycles/soundfonts": "workspace:*",
"@strudel.cycles/tonal": "workspace:*",
"@strudel.cycles/csound": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*",
"@uiw/codemirror-themes-all": "^4.19.8",
"escodegen": "^2.0.0",
"nanostores": "^0.7.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-syntax-highlighter": "^15.5.0",
"remark-gfm": "^3.0.1",
"vite-plugin-top-level-await": "^1.3.0"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.8",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@vitejs/plugin-react": "^3.1.0",
"autoprefixer": "^10.4.14",
"postcss": "^8.4.21",
"tailwindcss": "^3.2.7",
"vite": "^4.2.0"
}
}
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

File diff suppressed because it is too large Load Diff
+46
View File
@@ -0,0 +1,46 @@
import { useState, useCallback } from 'react';
import { atomone } from '@uiw/codemirror-themes-all';
import { CodeMirror, flash, useKeydown } from '@strudel.cycles/react';
import { initAudioOnFirstClick, webaudioOutput } from '@strudel.cycles/webaudio';
import { TimeSpan } from '@strudel.cycles/core';
const initAudio = initAudioOnFirstClick();
function EventEditor({ code: initialCode }) {
const [code, setCode] = useState(initialCode);
const [view, setView] = useState();
useKeydown(
useCallback(
async (e) => {
if (view?.hasFocus) {
if (e.ctrlKey || e.altKey) {
if (e.code === 'Enter') {
/* if (getAudioContext().state !== 'running') {
alert('please click play to initialize the audio. you can use shortcuts after that!');
return;
} */
e.preventDefault();
flash(view);
let json = JSON.parse(code);
if (!Array.isArray(json)) {
json = [json];
}
await initAudio;
json.forEach((value) => {
const hap = new Hap(new TimeSpan(0, 1), new TimeSpan(0, 1), value);
webaudioOutput(hap, 0.01, 1, 1);
});
} else if (e.key === '.') {
stop();
e.preventDefault();
}
}
}
},
[code, view],
),
);
return <CodeMirror value={code} onChange={setCode} theme={atomone} onViewChanged={(v) => setView(v)} fontSize={32} />;
}
export default EventEditor;
+39
View File
@@ -0,0 +1,39 @@
import EventEditor from './EventEditor.jsx';
import Stepper from './Stepper.jsx';
const snippets = [
[`{ "s": "cp", "crush": 4 }`, 'Samples'],
[`{ "s": "sawtooth", "note": "e3" }`, 'Oscillators'],
[
`[
{ "s": "gm_epiano2", "note": "c4" },
{ "s": "gm_epiano2", "note": "eb4" },
{ "s": "gm_epiano2", "note": "g4" }
]`,
'Soundfonts',
],
[
`{
"s": "toys",
"cutoff": 2000,
"delay": 0.5,
"crush": 4
}`,
'Effects',
],
];
function EventsDemo() {
return (
<Stepper
steps={snippets.map(([code, label]) => (
<div className="py-4 space-y-4">
<h3>{label}</h3>
<EventEditor code={code} />
</div>
))}
/>
);
}
export default EventsDemo;
+21
View File
@@ -0,0 +1,21 @@
//import {Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
//import { atomDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
//import SyntaxHighlighter from 'react-syntax-highlighter';
import { atomOneDark } from 'react-syntax-highlighter/dist/esm/styles/hljs';
import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
import js from 'react-syntax-highlighter/dist/esm/languages/hljs/javascript';
import hs from 'react-syntax-highlighter/dist/esm/languages/hljs/haskell';
SyntaxHighlighter.registerLanguage('javascript', js);
SyntaxHighlighter.registerLanguage('haskell', hs);
function Highlight({ code, language = 'javascript' }) {
return (
<div className="text-[32px] leading-10 rounded-xl overflow-hidden">
<SyntaxHighlighter language={language} style={atomOneDark} customStyle={{ padding: '8px 10px' }}>
{code}
</SyntaxHighlighter>
</div>
);
}
export default Highlight;
+38
View File
@@ -0,0 +1,38 @@
import { SlideRepl } from './SlideRepl.jsx';
import Stepper from './Stepper.jsx';
const snippets = [
[`sound("bd [hh sd]")`, `sound(seq("bd", ["hh", "sd"]))`, 'Nested Sequences'], //
[`sound("<bd sd>")`, `sound(cat("bd", "sd"))`, 'Cyclewise Sequences'], //
[`sound("bd,hh")`, `sound(stack("bd", "hh"))`, 'Stack'], //
[`sound("bd@3 sd@1")`, `sound(timeCat([3, "bd"], [1, "sd"]))`, 'Weighted Sequence'], //
[`sound("{lt ht mt, bd sd}")`, `sound(polymeter(["lt","ht","mt"], ["bd", "sd"]))`, 'Polymeter'], //
[`sound("{bd hh sd ht}%2")`, `sound(polymeterSteps(2, ["bd", "hh", "sd", "ht"]))`, 'Steps per Cycle'],
];
function MiniComparison() {
return (
<>
<h1>User Code: Mini Notation -&gt; JavaScript</h1>
<Stepper
steps={snippets.map(([a, b, label]) => (
<div className="py-4">
<h3 className="pb-4">{label}</h3>
<div className="space-y-2">
<div className="flex space-x-2">
<img src="./img/tidalcycles.svg" className={`h-10 mt-2`} />
<SlideRepl tune={a} hideHeader />
</div>
<div className="flex space-x-2">
<img src="./img/js.jpg" className={`h-10 mt-2`} />
<SlideRepl tune={b} hideHeader />
</div>
</div>
</div>
))}
/>
</>
);
}
export default MiniComparison;
+50
View File
@@ -0,0 +1,50 @@
import { SlideRepl } from './SlideRepl.jsx';
import Highlight from './Highlight.jsx';
import Stepper from './Stepper.jsx';
const snippets = [
[
<>
{' '}
<SlideRepl tune={`s("bd [hh sd]").crush(4).log()`} hideHeader />
</>,
'Logging Events',
],
[
<>
<Highlight
code={`let pattern = s("bd [hh sd]").crush(4);
let events = pattern.queryArc(0, 1);
console.log(events.map(e => e.show()))`}
language="javascript"
/>
<Highlight
language="json"
code={`[
"0/1 -> 1/2 | s:bd crush:4",
"1/2 -> 3/4 | s:hh crush:4",
"3/4 -> 1/1 | s:sd crush:4"
]`}
/>
</>,
'Querying Events Manually',
],
];
function QueryDemo() {
return (
<>
<img src="./img/queryflow.png" className="w-[800px]" />
<Stepper
steps={snippets.map(([snippet, label]) => (
<div className="py-4 space-y-4">
<h3>{label}</h3>
{snippet}
</div>
))}
/>
</>
);
}
export default QueryDemo;
+49
View File
@@ -0,0 +1,49 @@
import { evalScope, controls } from '@strudel.cycles/core';
import { initAudioOnFirstClick } from '@strudel.cycles/webaudio';
import { MiniRepl as _MiniRepl } from '@strudel.cycles/react';
import { prebake } from './prebake';
import { atomone } from '@uiw/codemirror-themes-all';
import blackscreen from '@strudel.cycles/react/src/themes/blackscreen';
if (typeof window !== 'undefined') {
await evalScope(
controls,
import('@strudel.cycles/core'),
import('@strudel.cycles/tonal'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/webaudio'),
import('@strudel.cycles/soundfonts'),
import('@strudel.cycles/csound'),
);
}
if (typeof window !== 'undefined') {
initAudioOnFirstClick();
prebake();
}
export function SlideRepl({
tune,
drawTime,
punchcard,
canvasHeight = 100,
hideHeader = false,
fontSize = 32,
disabled = false,
}) {
return (
<div className="not-prose rounded-xl overflow-hidden">
<_MiniRepl
hideHeader={hideHeader}
tune={tune}
hideOutsideView={true}
drawTime={drawTime}
punchcard={punchcard}
canvasHeight={canvasHeight}
fontSize={fontSize}
theme={disabled ? blackscreen : atomone}
// theme={themes[theme]}
/>
</div>
);
}
+125
View File
@@ -0,0 +1,125 @@
import { persistentAtom } from '@nanostores/persistent';
import { useStore } from '@nanostores/react'; // or '@nanostores/preact'
import useEvent from './useEvent';
function cx(...classes) {
return classes.filter(Boolean).join(' ');
}
// https://vitejs.dev/guide/features.html#glob-import
const slideImports = import.meta.glob('./slides/*.mdx');
//const slideImports = import.meta.glob('./slides/*.mdx', { eager: true });
console.log('load slides...', slideImports);
const loadedMDXFiles = await Promise.all(
Object.entries(slideImports).map(async ([path, load]) => {
//Object.entries(slideImports).map(async ([path, m]) => {
const segments = path.split('/');
const filename = segments[segments.length - 1].slice(0, -4); // expects .mdx at the end
return [filename, (await load()).default];
//return [filename, m.default];
}),
);
console.log('load done!');
const order = [
'cover',
'whatIsStrudel',
'history',
'hs2js',
'usage',
'swatch',
'repl',
//'repl-features',
'viz',
'replflow',
// User Code
'transpilation',
'mininotation',
// 'mini-comparison', // remove?
// Patterns
'patterns-events', // remove?
// scheduler?
// Output
'events-audio', // remove?
'outputs',
// 'csound', // remove?
//
// pattern alignment?
'bad',
'good',
'future',
'end',
];
// examples swatch
// draw examples!!
const slideEntries = order.map((name) => loadedMDXFiles.find(([file]) => file === name));
// current slide index is persisted, so it's safe to refresh the browser
export const slideIndex = persistentAtom('slideIndex', '0');
export const prev = () => slideIndex.set((parseInt(slideIndex.get()) - 1 + slideEntries.length) % slideEntries.length);
export const next = () => slideIndex.set((parseInt(slideIndex.get()) + 1) % slideEntries.length);
function Slides() {
const activeIndex = parseInt(useStore(slideIndex));
/*useEvent('click', (e) => {
if (!e.ctrlKey) {
return;
}
const leftHalf = e.clientX / document.body.clientWidth < 0.5;
if (leftHalf) {
prev();
} else {
next();
}
});*/
/*useEvent('keydown', (e) => {
if (e.key === 'Home') {
prev();
} else if (e.key === 'End') {
next();
}
});*/
return (
<div className="w-screen h-screen overflow-hidden relative">
{slideEntries.map(([path, Slide], i) => (
<div
key={i}
className={cx(
`bg-slate-900 w-full h-full flex justify-center absolute top-0 transition-all ease-in-out duration-500 overflow-auto`,
i < activeIndex && '-translate-x-full',
i > activeIndex && 'translate-x-full',
)}
>
<div className="prose prose-invert w-[1400px] p-12 max-w-full" style={{ fontSize: '2em' }}>
{Math.abs(i - activeIndex) <= 1 && <Slide />}
</div>
</div>
))}
<div className="text-white text-2xl text-bold cursor-pointer select-none">
<div className="fixed left-0 p-4 h-full items-start flex hover:bg-[#00000050]" onClick={() => prev()}>
&lt;
</div>
<div className="fixed right-0 p-4 h-full items-start flex hover:bg-[#00000050]" onClick={() => next()}>
&gt;
</div>
</div>
<div className="absolute top-0 w-full">
<div className="h-[5px] bg-slate-500 w-full z-100">
<div
style={{ width: ((activeIndex / (slideEntries.length - 1)) * 100).toFixed(0) + '%' }}
className="h-full bg-yellow-500"
/>
</div>
</div>
</div>
);
}
export default Slides;
+20
View File
@@ -0,0 +1,20 @@
import { useState } from 'react';
function Stepper({ steps }) {
const [step, setStep] = useState(1);
return (
<div className="not-prose">
{steps.slice(0, step).map((snippet, i) => {
const isActive = i === step - 1;
return (
<div key={i} className={`border-l-4 pl-8 border-gray-500 ${isActive ? 'border-yellow-500' : ''}`}>
{snippet}
</div>
);
})}
{step < steps.length && <button onClick={() => setStep((r) => r + 1)}>next</button>}
</div>
);
}
export default Stepper;
+73
View File
@@ -0,0 +1,73 @@
import { SlideRepl } from './SlideRepl.jsx';
import Highlight from './Highlight.jsx';
import Stepper from './Stepper.jsx';
const snippets = [
[`sound "bd ~ [sd cp]"`, [`sound("bd ~ [sd cp]")`], 'Mini Notation'], //
[`sound "bd ~ [sd cp]" # speed "1 2"`, [`sound("bd ~ [sd cp]").speed("1 2")`], 'Composing Patterns'],
[
`fast 2 $ sound "bd ~ [sd cp]"`,
[`fast(2, sound("bd ~ [sd cp]"))`, `sound("bd ~ [sd cp]").fast(2)`],
'Patterns Transformations',
],
[
`speed "1 2 3" + "4 5" # s "bd"`,
['speed("1 2 3".add("4 5")).s("bd")', '"1 2 3".add("4 5").speed().s("bd")'],
'Pattern Arithmetic',
],
[
`jux rev
$ every 3 (fast 2)
$ sound "bd sd"`,
[
`sound("bd sd")
.every(3, fast("2"))
.jux(rev)`,
],
'Higher order transformations (with partial application)',
],
[
`jux rev
$ every "<3 5>" (fast "1 2")
$ sound "bd sd cp mt"`,
[
`sound("bd sd cp mt")
.every("<3 5>", fast("1 2"))
.jux(rev)`,
],
'Patterns all the way down',
],
];
function SyntaxComparison() {
return (
<>
<h1>Haskell -&gt; JavaScript</h1>
<Stepper
steps={snippets
.map(([hs, js, label]) => (
<div className="py-4 space-y-4">
<h3>{label}</h3>
<div className="space-y-2">
<div className="flex space-x-4">
<img src="./img/haskell.png" className="h-10 mt-2" />
<Highlight language="haskell" code={hs} />
</div>
<div>
{js.map((c, j) => (
<div className="flex space-x-4" key={j}>
<img src="./img/js.jpg" className={`h-10 mt-2`} />
<SlideRepl tune={c} hideHeader />
</div>
))}
</div>
</div>
</div>
))
.concat([<>Fluent Interface: Just chain all the things!</>])}
/>
</>
);
}
export default SyntaxComparison;
+84
View File
@@ -0,0 +1,84 @@
import { useState, useCallback } from 'react';
import { atomone } from '@uiw/codemirror-themes-all';
import { CodeMirror, flash, useKeydown } from '@strudel.cycles/react';
import { transpiler } from '@strudel.cycles/transpiler';
import Highlight from './Highlight';
import Stepper from './Stepper';
import escodegen from 'escodegen';
function getTranspiled(code) {
return transpiler(code, {
addReturn: false,
codegenOptions: {
format: escodegen.FORMAT_MINIFY,
},
});
}
function TranspilationEditor({ code: initialCode }) {
const [code, setCode] = useState(initialCode);
const [transpiled, setTranspiled] = useState(getTranspiled(initialCode));
const [view, setView] = useState();
useKeydown(
useCallback(
async (e) => {
if (view?.hasFocus) {
if (e.ctrlKey || e.altKey) {
if (e.code === 'Enter') {
/* if (getAudioContext().state !== 'running') {
alert('please click play to initialize the audio. you can use shortcuts after that!');
return;
} */
e.preventDefault();
flash(view);
const t = getTranspiled(code);
setTranspiled(t);
console.log('transpiled', t);
} else if (e.key === '.') {
stop();
e.preventDefault();
}
}
}
},
[code, view],
),
);
return (
<div className="space-y-2 py-2">
<CodeMirror
value={code}
onChange={(v) => {
setCode(v);
}}
theme={atomone}
onViewChanged={(v) => setView(v)}
fontSize={32}
/>
<span> transpiles to</span>
<Highlight language="javascript" code={transpiled} />
</div>
);
}
function TranspilationDemo() {
return (
<div className="not-prose space-y-8">
<Stepper
steps={[
<>
<img src="./img/transpileflow.png" className="h-[500px]" />
<ul className="list-decimal pl-8">
<li>Parse JS Code with acorn to get AST</li>
<li>Modify AST with estree-walker</li>
<li>Convert modified AST back to JS with escodegen</li>
</ul>
</>,
<TranspilationEditor code={`"bd [hh sd]"`} />,
]}
/>
</div>
);
}
export default TranspilationDemo;
+21
View File
@@ -0,0 +1,21 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
.cm-activeLine {
background-color: transparent !important;
}
.cm-theme {
background-color: var(--background);
border: 1px solid var(--lineHighlight);
padding: 2px;
}
.cm-gutters {
display: none !important;
}
img {
@apply rounded-xl overflow-hidden;
}
+10
View File
@@ -0,0 +1,10 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import Slides from './Slides';
import './index.css';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<Slides />
</React.StrictMode>,
);
+22
View File
@@ -0,0 +1,22 @@
import { registerSynthSounds, samples } from '@strudel.cycles/webaudio';
import { registerSoundfonts } from '@strudel.cycles/soundfonts';
export async function prebake() {
// https://archive.org/details/SalamanderGrandPianoV3
// License: CC-by http://creativecommons.org/licenses/by/3.0/ Author: Alexander Holm
await Promise.all([
registerSynthSounds(),
registerSoundfonts(`./samples/webaudiofontdata/sound/`),
/* samples(`./piano.json`, `./piano/`, { prebake: true }),
samples(`./vcsl.json`, 'github:sgossner/VCSL/master/', { prebake: true }),
*/
//samples(`./tidal-drum-machines.json`, 'github:ritchse/tidal-drum-machines/main/machines/', {
samples(`./tidal-drum-machines.json`, './samples/tidal-drum-machines/machines/', {
prebake: true,
tag: 'drum-machines',
}) /*
samples(`./EmuSP12.json`, `./EmuSP12/`, { prebake: true, tag: 'drum-machines' }), */,
// samples('github:tidalcycles/Dirt-Samples/master'),
samples('./Dirt-Samples.json', './samples/Dirt-Samples/'),
]);
}
+6
View File
@@ -0,0 +1,6 @@
# What's bad
- Dynamic Typing in JS
- Fractions and Hot Paths
- Verbose function composition
- Method-chaining tradeoffs
+7
View File
@@ -0,0 +1,7 @@
<div className="flex flex-col justify-between h-full overflow-hidden max-h-screen space-y-8">
<h2 className="m-0">Strudel: live coding patterns on the Web</h2>
<div className="justify-center flex overflow-hidden not-prose">
<img src="./img/strudel_icon.png" className="h-full" />
</div>
<span>Felix Roos, Alex McLean</span>
</div>
+65
View File
@@ -0,0 +1,65 @@
import { SlideRepl } from '../SlideRepl.jsx';
# CSound Example
## Loading ORC instruments
<SlideRepl
tune={`// livecode.orc by Steven Yi
await loadOrc('github:kunstmusik/csound-live-code/master/livecode.orc')
stack(
note("<C^7 A7 Dm7 Fm7>/2".voicings('lefthand'))
.cutoff(sine.range(500,2000).round().slow(16))
.euclidLegato(3,8).csound('FM1'),
note("<C2 A1 D2 F2>/2").ply(8).csound('Bass')
.gain("1 4 1 4"),
note("0 7 [4 3] 2".fast(2/3)
.off(".25 .125", add("<2 4 -3 -1>"))
.slow(2).scale('A4 minor'))
.legato(.25).csound('SynHarp'),
s("bd*2,[~ hh]*2,~ cp").bank('RolandTR909')
)`}
hideHeader
/>
## Synthesis
<SlideRepl
tune={`await loadCsound\`
instr CoolSynth
iduration = p3
ifreq = p4
igain = p5
ioct = octcps(ifreq)
kpwm = oscili(.05, 8)
asig = vco2(igain, ifreq, 4, .5 + kpwm)
asig += vco2(igain, ifreq * 2)
idepth = 2
acut = transegr:a(0, .005, 0, idepth, .06, -4.2, 0.001, .01, -4.2, 0) ; filter envelope
asig = zdf_2pole(asig, cpsoct(ioct + acut + 2), 0.5)
iattack = .01
isustain = .5
idecay = .1
irelease = .1
asig *= linsegr:a(0, iattack, 1, idecay, isustain, iduration, isustain, irelease, 0)
out(asig, asig)
endin\`
"<0 2 [4 6](3,4,2) 3\*2>"
.off(1/4, add(2))
.off(1/2, add(6))
.scale('D minor')
.note()
//.pianoroll()
.csound('CoolSynth')`}
hideHeader
/>
+1
View File
@@ -0,0 +1 @@
# Strudel Docs
+4
View File
@@ -0,0 +1,4 @@
# Thanks
- ICLC organizers
- Tidal Community
@@ -0,0 +1,7 @@
import { SlideRepl } from '../SlideRepl.jsx';
import Highlight from '../Highlight.jsx';
import EventsDemo from '../EventsDemo.jsx';
# Webaudio Output: Values -> Sound
<EventsDemo />
+15
View File
@@ -0,0 +1,15 @@
import { SlideRepl } from '../SlideRepl.jsx';
# Pattern Example
<SlideRepl
tune={`"<0 2 [4 6](3,4,1) 3>"
.off(1/4, add(2))
.off(1/2, add(6))
.scale('D minor')
.legato(.25)
.note().s("sawtooth square")
.delay(.8).delaytime(.125)`}
hideHeader
punchcard
/>
+10
View File
@@ -0,0 +1,10 @@
# Future Outlook
- (slow) community growth
- block based evaluation
- live collaboration
- more audio backends
- interface experiments, e.g.:
- alternative syntax, visual notations
- time scrubbing
- ui controls
+9
View File
@@ -0,0 +1,9 @@
# What's good
- Zero-install, multi-platform
- works (surprisingly) well on mobile devices
- visual feedback good for teaching + learning
- loads of potential contributors
- instant pattern sharing
- two-way flow of features between strudel and tidal
- full mini-notation in hydra and gibber
+3
View File
@@ -0,0 +1,3 @@
# The journey
![ports](./img/ports.png)
+3
View File
@@ -0,0 +1,3 @@
import SyntaxComparison from '../SyntaxComparison.jsx';
<SyntaxComparison />
@@ -0,0 +1,5 @@
import { SlideRepl } from '../SlideRepl.jsx';
import Highlight from '../Highlight.jsx';
import MiniComparison from '../MiniComparison.jsx';
<MiniComparison />
@@ -0,0 +1,29 @@
import Highlight from '../Highlight';
import Stepper from '../Stepper';
# User Code: Mini Notation
<img src="./img/pegflow.png" />
<Stepper
steps={[
<ul className="list-decimal pl-8">
<li>Parse Mini Notation to get AST</li>
<li>walk AST and construct function calls</li>
</ul>,
<div className="space-y-2 py-2">
<Highlight code={`mini('bd [hh sd]')`} />
<span>⬇️ resulting calls</span>
<Highlight
code={`seq(
reify('bd').withLocation([1,1,1], [1,4,4]),
seq(
reify('hh').withLocation([1,5,5], [1,8,8]),
reify('sd').withLocation([1,8,8], [1,10,10]),
)
)`}
/>
<span>➡️ location can be used for highlighting</span>
</div>,
]}
/>
+7
View File
@@ -0,0 +1,7 @@
# What can I pattern?
- Default: Web Audio API
- OSC via osc-js, e.g. SuperDirt
- Csound via the Csound WebAssembly build
- MIDI via WebMIDI
- Serial via WebSerial, e.g. for robot dancing
@@ -0,0 +1,5 @@
import QueryDemo from '../QueryDemo.jsx';
# Querying: From Patterns to Events
<QueryDemo />
@@ -0,0 +1,12 @@
# REPL: Features
- Visual Feedback
- Example shuffler (Hydra-style)
- Tutorial + reference
- Shareable links
- Sound Browser
- Offline Support (+ installable PWA)
- Outputs: Web Audio, MIDI, OSC, CSound, Serial, ..
- Themes
![illegible](./img/illegible.png)
+7
View File
@@ -0,0 +1,7 @@
# REPL - live coding editor
[strudel.tidalcycles.org](https://strudel.tidalcycles.org/)
{/* <iframe className="w-full h-[600px]" src="https://strudel.tidalcycles.org/#c2FtcGxlcyh7CiAgYmQ6IFsnYmQvQlQwQUFEQS53YXYnLCdiZC9CVDBBQUQwLndhdicsJ2JkL0JUMEEwREEud2F2JywnYmQvQlQwQTBEMy53YXYnLCdiZC9CVDBBMEQwLndhdicsJ2JkL0JUMEEwQTcud2F2J10sCiAgc2Q6IFsnc2Qvcnl0bS0wMS1jbGFzc2ljLndhdicsJ3NkL3J5dG0tMDAtaGFyZC53YXYnXSwKICBoaDogWydoaDI3LzAwMF9oaDI3Y2xvc2VkaGgud2F2JywnaGgvMDAwX2hoM2Nsb3NlZGhoLndhdiddLAp9LCAnZ2l0aHViOnRpZGFsY3ljbGVzL0RpcnQtU2FtcGxlcy9tYXN0ZXIvJyk7CnN0YWNrKAogIHMoImJkLFt%2BIDxzZCEzIHNkKDMsNCwyKT5dLGhoKjQiKSAvLyBkcnVtcwogIC5zcGVlZChwZXJsaW4ucmFuZ2UoLjcsMSkpIC8vIHJhbmRvbSBzYW1wbGUgc3BlZWQgdmFyaWF0aW9uCiAgLy8uaHVzaCgpCiAgLCI8YTEgYjEqMiBhMSgzLDgpIGUyPiIgLy8gYmFzc2xpbmUKICAub2ZmKDEvOCx4PT54LmFkZCgxMikuZGVncmFkZUJ5KC41KSkgLy8gcmFuZG9tIG9jdGF2ZSBqdW1wcwogIC5hZGQocGVybGluLnJhbmdlKDAsLjUpKSAvLyByYW5kb20gcGl0Y2ggdmFyaWF0aW9uCiAgLnN1cGVyaW1wb3NlKGFkZCguMDUpKSAvLyBhZGQgc2Vjb25kLCBzbGlnaHRseSBkZXR1bmVkIHZvaWNlCiAgLm4oKSAvLyB3cmFwIGluICJuIgogIC5kZWNheSguMTUpLnN1c3RhaW4oMCkgLy8gbWFrZSBlYWNoIG5vdGUgb2YgZXF1YWwgbGVuZ3RoCiAgLnMoJ3Nhd3Rvb3RoJykgLy8gd2F2ZWZvcm0KICAuZ2FpbiguNCkgLy8gdHVybiBkb3duCiAgLmN1dG9mZihzaW5lLnNsb3coNykucmFuZ2UoMzAwLDUwMDApKSAvLyBhdXRvbWF0ZSBjdXRvZmYKICAvLy5odXNoKCkKICAsIjxBbTchMyA8RW03IEU3YjEzIEVtNyBFYm03YjU%2BPiIudm9pY2luZ3MoJ2xlZnRoYW5kJykgLy8gY2hvcmRzCiAgLnN1cGVyaW1wb3NlKHg9PnguYWRkKC4wNCkpIC8vIGFkZCBzZWNvbmQsIHNsaWdodGx5IGRldHVuZWQgdm9pY2UKICAuYWRkKHBlcmxpbi5yYW5nZSgwLC41KSkgLy8gcmFuZG9tIHBpdGNoIHZhcmlhdGlvbgogIC5uKCkgLy8gd3JhcCBpbiAibiIKICAucygnc2F3dG9vdGgnKSAvLyB3YXZlZm9ybQogIC5nYWluKC4xNikgLy8gdHVybiBkb3duCiAgLmN1dG9mZig1MDApIC8vIGZpeGVkIGN1dG9mZgogIC5hdHRhY2soMSkgLy8gc2xvd2x5IGZhZGUgaW4KICAvLy5odXNoKCkKICAsImE0IGM1IDxlNiBhNj4iLnN0cnVjdCgieCg1LDgpIikKICAuc3VwZXJpbXBvc2UoeD0%2BeC5hZGQoLjA0KSkgLy8gYWRkIHNlY29uZCwgc2xpZ2h0bHkgZGV0dW5lZCB2b2ljZQogIC5hZGQocGVybGluLnJhbmdlKDAsLjUpKSAvLyByYW5kb20gcGl0Y2ggdmFyaWF0aW9uCiAgLm4oKSAvLyB3cmFwIGluICJuIgogIC5kZWNheSguMSkuc3VzdGFpbigwKSAvLyBtYWtlIG5vdGVzIHNob3J0CiAgLnMoJ3RyaWFuZ2xlJykgLy8gd2F2ZWZvcm0KICAuZGVncmFkZUJ5KHBlcmxpbi5yYW5nZSgwLC41KSkgLy8gcmFuZG9tbHkgY29udHJvbGxlZCByYW5kb20gcmVtb3ZhbCA6KQogIC5lY2hvV2l0aCg0LC4xMjUsKHgsbik9PnguZ2FpbiguMTUqMS8obisxKSkpIC8vIGVjaG8gbm90ZXMKICAvLy5odXNoKCkKKQogIC5zbG93KDEuNCk%3D" /> */}
![repl](./img/repl.png)
+5
View File
@@ -0,0 +1,5 @@
# REPL: System Overview
<center>
<img src="./img/strudelflow.png" className="w-[900px]" />
</center>
@@ -0,0 +1,3 @@
# Strudel Docs
![docs](./img/docs.png)
+5
View File
@@ -0,0 +1,5 @@
# Pattern Swatches
fork strudel - publish your own patterns with a fixed version
![swatch](./img/swatch.png)
@@ -0,0 +1,5 @@
import TranspilationDemo from '../TranspilationDemo';
# User Code: Transpilation
<TranspilationDemo />
+14
View File
@@ -0,0 +1,14 @@
import Highlight from '../Highlight.jsx';
# Ways to use Strudel
- "Strudel REPL" at [strudel.tidalcycles.org](https://strudel.tidalcycles.org)
- strudel on [flok.cc](https://flok.cc/) for collaboration
- `@strudel.cycles/*` npm packages for your own projects
- Embeddable [Web Component](https://github.com/tidalcycles/strudel/tree/main/packages/embed#strudelcyclesembed)
- Browser Plugin for [Mastodon](https://github.com/mortuosplango/topfenstrudel) + Discourse Plugin:
<div className="flex">
<img src="./img/topfenstrudel.png" />
<img src="./img/discourse.png" />
</div>
+20
View File
@@ -0,0 +1,20 @@
import { SlideRepl } from '../SlideRepl.jsx';
# REPL: Visual Feedback
Highlighting + Piano Roll
<SlideRepl
hideHeader
punchcard
canvasHeight={300}
fontSize={24}
tune={`// Steve Reich - Piano Phase (1967)
note("E4 F#4 B4 C#5 D5 F#4 E4 C#5 B4 F#4 D5 C#5")
.layer(
x=>x.color('#00ff0050').pan(0).fast(1), // left speaker
x=>x.color('#ff00ff50').pan(1).fast(1.01) // right speaker
)
.slow(2).room(.5)
`}
/>
@@ -0,0 +1,28 @@
import { SlideRepl } from '../SlideRepl.jsx';
# What is Strudel
- TidalCycles faithfully ported to JavaScript
- zero-install live coding environment for the browser
- free & open source, modular, hackable, sharable, embeddable:
<SlideRepl
hideHeader
fontSize={24}
tune={`// froos - freakuency - 2023/03/29
stack(
stack(
s("bd, ~ <rim cp>,~ hh*2").off(1/8, x=>x.speed(2).gain(.5)).fast(2),
s("oh*6").end(.01).speed(.5).room(.5).fast("1 2")
).bank('RolandTR909'),
//
note("[c a f e](<3 5>,8,<0 1>)".add("0,.1").sub(24)).s('sawtooth').lpf(500),
//
"[~ <Cm7 Fm7 Bb7 Db7>]*2".voicings('lefthand').note().legato(.2)
.echoWith(6, .1, (p,n)=>p.add(note(n*.6)))
.lpf(sine.range(300,1000).slow(8)).lpq(6)
.add(note(perlin.range(0,.8))).s('gm_epiano2')
.mask("<0 1>")
).restart("<1@3 1*[4 [8 16]]>")
.slow(2)`}
/>
+12
View File
@@ -0,0 +1,12 @@
import { useEffect } from 'react';
function useEvent(name, onTrigger, useCapture = false) {
useEffect(() => {
document.addEventListener(name, onTrigger, useCapture);
return () => {
document.removeEventListener(name, onTrigger, useCapture);
};
}, [onTrigger]);
}
export default useEvent;
+26
View File
@@ -0,0 +1,26 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./index.html',
'./src/**/*.{js,ts,jsx,tsx,mdx}',
'../../packages/react/src/**/*.{html,js,jsx,md,mdx,ts,tsx}',
],
theme: {
extend: {
colors: {
// codemirror-theme settings
background: 'var(--background)',
lineBackground: 'var(--lineBackground)',
foreground: 'var(--foreground)',
caret: 'var(--caret)',
selection: 'var(--selection)',
selectionMatch: 'var(--selectionMatch)',
gutterBackground: 'var(--gutterBackground)',
gutterForeground: 'var(--gutterForeground)',
gutterBorder: 'var(--gutterBorder)',
lineHighlight: 'var(--lineHighlight)',
},
},
},
plugins: [require('@tailwindcss/typography')],
};
+28
View File
@@ -0,0 +1,28 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import mdx from '@mdx-js/rollup';
import gfm from 'remark-gfm';
import topLevelAwait from 'vite-plugin-top-level-await';
const options = {
// See https://mdxjs.com/advanced/plugins
remarkPlugins: [gfm],
rehypePlugins: [],
};
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
react(),
mdx(options),
topLevelAwait({
// The export name of top-level await promise for each chunk module
promiseExportName: '__tla',
// The function to generate import names of top-level await promise in each chunk module
promiseImportName: (i) => `__tla_${i}`,
}),
],
build: {
target: 'esnext', //browsers can handle the latest ES features
},
});
+3 -1
View File
@@ -2,7 +2,7 @@
export * from './packages/core/index.mjs'; export * from './packages/core/index.mjs';
export * from './packages/csound/index.mjs'; export * from './packages/csound/index.mjs';
export * from './packages/embed/index.mjs'; export * from './packages/embed/index.mjs';
export * from './packages/desktopbridge/index.mjs'; export * from './packages/eval/index.mjs';
export * from './packages/midi/index.mjs'; export * from './packages/midi/index.mjs';
export * from './packages/mini/index.mjs'; export * from './packages/mini/index.mjs';
export * from './packages/osc/index.mjs'; export * from './packages/osc/index.mjs';
@@ -10,6 +10,8 @@ export * from './packages/react/index.mjs';
export * from './packages/serial/index.mjs'; export * from './packages/serial/index.mjs';
export * from './packages/soundfonts/index.mjs'; export * from './packages/soundfonts/index.mjs';
export * from './packages/tonal/index.mjs'; export * from './packages/tonal/index.mjs';
export * from './packages/tone/index.mjs';
export * from './packages/transpiler/index.mjs'; export * from './packages/transpiler/index.mjs';
export * from './packages/webaudio/index.mjs'; export * from './packages/webaudio/index.mjs';
export * from './packages/webdirt/index.mjs';
export * from './packages/xen/index.mjs'; export * from './packages/xen/index.mjs';
+13 -11
View File
@@ -18,6 +18,7 @@
"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",
"deploy": "NODE_DEBUG=gh-pages gh-pages -d out",
"jsdoc": "jsdoc packages/ -c jsdoc.config.json", "jsdoc": "jsdoc packages/ -c 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.config.json",
"lint": "eslint . --ext mjs,js --quiet", "lint": "eslint . --ext mjs,js --quiet",
@@ -52,21 +53,22 @@
"@strudel.cycles/webaudio": "workspace:*", "@strudel.cycles/webaudio": "workspace:*",
"@strudel.cycles/xen": "workspace:*", "@strudel.cycles/xen": "workspace:*",
"acorn": "^8.8.1", "acorn": "^8.8.1",
"dependency-tree": "^9.0.0" "dependency-tree": "^9.0.0",
"vitest": "^0.25.7"
}, },
"devDependencies": { "devDependencies": {
"@tauri-apps/cli": "^1.4.0", "@vitest/ui": "^0.25.7",
"@vitest/ui": "^0.28.0", "c8": "^7.12.0",
"canvas": "^2.11.2", "canvas": "^2.11.0",
"eslint": "^8.39.0", "eslint": "^8.28.0",
"eslint-plugin-import": "^2.27.5", "eslint-plugin-import": "^2.27.5",
"events": "^3.3.0", "events": "^3.3.0",
"jsdoc": "^4.0.2", "gh-pages": "^4.0.0",
"jsdoc": "^3.6.10",
"jsdoc-json": "^2.0.2", "jsdoc-json": "^2.0.2",
"jsdoc-to-markdown": "^8.0.0", "jsdoc-to-markdown": "^7.1.1",
"lerna": "^6.6.1", "lerna": "^6.5.1",
"prettier": "^2.8.8", "prettier": "^2.8.1",
"rollup-plugin-visualizer": "^5.8.1", "rollup-plugin-visualizer": "^5.8.1"
"vitest": "^0.33.0"
} }
} }
-3
View File
@@ -1,3 +0,0 @@
# @strudel/codemirror
This package contains helpers and extensions to use codemirror6. See [vite-vanilla-repl-cm6](../core/examples/vite-vanilla-repl-cm6/main.js) as an example of using it.
-113
View File
@@ -1,113 +0,0 @@
import { defaultKeymap } from '@codemirror/commands';
import { javascript } from '@codemirror/lang-javascript';
import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
import { EditorState } from '@codemirror/state';
import { EditorView, highlightActiveLineGutter, keymap, lineNumbers } from '@codemirror/view';
import { Drawer, repl } from '@strudel.cycles/core';
import { flashField, flash } from './flash.mjs';
import { highlightExtension, highlightMiniLocations } from './highlight.mjs';
import { oneDark } from './themes/one-dark';
// https://codemirror.net/docs/guide/
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, theme = oneDark, root }) {
let state = EditorState.create({
doc: initialCode,
extensions: [
theme,
javascript(),
lineNumbers(),
highlightExtension,
highlightActiveLineGutter(),
syntaxHighlighting(defaultHighlightStyle),
keymap.of(defaultKeymap),
flashField,
EditorView.updateListener.of((v) => onChange(v)),
keymap.of([
{
key: 'Ctrl-Enter',
run: () => onEvaluate(),
},
{
key: 'Ctrl-.',
run: () => onStop(),
},
]),
],
});
return new EditorView({
state,
parent: root,
});
}
export class StrudelMirror {
constructor(options) {
const { root, initialCode = '', onDraw, drawTime = [-2, 2], prebake, ...replOptions } = options;
this.code = initialCode;
this.drawer = new Drawer((haps, time) => {
const currentFrame = haps.filter((hap) => time >= hap.whole.begin && time <= hap.endClipped);
this.highlight(currentFrame, time);
onDraw?.(haps, time, currentFrame);
}, drawTime);
const prebaked = prebake();
prebaked.then(async () => {
if (!onDraw) {
return;
}
const { scheduler, evaluate } = await this.repl;
// draw first frame instantly
prebaked.then(async () => {
await evaluate(this.code, false);
this.drawer.invalidate(scheduler);
onDraw?.(this.drawer.visibleHaps, 0, []);
});
});
this.repl = repl({
...replOptions,
onToggle: async (started) => {
replOptions?.onToggle?.(started);
const { scheduler } = await this.repl;
if (started) {
this.drawer.start(scheduler);
} else {
this.drawer.stop();
}
},
beforeEval: async () => {
await prebaked;
},
afterEval: (options) => {
replOptions?.afterEval?.(options);
this.drawer.invalidate();
},
});
this.editor = initEditor({
root,
initialCode,
onChange: (v) => {
this.code = v.state.doc.toString();
},
onEvaluate: () => this.evaluate(),
onStop: () => this.stop(),
});
}
async evaluate() {
const { evaluate } = await this.repl;
this.flash();
await evaluate(this.code);
}
async stop() {
const { scheduler } = await this.repl;
scheduler.stop();
}
flash(ms) {
flash(this.editor, ms);
}
highlight(haps, time) {
highlightMiniLocations(this.editor.view, time, haps);
}
}
-35
View File
@@ -1,35 +0,0 @@
import { StateEffect, StateField } from '@codemirror/state';
import { Decoration, EditorView } from '@codemirror/view';
export const setFlash = StateEffect.define();
export const flashField = StateField.define({
create() {
return Decoration.none;
},
update(flash, tr) {
try {
for (let e of tr.effects) {
if (e.is(setFlash)) {
if (e.value && tr.newDoc.length > 0) {
const mark = Decoration.mark({ attributes: { style: `background-color: #FFCA2880` } });
flash = Decoration.set([mark.range(0, tr.newDoc.length)]);
} else {
flash = Decoration.set([]);
}
}
}
return flash;
} catch (err) {
console.warn('flash error', err);
return flash;
}
},
provide: (f) => EditorView.decorations.from(f),
});
export const flash = (view, ms = 200) => {
view.dispatch({ effects: setFlash.of(true) });
setTimeout(() => {
view.dispatch({ effects: setFlash.of(false) });
}, ms);
};
-126
View File
@@ -1,126 +0,0 @@
import { RangeSetBuilder, StateEffect, StateField } from '@codemirror/state';
import { Decoration, EditorView } from '@codemirror/view';
export const setMiniLocations = StateEffect.define();
export const showMiniLocations = StateEffect.define();
export const updateMiniLocations = (view, locations) => {
view.dispatch({ effects: setMiniLocations.of(locations) });
};
export const highlightMiniLocations = (view, atTime, haps) => {
view.dispatch({ effects: showMiniLocations.of({ atTime, haps }) });
};
const miniLocations = StateField.define({
create() {
return Decoration.none;
},
update(locations, tr) {
if (tr.docChanged) {
locations = locations.map(tr.changes);
}
for (let e of tr.effects) {
if (e.is(setMiniLocations)) {
// this is called on eval, with the mini locations obtained from the transpiler
// codemirror will automatically remap the marks when the document is edited
// create a mark for each mini location, adding the range to the spec to find it later
const marks = e.value
.filter(([from]) => from < tr.newDoc.length)
.map(([from, to]) => [from, Math.min(to, tr.newDoc.length)])
.map(
(range) =>
Decoration.mark({
id: range.join(':'),
// this green is only to verify that the decoration moves when the document is edited
// it will be removed later, so the mark is not visible by default
attributes: { style: `background-color: #00CA2880` },
}).range(...range), // -> Decoration
);
locations = Decoration.set(marks, true); // -> DecorationSet === RangeSet<Decoration>
}
}
return locations;
},
});
const visibleMiniLocations = StateField.define({
create() {
return { atTime: 0, haps: new Map() };
},
update(visible, tr) {
for (let e of tr.effects) {
if (e.is(showMiniLocations)) {
// this is called every frame to show the locations that are currently active
// we can NOT create new marks because the context.locations haven't changed since eval time
// this is why we need to find a way to update the existing decorations, showing the ones that have an active range
const haps = new Map();
for (let hap of e.value.haps) {
for (let { start, end } of hap.context.locations) {
let id = `${start}:${end}`;
if (!haps.has(id) || haps.get(id).whole.begin.lt(hap.whole.begin)) {
haps.set(id, hap);
}
}
}
visible = { atTime: e.value.atTime, haps };
}
}
return visible;
},
});
// // Derive the set of decorations from the miniLocations and visibleLocations
const miniLocationHighlights = EditorView.decorations.compute([miniLocations, visibleMiniLocations], (state) => {
const iterator = state.field(miniLocations).iter();
const { haps } = state.field(visibleMiniLocations);
const builder = new RangeSetBuilder();
while (iterator.value) {
const {
from,
to,
value: {
spec: { id },
},
} = iterator;
if (haps.has(id)) {
const hap = haps.get(id);
const color = hap.context.color ?? 'var(--foreground)';
// Get explicit channels for color values
/*
const swatch = document.createElement('div');
swatch.style.color = color;
document.body.appendChild(swatch);
let channels = getComputedStyle(swatch)
.color.match(/^rgba?\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})(?:,\s*(\d*(?:\.\d+)?))?\)$/)
.slice(1)
.map((c) => parseFloat(c || 1));
document.body.removeChild(swatch);
// Get percentage of event
const percent = 1 - (atTime - hap.whole.begin) / hap.whole.duration;
channels[3] *= percent;
*/
builder.add(
from,
to,
Decoration.mark({
// attributes: { style: `outline: solid 2px rgba(${channels.join(', ')})` },
attributes: { style: `outline: solid 2px ${color}` },
}),
);
}
iterator.next();
}
return builder.finish();
});
export const highlightExtension = [miniLocations, visibleMiniLocations, miniLocationHighlights];
-3
View File
@@ -1,3 +0,0 @@
export * from './codemirror.mjs';
export * from './highlight.mjs';
export * from './flash.mjs';
-139
View File
@@ -1,139 +0,0 @@
import { EditorView } from '@codemirror/view';
import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
import { tags as t } from '@lezer/highlight';
// Using https://github.com/one-dark/vscode-one-dark-theme/ as reference for the colors
const chalky = '#e5c07b',
coral = '#e06c75',
cyan = '#56b6c2',
invalid = '#ffffff',
ivory = '#abb2bf',
stone = '#7d8799', // Brightened compared to original to increase contrast
malibu = '#61afef',
sage = '#98c379',
whiskey = '#d19a66',
violet = '#c678dd',
darkBackground = '#21252b',
highlightBackground = '#2c313a',
background = '#282c34',
tooltipBackground = '#353a42',
selection = '#3E4451',
cursor = '#528bff';
/// The colors used in the theme, as CSS color strings.
export const color = {
chalky,
coral,
cyan,
invalid,
ivory,
stone,
malibu,
sage,
whiskey,
violet,
darkBackground,
highlightBackground,
background,
tooltipBackground,
selection,
cursor,
};
/// The editor theme styles for One Dark.
export const oneDarkTheme = EditorView.theme(
{
'&': {
color: ivory,
backgroundColor: background,
},
'.cm-content': {
caretColor: cursor,
},
'.cm-cursor, .cm-dropCursor': { borderLeftColor: cursor },
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection':
{ backgroundColor: selection },
'.cm-panels': { backgroundColor: darkBackground, color: ivory },
'.cm-panels.cm-panels-top': { borderBottom: '2px solid black' },
'.cm-panels.cm-panels-bottom': { borderTop: '2px solid black' },
'.cm-searchMatch': {
backgroundColor: '#72a1ff59',
outline: '1px solid #457dff',
},
'.cm-searchMatch.cm-searchMatch-selected': {
backgroundColor: '#6199ff2f',
},
'.cm-activeLine': { backgroundColor: '#6699ff0b' },
'.cm-selectionMatch': { backgroundColor: '#aafe661a' },
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
backgroundColor: '#bad0f847',
},
'.cm-gutters': {
backgroundColor: background,
color: stone,
border: 'none',
},
'.cm-activeLineGutter': {
backgroundColor: highlightBackground,
},
'.cm-foldPlaceholder': {
backgroundColor: 'transparent',
border: 'none',
color: '#ddd',
},
'.cm-tooltip': {
border: 'none',
backgroundColor: tooltipBackground,
},
'.cm-tooltip .cm-tooltip-arrow:before': {
borderTopColor: 'transparent',
borderBottomColor: 'transparent',
},
'.cm-tooltip .cm-tooltip-arrow:after': {
borderTopColor: tooltipBackground,
borderBottomColor: tooltipBackground,
},
'.cm-tooltip-autocomplete': {
'& > ul > li[aria-selected]': {
backgroundColor: highlightBackground,
color: ivory,
},
},
},
{ dark: true },
);
/// The highlighting style for code in the One Dark theme.
export const oneDarkHighlightStyle = HighlightStyle.define([
{ tag: t.keyword, color: violet },
{ tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName], color: coral },
{ tag: [t.function(t.variableName), t.labelName], color: malibu },
{ tag: [t.color, t.constant(t.name), t.standard(t.name)], color: whiskey },
{ tag: [t.definition(t.name), t.separator], color: ivory },
{ tag: [t.typeName, t.className, t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: chalky },
{ tag: [t.operator, t.operatorKeyword, t.url, t.escape, t.regexp, t.link, t.special(t.string)], color: cyan },
{ tag: [t.meta, t.comment], color: stone },
{ tag: t.strong, fontWeight: 'bold' },
{ tag: t.emphasis, fontStyle: 'italic' },
{ tag: t.strikethrough, textDecoration: 'line-through' },
{ tag: t.link, color: stone, textDecoration: 'underline' },
{ tag: t.heading, fontWeight: 'bold', color: coral },
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: whiskey },
{ tag: [t.processingInstruction, t.string, t.inserted], color: sage },
{ tag: t.invalid, color: invalid },
]);
/// Extension to enable the One Dark theme (both the editor theme and
/// the highlight style).
export const oneDark = [oneDarkTheme, syntaxHighlighting(oneDarkHighlightStyle)];
+1 -2
View File
@@ -1,5 +1,4 @@
import { Pattern, getDrawContext, silence, register, pure } from './index.mjs'; import { controls, Pattern, getDrawContext, silence, register, pure } from './index.mjs';
import controls from './controls.mjs'; // do not import from index.mjs as it breaks for some reason..
const { createParams } = controls; const { createParams } = controls;
let clearColor = '#22222210'; let clearColor = '#22222210';
+19 -429
View File
@@ -4,7 +4,7 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
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/>. 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/>.
*/ */
import { Pattern, register, sequence } from './pattern.mjs'; import { Pattern, sequence } from './pattern.mjs';
import { zipWith } from './util.mjs'; import { zipWith } from './util.mjs';
const controls = {}; const controls = {};
@@ -109,97 +109,6 @@ const generic_params = [
*/ */
['attack', 'att'], ['attack', 'att'],
/**
* Sets the Frequency Modulation Harmonicity Ratio.
* Controls the timbre of the sound.
* Whole numbers and simple ratios sound more natural,
* while decimal numbers and complex ratios sound metallic.
*
* @name fmh
* @param {number | Pattern} harmonicity
* @example
* note("c e g b")
* .fm(4)
* .fmh("<1 2 1.5 1.61>")
* .scope()
*
*/
[['fmh', 'fmi'], 'fmh'],
/**
* Sets the Frequency Modulation of the synth.
* Controls the modulation index, which defines the brightness of the sound.
*
* @name fm
* @param {number | Pattern} brightness modulation index
* @synonyms fmi
* @example
* note("c e g b")
* .fm("<0 1 2 8 32>")
* .scope()
*
*/
[['fmi', 'fmh'], 'fm'],
// fm envelope
/**
* Ramp type of fm envelope. Exp might be a bit broken..
*
* @name fmenv
* @param {number | Pattern} type lin | exp
* @example
* note("c e g b")
* .fm(4)
* .fmdecay(.2)
* .fmsustain(0)
* .fmenv("<exp lin>")
* .scope()
*
*/
['fmenv'],
/**
* Attack time for the FM envelope: time it takes to reach maximum modulation
*
* @name fmattack
* @param {number | Pattern} time attack time
* @example
* note("c e g b")
* .fm(4)
* .fmattack("<0 .05 .1 .2>")
* .scope()
*
*/
['fmattack'],
/**
* Decay time for the FM envelope: seconds until the sustain level is reached after the attack phase.
*
* @name fmdecay
* @param {number | Pattern} time decay time
* @example
* note("c e g b")
* .fm(4)
* .fmdecay("<.01 .05 .1 .2>")
* .fmsustain(.4)
* .scope()
*
*/
['fmdecay'],
/**
* Sustain level for the FM envelope: how much modulation is applied after the decay phase
*
* @name fmsustain
* @param {number | Pattern} level sustain level
* @example
* note("c e g b")
* .fm(4)
* .fmdecay(.1)
* .fmsustain("<1 .75 .5 0>")
* .scope()
*
*/
['fmsustain'],
// these are not really useful... skipping for now
['fmrelease'],
['fmvelocity'],
/** /**
* Select the sound bank to use. To be used together with `s`. The bank name (+ "_") will be prepended to the value of `s`. * Select the sound bank to use. To be used together with `s`. The bank name (+ "_") will be prepended to the value of `s`.
* *
@@ -211,9 +120,6 @@ const generic_params = [
*/ */
['bank'], ['bank'],
['analyze'], // analyser node send amount 0 - 1 (used by scope)
['fft'], // fftSize of analyser
/** /**
* Amplitude envelope decay time: the time it takes after the attack time to reach the sustain level. * Amplitude envelope decay time: the time it takes after the attack time to reach the sustain level.
* Note that the decay is only audible if the sustain value is lower than 1. * Note that the decay is only audible if the sustain value is lower than 1.
@@ -299,43 +205,29 @@ const generic_params = [
*/ */
['end'], ['end'],
/** /**
* Loops the sample. * Loops the sample (from `begin` to `end`) the specified number of times.
* Note that the tempo of the loop is not synced with the cycle tempo. * Note that the tempo of the loop is not synced with the cycle tempo.
* To change the loop region, use loopBegin / loopEnd.
* *
* @name loop * @name loop
* @param {number | Pattern} on If 1, the sample is looped * @param {number | Pattern} times How often the sample is looped
* @example * @example
* s("casio").loop(1) * s("bd").loop("<1 2 3 4>").osc()
* *
*/ */
['loop'], ['loop'],
// TODO: currently duplicated with "native" legato
// TODO: superdirt legato will do more: https://youtu.be/dQPmE1WaD1k?t=419
/** /**
* Begin to loop at a specific point in the sample (inbetween `begin` and `end`). * a pattern of numbers from 0 to 1. Skips the beginning of each sample, e.g. `0.25` to cut off the first quarter from each sample.
* Note that the loop point must be inbetween `begin` and `end`, and before `loopEnd`!
* Note: Samples starting with wt_ will automatically loop! (wt = wavetable)
* *
* @name loopBegin * @name legato
* @param {number | Pattern} time between 0 and 1, where 1 is the length of the sample * @param {number | Pattern} duration between 0 and 1, where 1 is the length of the whole hap time
* @synonyms loopb
* @example * @example
* s("space").loop(1) * "c4 eb4 g4 bb4".legato("<0.125 .25 .5 .75 1 2 4>")
* .loopBegin("<0 .125 .25>").scope()
*/
['loopBegin', 'loopb'],
/**
* *
* End the looping section at a specific point in the sample (inbetween `begin` and `end`).
* Note that the loop point must be inbetween `begin` and `end`, and after `loopBegin`!
*
* @name loopEnd
* @param {number | Pattern} time between 0 and 1, where 1 is the length of the sample
* @synonyms loope
* @example
* s("space").loop(1)
* .loopEnd("<1 .75 .5 .25>").scope()
*/ */
['loopEnd', 'loope'], // ['legato'],
// ['clhatdecay'],
/** /**
* bit crusher effect. * bit crusher effect.
* *
@@ -345,20 +237,6 @@ const generic_params = [
* s("<bd sd>,hh*3").fast(2).crush("<16 8 7 6 5 4 3 2>") * s("<bd sd>,hh*3").fast(2).crush("<16 8 7 6 5 4 3 2>")
* *
*/ */
// TODO: currently duplicated with "native" legato
// TODO: superdirt legato will do more: https://youtu.be/dQPmE1WaD1k?t=419
/**
* a pattern of numbers from 0 to 1. Skips the beginning of each sample, e.g. `0.25` to cut off the first quarter from each sample.
*
* @name legato
* @param {number | Pattern} duration between 0 and 1, where 1 is the length of the whole hap time
* @noAutocomplete
* @example
* "c4 eb4 g4 bb4".legato("<0.125 .25 .5 .75 1 2 4>")
*
*/
// ['legato'],
// ['clhatdecay'],
['crush'], ['crush'],
/** /**
* fake-resampling for lowering the sample rate. Caution: This effect seems to only work in chromium based browsers * fake-resampling for lowering the sample rate. Caution: This effect seems to only work in chromium based browsers
@@ -370,6 +248,7 @@ const generic_params = [
* *
*/ */
['coarse'], ['coarse'],
/** /**
* choose the channel the pattern is sent to in superdirt * choose the channel the pattern is sent to in superdirt
* *
@@ -403,227 +282,6 @@ const generic_params = [
* *
*/ */
[['cutoff', 'resonance'], 'ctf', 'lpf', 'lp'], [['cutoff', 'resonance'], 'ctf', 'lpf', 'lp'],
/**
* Sets the lowpass filter envelope modulation depth.
* @name lpenv
* @param {number | Pattern} modulation depth of the lowpass filter envelope between 0 and _n_
* @synonyms lpe
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .lpf(500)
* .lpa(.5)
* .lpenv("<4 2 1 0 -1 -2 -4>/4")
*/
['lpenv', 'lpe'],
/**
* Sets the highpass filter envelope modulation depth.
* @name hpenv
* @param {number | Pattern} modulation depth of the highpass filter envelope between 0 and _n_
* @synonyms hpe
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .hpf(500)
* .hpa(.5)
* .hpenv("<4 2 1 0 -1 -2 -4>/4")
*/
['hpenv', 'hpe'],
/**
* Sets the bandpass filter envelope modulation depth.
* @name bpenv
* @param {number | Pattern} modulation depth of the bandpass filter envelope between 0 and _n_
* @synonyms bpe
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .bpf(500)
* .bpa(.5)
* .bpenv("<4 2 1 0 -1 -2 -4>/4")
*/
['bpenv', 'bpe'],
/**
* Sets the attack duration for the lowpass filter envelope.
* @name lpattack
* @param {number | Pattern} attack time of the filter envelope
* @synonyms lpa
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .lpf(500)
* .lpa("<.5 .25 .1 .01>/4")
* .lpenv(4)
*/
['lpattack', 'lpa'],
/**
* Sets the attack duration for the highpass filter envelope.
* @name hpattack
* @param {number | Pattern} attack time of the highpass filter envelope
* @synonyms hpa
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .hpf(500)
* .hpa("<.5 .25 .1 .01>/4")
* .hpenv(4)
*/
['hpattack', 'hpa'],
/**
* Sets the attack duration for the bandpass filter envelope.
* @name bpattack
* @param {number | Pattern} attack time of the bandpass filter envelope
* @synonyms bpa
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .bpf(500)
* .bpa("<.5 .25 .1 .01>/4")
* .bpenv(4)
*/
['bpattack', 'bpa'],
/**
* Sets the decay duration for the lowpass filter envelope.
* @name lpdecay
* @param {number | Pattern} decay time of the filter envelope
* @synonyms lpd
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .lpf(500)
* .lpd("<.5 .25 .1 0>/4")
* .lps(0.2)
* .lpenv(4)
*/
['lpdecay', 'lpd'],
/**
* Sets the decay duration for the highpass filter envelope.
* @name hpdecay
* @param {number | Pattern} decay time of the highpass filter envelope
* @synonyms hpd
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .hpf(500)
* .hpd("<.5 .25 .1 0>/4")
* .hps(0.2)
* .hpenv(4)
*/
['hpdecay', 'hpd'],
/**
* Sets the decay duration for the bandpass filter envelope.
* @name bpdecay
* @param {number | Pattern} decay time of the bandpass filter envelope
* @synonyms bpd
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .bpf(500)
* .bpd("<.5 .25 .1 0>/4")
* .bps(0.2)
* .bpenv(4)
*/
['bpdecay', 'bpd'],
/**
* Sets the sustain amplitude for the lowpass filter envelope.
* @name lpsustain
* @param {number | Pattern} sustain amplitude of the lowpass filter envelope
* @synonyms lps
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .lpf(500)
* .lpd(.5)
* .lps("<0 .25 .5 1>/4")
* .lpenv(4)
*/
['lpsustain', 'lps'],
/**
* Sets the sustain amplitude for the highpass filter envelope.
* @name hpsustain
* @param {number | Pattern} sustain amplitude of the highpass filter envelope
* @synonyms hps
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .hpf(500)
* .hpd(.5)
* .hps("<0 .25 .5 1>/4")
* .hpenv(4)
*/
['hpsustain', 'hps'],
/**
* Sets the sustain amplitude for the bandpass filter envelope.
* @name bpsustain
* @param {number | Pattern} sustain amplitude of the bandpass filter envelope
* @synonyms bps
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .bpf(500)
* .bpd(.5)
* .bps("<0 .25 .5 1>/4")
* .bpenv(4)
*/
['bpsustain', 'bps'],
/**
* Sets the release time for the lowpass filter envelope.
* @name lprelease
* @param {number | Pattern} release time of the filter envelope
* @synonyms lpr
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .clip(.5)
* .lpf(500)
* .lpenv(4)
* .lpr("<.5 .25 .1 0>/4")
* .release(.5)
*/
['lprelease', 'lpr'],
/**
* Sets the release time for the highpass filter envelope.
* @name hprelease
* @param {number | Pattern} release time of the highpass filter envelope
* @synonyms hpr
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .clip(.5)
* .hpf(500)
* .hpenv(4)
* .hpr("<.5 .25 .1 0>/4")
* .release(.5)
*/
['hprelease', 'hpr'],
/**
* Sets the release time for the bandpass filter envelope.
* @name bprelease
* @param {number | Pattern} release time of the bandpass filter envelope
* @synonyms bpr
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .clip(.5)
* .bpf(500)
* .bpenv(4)
* .bpr("<.5 .25 .1 0>/4")
* .release(.5)
*/
['bprelease', 'bpr'],
/**
* Sets the filter type. The 24db filter is more aggressive. More types might be added in the future.
* @name ftype
* @param {number | Pattern} type 12db (default) or 24db
* @example
* note("<c2 e2 f2 g2>")
* .sound('sawtooth')
* .lpf(500)
* .bpenv(4)
* .ftype("<12db 24db>")
*/
['ftype'],
['fanchor'],
/** /**
* Applies the cutoff frequency of the **h**igh-**p**ass **f**ilter. * Applies the cutoff frequency of the **h**igh-**p**ass **f**ilter.
* *
@@ -640,36 +298,6 @@ const generic_params = [
*/ */
// currently an alias of 'hcutoff' https://github.com/tidalcycles/strudel/issues/496 // currently an alias of 'hcutoff' https://github.com/tidalcycles/strudel/issues/496
// ['hpf'], // ['hpf'],
/**
* Applies a vibrato to the frequency of the oscillator.
*
* @name vib
* @synonyms vibrato, v
* @param {number | Pattern} frequency of the vibrato in hertz
* @example
* note("a")
* .vib("<.5 1 2 4 8 16>")
* @example
* // change the modulation depth with ":"
* note("a")
* .vib("<.5 1 2 4 8 16>:12")
*/
[['vib', 'vibmod'], 'vibrato', 'v'],
/**
* Sets the vibrato depth in semitones. Only has an effect if `vibrato` | `vib` | `v` is is also set
*
* @name vibmod
* @synonyms vmod
* @param {number | Pattern} depth of vibrato (in semitones)
* @example
* note("a").vib(4)
* .vibmod("<.25 .5 1 2 12>")
* @example
* // change the vibrato frequency with ":"
* note("a")
* .vibmod("<.25 .5 1 2 12>:8")
*/
[['vibmod', 'vib'], 'vmod'],
[['hcutoff', 'hresonance'], 'hpf', 'hp'], [['hcutoff', 'hresonance'], 'hpf', 'hp'],
/** /**
* Controls the **h**igh-**p**ass **q**-value. * Controls the **h**igh-**p**ass **q**-value.
@@ -848,9 +476,6 @@ const generic_params = [
* *
*/ */
['lsize'], ['lsize'],
// label for pianoroll
['activeLabel'],
[['label', 'activeLabel']],
// ['lfo'], // ['lfo'],
// ['lfocutoffint'], // ['lfocutoffint'],
// ['lfodelay'], // ['lfodelay'],
@@ -883,7 +508,7 @@ const generic_params = [
* @superDirtOnly * @superDirtOnly
*/ */
['octave'], ['octave'],
['offset'], // TODO: what is this? not found in tidal doc
// ['ophatdecay'], // ['ophatdecay'],
// TODO: example // TODO: example
/** /**
@@ -947,15 +572,6 @@ const generic_params = [
// TODO: dedup with synth param, see https://tidalcycles.org/docs/reference/synthesizers/#superpiano // TODO: dedup with synth param, see https://tidalcycles.org/docs/reference/synthesizers/#superpiano
// ['velocity'], // ['velocity'],
['voice'], // TODO: synth param ['voice'], // TODO: synth param
// voicings // https://github.com/tidalcycles/strudel/issues/506
['chord'], // chord to voice, like C Eb Fm7 G7. the symbols can be defined via addVoicings
['dictionary', 'dict'], // which dictionary to use for the voicings
['anchor'], // the top note to align the voicing to, defaults to c5
['offset'], // how the voicing is offset from the anchored position
['octaves'], // how many octaves are voicing steps spread apart, defaults to 1
[['mode', 'anchor']], // below = anchor note will be removed from the voicing, useful for melody harmonization
/** /**
* Sets the level of reverb. * Sets the level of reverb.
* *
@@ -1133,33 +749,18 @@ const generic_params = [
['val'], ['val'],
['cps'], ['cps'],
/** /**
* Multiplies the duration with the given number. Also cuts samples off at the end if they exceed the duration. * If set to 1, samples will be cut to the duration of their event.
* In tidal, this would be done with legato, [which has a complicated history in strudel](https://github.com/tidalcycles/strudel/issues/111). * In tidal, this would be done with legato, which [is about to land in strudel too](https://github.com/tidalcycles/strudel/issues/111)
* For now, if you're coming from tidal, just think clip = legato.
* *
* @name clip * @name clip
* @param {number | Pattern} factor >= 0 * @param {number | Pattern} active 1 or 0
* @example * @example
* note("c a f e").s("piano").clip("<.5 1 2>") * note("c a f e ~").s("piano").clip(1)
* *
*/ */
['clip'], ['clip'],
// ZZFX
['zrand'],
['curve'],
['slide'], // superdirt duplicate
['deltaSlide'],
['pitchJump'],
['pitchJumpTime'],
['lfo', 'repeatTime'],
['noise'],
['zmod'],
['zcrush'], // like crush but scaled differently
['zdelay'],
['tremolo'],
['zzfx'],
]; ];
// TODO: slice / splice https://www.youtube.com/watch?v=hKhPdO0RKDQ&list=PL2lW1zNIIwj3bDkh-Y3LUGDuRcoUigoDs&index=13 // TODO: slice / splice https://www.youtube.com/watch?v=hKhPdO0RKDQ&list=PL2lW1zNIIwj3bDkh-Y3LUGDuRcoUigoDs&index=13
controls.createParam = function (names) { controls.createParam = function (names) {
@@ -1209,15 +810,4 @@ generic_params.forEach(([names, ...aliases]) => {
controls.createParams = (...names) => controls.createParams = (...names) =>
names.reduce((acc, name) => Object.assign(acc, { [name]: controls.createParam(name) }), {}); names.reduce((acc, name) => Object.assign(acc, { [name]: controls.createParam(name) }), {});
controls.adsr = register('adsr', (adsr, pat) => {
adsr = !Array.isArray(adsr) ? [adsr] : adsr;
const [attack, decay, sustain, release] = adsr;
return pat.set({ attack, decay, sustain, release });
});
controls.ds = register('ds', (ds, pat) => {
ds = !Array.isArray(ds) ? [ds] : ds;
const [decay, sustain] = ds;
return pat.set({ decay, sustain });
});
export default controls; export default controls;
+6 -112
View File
@@ -1,6 +1,6 @@
/* /*
draw.mjs - <short description TODO> draw.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/draw.mjs> Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/tone/draw.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/>. 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/>.
*/ */
@@ -9,29 +9,17 @@ import { Pattern, getTime, State, TimeSpan } from './index.mjs';
export const getDrawContext = (id = 'test-canvas') => { export const getDrawContext = (id = 'test-canvas') => {
let canvas = document.querySelector('#' + id); let canvas = document.querySelector('#' + id);
if (!canvas) { if (!canvas) {
const scale = 2; // 2 = crisp on retina screens
canvas = document.createElement('canvas'); canvas = document.createElement('canvas');
canvas.id = id; canvas.id = id;
canvas.width = window.innerWidth * scale; canvas.width = window.innerWidth;
canvas.height = window.innerHeight * scale; canvas.height = window.innerHeight;
canvas.style = 'pointer-events:none;width:100%;height:100%;position:fixed;top:0;left:0'; canvas.style = 'pointer-events:none;width:100%;height:100%;position:fixed;top:0;left:0;z-index:5';
document.body.prepend(canvas); document.body.prepend(canvas);
let timeout;
window.addEventListener('resize', () => {
timeout && clearTimeout(timeout);
timeout = setTimeout(() => {
canvas.width = window.innerWidth * scale;
canvas.height = window.innerHeight * scale;
}, 200);
});
} }
return canvas.getContext('2d'); return canvas.getContext('2d');
}; };
Pattern.prototype.draw = function (callback, { from, to, onQuery } = {}) { Pattern.prototype.draw = function (callback, { from, to, onQuery }) {
if (typeof window === 'undefined') {
return this;
}
if (window.strudelAnimation) { if (window.strudelAnimation) {
cancelAnimationFrame(window.strudelAnimation); cancelAnimationFrame(window.strudelAnimation);
} }
@@ -63,7 +51,7 @@ Pattern.prototype.draw = function (callback, { from, to, onQuery } = {}) {
export const cleanupDraw = (clearScreen = true) => { export const cleanupDraw = (clearScreen = true) => {
const ctx = getDrawContext(); const ctx = getDrawContext();
clearScreen && ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.width); clearScreen && ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
if (window.strudelAnimation) { if (window.strudelAnimation) {
cancelAnimationFrame(window.strudelAnimation); cancelAnimationFrame(window.strudelAnimation);
} }
@@ -77,97 +65,3 @@ Pattern.prototype.onPaint = function (onPaint) {
this.context = { onPaint }; this.context = { onPaint };
return this; return this;
}; };
// const round = (x) => Math.round(x * 1000) / 1000;
// encapsulates starting and stopping animation frames
export class Framer {
constructor(onFrame, onError) {
this.onFrame = onFrame;
this.onError = onError;
}
start() {
const self = this;
let frame = requestAnimationFrame(function updateHighlights(time) {
try {
self.onFrame(time);
} catch (err) {
self.onError(err);
}
frame = requestAnimationFrame(updateHighlights);
});
self.cancel = () => {
cancelAnimationFrame(frame);
};
}
stop() {
if (this.cancel) {
this.cancel();
}
}
}
// syncs animation frames to a cyclist scheduler
// see vite-vanilla-repl-cm6 for an example
export class Drawer {
constructor(onDraw, drawTime) {
let [lookbehind, lookahead] = drawTime; // e.g. [-2, 2]
lookbehind = Math.abs(lookbehind);
this.visibleHaps = [];
this.lastFrame = null;
this.drawTime = drawTime;
this.framer = new Framer(
() => {
if (!this.scheduler) {
console.warn('Drawer: no scheduler');
return;
}
// calculate current frame time (think right side of screen for pianoroll)
const phase = this.scheduler.now() + lookahead;
// first frame just captures the phase
if (this.lastFrame === null) {
this.lastFrame = phase;
return;
}
// query haps from last frame till now. take last 100ms max
const haps = this.scheduler.pattern.queryArc(Math.max(this.lastFrame, phase - 1 / 10), phase);
this.lastFrame = phase;
this.visibleHaps = (this.visibleHaps || [])
// filter out haps that are too far in the past (think left edge of screen for pianoroll)
.filter((h) => h.whole.end >= phase - lookbehind - lookahead)
// add new haps with onset (think right edge bars scrolling in)
.concat(haps.filter((h) => h.hasOnset()));
const time = phase - lookahead;
onDraw(this.visibleHaps, time, this);
},
(err) => {
console.warn('draw error', err);
},
);
}
invalidate(scheduler = this.scheduler) {
if (!scheduler) {
return;
}
this.scheduler = scheduler;
const t = scheduler.now();
let [_, lookahead] = this.drawTime;
const [begin, end] = [Math.max(t, 0), t + lookahead + 0.1];
// remove all future haps
this.visibleHaps = this.visibleHaps.filter((h) => h.whole.begin < t);
// query future haps
const futureHaps = scheduler.pattern.queryArc(begin, end); // +0.1 = workaround for weird holes in query..
// append future haps
this.visibleHaps = this.visibleHaps.concat(futureHaps);
}
start(scheduler) {
this.scheduler = scheduler;
this.invalidate();
this.framer.start();
}
stop() {
if (this.framer) {
this.framer.stop();
}
}
}
+3 -7
View File
@@ -1,6 +1,6 @@
/* /*
evaluate.mjs - <short description TODO> evaluate.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/evaluate.mjs> Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/eval/evaluate.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/>. 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/>.
*/ */
@@ -37,12 +37,8 @@ function safeEval(str, options = {}) {
} }
export const evaluate = async (code, transpiler) => { export const evaluate = async (code, transpiler) => {
let meta = {};
if (transpiler) { if (transpiler) {
// transform syntactically correct js code to semantically usable code code = transpiler(code); // transform syntactically correct js code to semantically usable code
const transpiled = transpiler(code);
code = transpiled.output;
meta = transpiled;
} }
// if no transpiler is given, we expect a single instruction (!wrapExpression) // if no transpiler is given, we expect a single instruction (!wrapExpression)
const options = { wrapExpression: !!transpiler }; const options = { wrapExpression: !!transpiler };
@@ -52,5 +48,5 @@ export const evaluate = async (code, transpiler) => {
const message = `got "${typeof evaluated}" instead of pattern`; const message = `got "${typeof evaluated}" instead of pattern`;
throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.')); throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.'));
} }
return { mode: 'javascript', pattern: evaluated, meta }; return { mode: 'javascript', pattern: evaluated };
}; };
@@ -1,8 +0,0 @@
# vite-vanilla-repl-cm6
This folder demonstrates how to set up a strudel repl using vite and vanilla JS + codemirror. Run it using:
```sh
npm i
npm run dev
```
@@ -1,22 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite Vanilla Strudel REPL</title>
</head>
<body>
<main>
<nav>
<button id="play">eval</button>
<button id="stop">stop</button>
</nav>
<div class="container">
<div id="editor"></div>
<div id="output"></div>
</div>
<canvas id="roll"></canvas>
</main>
<script type="module" src="./main.js"></script>
</body>
</html>
@@ -1,39 +0,0 @@
import { StrudelMirror } from '@strudel/codemirror';
import { funk42 } from './tunes';
import { drawPianoroll, evalScope, controls } from '@strudel.cycles/core';
import './style.css';
import { initAudioOnFirstClick } from '@strudel.cycles/webaudio';
import { transpiler } from '@strudel.cycles/transpiler';
import { getAudioContext, webaudioOutput, registerSynthSounds } from '@strudel.cycles/webaudio';
import { registerSoundfonts } from '@strudel.cycles/soundfonts';
// init canvas
const canvas = document.getElementById('roll');
canvas.width = canvas.width * 2;
canvas.height = canvas.height * 2;
const drawContext = canvas.getContext('2d');
const drawTime = [-2, 2]; // time window of drawn haps
const editor = new StrudelMirror({
defaultOutput: webaudioOutput,
getTime: () => getAudioContext().currentTime,
transpiler,
root: document.getElementById('editor'),
initialCode: funk42,
drawTime,
onDraw: (haps, time) => drawPianoroll({ haps, time, ctx: drawContext, drawTime, fold: 0 }),
prebake: async () => {
initAudioOnFirstClick(); // needed to make the browser happy (don't await this here..)
const loadModules = evalScope(
controls,
import('@strudel.cycles/core'),
import('@strudel.cycles/mini'),
import('@strudel.cycles/tonal'),
import('@strudel.cycles/webaudio'),
);
await Promise.all([loadModules, registerSynthSounds(), registerSoundfonts()]);
},
});
document.getElementById('play').addEventListener('click', () => editor.evaluate());
document.getElementById('stop').addEventListener('click', () => editor.stop());
@@ -1,23 +0,0 @@
{
"name": "vite-vanilla-repl-cm6",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"vite": "^4.3.2"
},
"dependencies": {
"@strudel/codemirror": "workspace:*",
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/mini": "workspace:*",
"@strudel.cycles/soundfonts": "workspace:*",
"@strudel.cycles/tonal": "workspace:*",
"@strudel.cycles/transpiler": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*"
}
}
@@ -1,31 +0,0 @@
body,
html {
margin: 0;
height: 100%;
background: #282c34;
}
main {
height: 100%;
display: flex;
flex-direction: column;
}
.container {
flex-grow: 1;
max-height: 100%;
position: relative;
overflow: auto;
}
#editor {
overflow: auto;
}
.cm-editor {
height: 100%;
}
#roll {
height: 300px;
}
@@ -1,112 +0,0 @@
export const bumpStreet = `// froos - "22 bump street", licensed with CC BY-NC-SA 4.0
await samples('github:felixroos/samples/main')
await samples('https://strudel.tidalcycles.org/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
"<[0,<6 7 9>,13,<17 20 22 26>]!2>/2"
// make it 22 edo
.fmap(v => Math.pow(2,v/22))
// mess with the base frequency
.mul("<300 [300@3 200]>/8").freq()
.layer(
// chords
x=>x.div(freq(2)).s("flute").euclidLegato("<3 2>",8)
.shape(.4).lpf(sine.range(800,4000).slow(8)),
// adlibs
x=>x.arp("{0 3 2 [1 3]}%1.5")
.s('xylo').mul(freq(2))
.delay(.5).delayfeedback(.4).juxBy(.5, rev)
.hpf(sine.range(200,3000).slow(8)),
// bass
x=>x.arp("[0 [2 1?]](5,8)").s('sawtooth').div(freq(4))
.lpf(sine.range(400,2000).slow(8)).lpq(8).shape(.4)
.off(1/8, x=>x.mul(freq(2)).degradeBy(.5)).gain(.3)
).clip(1).release(.2)
.stack(
// drums
s("bd sd:<2 1>, [~ hh]*2, [~ rim]").bank('RolandTR909')
.off(1/8, x=>x.speed(2).gain(.4)).sometimes(ply(2)).gain(.8)
.mask("<0@4 1@12>/4")
.reset("<x@15 [x(3,8) x*[4 8]]>")
// wait for it...
).fast(2/3)
//.crush(6) // remove "//" if you dare`;
export const trafficFlam = `// froos - "traffic flam", licensed with CC BY-NC-SA 4.0
await samples('github:felixroos/samples/main')
await samples('https://strudel.tidalcycles.org/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
addVoicings('hip', {
m11: ['2M 3m 4P 7m'],
'^7#11': ['3M 4A 5P 7M'],
}, ['C4', 'C6'])
stack(
stack(
"<Bbm11 A^7#11>/2".voicings('hip').note()
.s("gm_epiano1:2")
.arp("[<[0 1 2 3] [3 2 1 0]> ~@5]/2")
.release(2).late(.25).lpf(2000),
"<Bb1 A1>/2".note().s('gm_acoustic_bass'),
n("<0 2 3>(3,8)".off(1/8, add(4)))
.scale("<Bb4:minor A4:lydian>/2")
.s('gm_electric_guitar_jazz')
.decay(sine.range(.05, .2).slow(32)).sustain(0)
.delay(.5).lpf(sine.range(100,5000).slow(64))
.gain(.7).room(.5).pan(sine.range(0,1).slow(11))
).add(perlin.range(0,.25).note()),
stack(
s("bd:1(3,8) rim").bank('RolandTR707').slow(2).room("<0 <.1 .6>>")
.when("<0@7 1>",x=>x.echoWith(3, .0625, (x,i) => x.speed(1+i*.24))),
s("rim*4").end(.05).bank('RolandTR808').speed(.8).room(.2)
)
)
.late("[0 .05]*2").late(12)
`;
export const funk42 = `// froos - how to funk in 42 lines of code
// adapted from "how to funk in two minutes" by marc rebillet https://www.youtube.com/watch?v=3vBwRfQbXkg
// thanks to peach for the transcription: https://www.youtube.com/watch?v=8eiPXvIgda4
await samples('github:felixroos/samples/main')
await samples('https://strudel.tidalcycles.org/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
setcps(.5)
let drums = stack(
s("bd*2, ~ sd").bank('RolandTR707').room("0 .1"),
s("hh*4").begin(.2).release(.02).end(.25).release(.02)
.gain(.3).bank('RolandTR707').late(.02).room(.5),
//s("shaker_small").struct("[x x*2]*2").speed(".8,.9").release(.02)
).fast(2)
let wurli = note(\`<
[[a2,g3,[b3 c4],e4] ~ [g3,c4,e4](3,8)@4 ~@2]!3
[[e2,e3,a3,b3,e4]@3 [e2,e3,ab3,b3,e4]@5]>\`)
.s("gm_epiano1:5").decay(.2).sustain("<[1 0@7]!3 1>")
.gain("<[.8@2 .4@14]!3 .7>").room(.3)
let organ = note("<[~@3 [a3,d4,f#4]@2 [[a3,c4,e4]@2 ~] ~@2]!3 ~>".add(12))
.s("gm_percussive_organ:2").gain(.6).lpf(1800).pan(.2).room(.3);
let clav = note(\`<
[~@3 a2 [g3,[b3 c4],e4]@2 ~ a2 [g3,b3,e4] ~@2 [g3,c4,e4] ~@4]!3
[~@3 e3 [[a3 b3],c3,e3]@2 ~ e2 [e3,a3]@3 [b3,e3] ~@2 [b3,e3]@2]>\`)
.s("gm_clavinet:1").decay("<.25!3 [.25 .4]>").sustain(0)
.gain(.7).pan(.8).room(.2);
let bass = note(\`<
[a1 [~ [g2 a2]] [g1 g#1] [a1 [g2 a2]]]
[a1 [~ [g2 a2]] [e3 d3] [c3 [g3 a3]]]
[a1 [~ [g2 a2]] [g1 g#1] [a1 [g2 a2]]]
[e2@6 e1@5 e1 [[d2 e3] g1]@4]
>\`).s("gm_electric_bass_pick:1").release(.1)
stack(
drums
,wurli
,organ
,clav
,bass
)`;
@@ -6,3 +6,5 @@ This folder demonstrates how to set up a strudel repl using vite and vanilla JS.
npm i npm i
npm run dev npm run dev
``` ```
or view it [live on githack](https://rawcdn.githack.com/tidalcycles/strudel/5fb36acb046ead7cd6ad3cd10f532e7f585f536a/packages/core/examples/vite-vanilla-repl/dist/index.html)
@@ -10,7 +10,7 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"devDependencies": { "devDependencies": {
"vite": "^4.3.3" "vite": "^3.2.0"
}, },
"dependencies": { "dependencies": {
"@strudel.cycles/core": "workspace:*", "@strudel.cycles/core": "workspace:*",
+1 -5
View File
@@ -32,11 +32,7 @@ export class Hap {
} }
get duration() { get duration() {
return this.whole.end.sub(this.whole.begin).mul(typeof this.value?.clip === 'number' ? this.value?.clip : 1); return this.whole.end.sub(this.whole.begin);
}
get endClipped() {
return this.whole.begin.add(this.duration);
} }
wholeOrPart() { wholeOrPart() {
-2
View File
@@ -18,13 +18,11 @@ export * from './util.mjs';
export * from './speak.mjs'; export * from './speak.mjs';
export * from './evaluate.mjs'; export * from './evaluate.mjs';
export * from './repl.mjs'; export * from './repl.mjs';
export * from './cyclist.mjs';
export * from './logger.mjs'; export * from './logger.mjs';
export * from './time.mjs'; export * from './time.mjs';
export * from './draw.mjs'; export * from './draw.mjs';
export * from './animate.mjs'; export * from './animate.mjs';
export * from './pianoroll.mjs'; export * from './pianoroll.mjs';
export * from './spiral.mjs';
export * from './ui.mjs'; export * from './ui.mjs';
export { default as drawLine } from './drawLine.mjs'; export { default as drawLine } from './drawLine.mjs';
export { default as gist } from './gist.js'; export { default as gist } from './gist.js';
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel.cycles/core", "name": "@strudel.cycles/core",
"version": "0.9.0", "version": "0.7.2",
"description": "Port of Tidal Cycles to JavaScript", "description": "Port of Tidal Cycles to JavaScript",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",
@@ -35,7 +35,7 @@
}, },
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2", "gitHead": "0e26d4e741500f5bae35b023608f062a794905c2",
"devDependencies": { "devDependencies": {
"vite": "^4.3.3", "vite": "^3.2.2",
"vitest": "^0.33.0" "vitest": "^0.25.7"
} }
} }
+49 -76
View File
@@ -472,15 +472,15 @@ export class Pattern {
/** /**
* Returns a new pattern with the given location information added to the * Returns a new pattern with the given location information added to the
* context of every hap. * context of every hap.
* @param {Number} start start offset * @param {Number} start
* @param {Number} end end offset * @param {Number} end
* @returns Pattern * @returns Pattern
* @noAutocomplete * @noAutocomplete
*/ */
withLoc(start, end) { withLocation(start, end) {
const location = { const location = {
start, start: { line: start[0], column: start[1], offset: start[2] },
end, end: { line: end[0], column: end[1], offset: end[2] },
}; };
return this.withContext((context) => { return this.withContext((context) => {
const locations = (context.locations || []).concat([location]); const locations = (context.locations || []).concat([location]);
@@ -488,6 +488,32 @@ export class Pattern {
}); });
} }
withMiniLocation(start, end) {
const offset = {
start: { line: start[0], column: start[1], offset: start[2] },
end: { line: end[0], column: end[1], offset: end[2] },
};
return this.withContext((context) => {
let locations = context.locations || [];
locations = locations.map(({ start, end }) => {
const colOffset = start.line === 1 ? offset.start.column : 0;
return {
start: {
...start,
line: start.line - 1 + (offset.start.line - 1) + 1,
column: start.column - 1 + colOffset,
},
end: {
...end,
line: end.line - 1 + (offset.start.line - 1) + 1,
column: end.column - 1 + colOffset,
},
};
});
return { ...context, locations };
});
}
/** /**
* Returns a new Pattern, which only returns haps that meet the given test. * Returns a new Pattern, which only returns haps that meet the given test.
* @param {Function} hap_test - a function which returns false for haps to be removed from the pattern * @param {Function} hap_test - a function which returns false for haps to be removed from the pattern
@@ -783,14 +809,13 @@ export class Pattern {
hap.setContext({ hap.setContext({
...hap.context, ...hap.context,
onTrigger: (...args) => { onTrigger: (...args) => {
// run previously set trigger, if it exists if (!dominant && hap.context.onTrigger) {
hap.context.onTrigger?.(...args); hap.context.onTrigger(...args);
}
onTrigger(...args); onTrigger(...args);
}, },
// if dominantTrigger is set to true, the default output (webaudio) will be disabled // we need this to know later if the default trigger should still fire
// when using multiple triggers, you cannot flip this flag to false again! dominantTrigger: dominant,
// example: x.csound('CooLSynth').log() as well as x.log().csound('CooLSynth') should work the same
dominantTrigger: hap.context.dominantTrigger || dominant,
}), }),
); );
} }
@@ -1557,24 +1582,6 @@ export const range2 = register('range2', function (min, max, pat) {
return pat.fromBipolar()._range(min, max); return pat.fromBipolar()._range(min, max);
}); });
/**
* Allows dividing numbers via list notation using ":".
* Returns a new pattern with just numbers.
* @name ratio
* @memberof Pattern
* @returns Pattern
* @example
* ratio("1, 5:4, 3:2").mul(110).freq().s("piano").slow(2)
*/
export const ratio = register('ratio', (pat) =>
pat.fmap((v) => {
if (!Array.isArray(v)) {
return v;
}
return v.slice(1).reduce((acc, n) => acc / n, v[0]);
}),
);
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
// Structural and temporal transformations // Structural and temporal transformations
@@ -1670,9 +1677,6 @@ export const ply = register('ply', function (factor, pat) {
* s("<bd sd> hh").fast(2) // s("[<bd sd> hh]*2") * s("<bd sd> hh").fast(2) // s("[<bd sd> hh]*2")
*/ */
export const { fast, density } = register(['fast', 'density'], function (factor, pat) { export const { fast, density } = register(['fast', 'density'], function (factor, pat) {
if (factor === 0) {
return silence;
}
factor = Fraction(factor); factor = Fraction(factor);
const fastQuery = pat.withQueryTime((t) => t.mul(factor)); const fastQuery = pat.withQueryTime((t) => t.mul(factor));
return fastQuery.withHapTime((t) => t.div(factor)); return fastQuery.withHapTime((t) => t.div(factor));
@@ -1699,9 +1703,6 @@ export const hurry = register('hurry', function (r, pat) {
* s("<bd sd> hh").slow(2) // s("[<bd sd> hh]/2") * s("<bd sd> hh").slow(2) // s("[<bd sd> hh]/2")
*/ */
export const { slow, sparsity } = register(['slow', 'sparsity'], function (factor, pat) { export const { slow, sparsity } = register(['slow', 'sparsity'], function (factor, pat) {
if (factor === 0) {
return silence;
}
return pat._fast(Fraction(1).div(factor)); return pat._fast(Fraction(1).div(factor));
}); });
@@ -2028,7 +2029,7 @@ export const jux = register('jux', function (func, pat) {
* @example * @example
* "<0 [2 4]>" * "<0 [2 4]>"
* .echoWith(4, 1/8, (p,n) => p.add(n*2)) * .echoWith(4, 1/8, (p,n) => p.add(n*2))
* .scale('C minor').note().clip(.2) * .scale('C minor').note().legato(.2)
*/ */
export const { echoWith, echowith, stutWith, stutwith } = register( export const { echoWith, echowith, stutWith, stutwith } = register(
['echoWith', 'echowith', 'stutWith', 'stutwith'], ['echoWith', 'echowith', 'stutWith', 'stutwith'],
@@ -2178,7 +2179,6 @@ export const velocity = register('velocity', function (velocity, pat) {
/** /**
* *
* Multiplies the hap duration with the given factor. * Multiplies the hap duration with the given factor.
* With samples, `clip` might be a better function to use ([more info](https://github.com/tidalcycles/strudel/pull/598))
* @name legato * @name legato
* @memberof Pattern * @memberof Pattern
* @example * @example
@@ -2241,21 +2241,17 @@ const _loopAt = function (factor, pat, cps = 1) {
.slow(factor); .slow(factor);
}; };
/** /*
* Chops samples into the given number of slices, triggering those slices with a given pattern of slice numbers. * Chops samples into the given number of slices, triggering those slices with a given pattern of slice numbers.
* Instead of a number, it also accepts a list of numbers from 0 to 1 to slice at specific points.
* @name slice * @name slice
* @memberof Pattern * @memberof Pattern
* @returns Pattern * @returns Pattern
* @example * @example
* await samples('github:tidalcycles/Dirt-Samples/master') * await samples('github:tidalcycles/Dirt-Samples/master')
* s("breaks165").slice(8, "0 1 <2 2*2> 3 [4 0] 5 6 7".every(3, rev)).slow(1.5) * s("breaks165").slice(8, "0 1 <2 2*2> 3 [4 0] 5 6 7".every(3, rev)).slow(1.5)
* @example
* await samples('github:tidalcycles/Dirt-Samples/master')
* s("breaks125/2").fit().slice([0,.25,.5,.75], "0 1 1 <2 3>")
*/ */
export const slice = register( const slice = register(
'slice', 'slice',
function (npat, ipat, opat) { function (npat, ipat, opat) {
return npat.innerBind((n) => return npat.innerBind((n) =>
@@ -2263,9 +2259,9 @@ export const slice = register(
opat.outerBind((o) => { opat.outerBind((o) => {
// If it's not an object, assume it's a string and make it a 's' control parameter // If it's not an object, assume it's a string and make it a 's' control parameter
o = o instanceof Object ? o : { s: o }; o = o instanceof Object ? o : { s: o };
const begin = Array.isArray(n) ? n[i] : i / n; // Remember we must stay pure and avoid editing the object directly
const end = Array.isArray(n) ? n[i + 1] : (i + 1) / n; const toAdd = { begin: i / n, end: (i + 1) / n, _slices: n };
return pure({ begin, end, _slices: n, ...o }); return pure({ ...toAdd, ...o });
}), }),
), ),
); );
@@ -2273,17 +2269,17 @@ export const slice = register(
false, // turns off auto-patternification false, // turns off auto-patternification
); );
/** /*
* Works the same as slice, but changes the playback speed of each slice to match the duration of its step. * Works the same as slice, but changes the playback speed of each slice to match the duration of its step.
* @name splice * @name splice
* @memberof Pattern
* @returns Pattern
* @example * @example
* await samples('github:tidalcycles/Dirt-Samples/master') * await samples('github:tidalcycles/Dirt-Samples/master')
* s("breaks165") * s("breaks165").splice(8, "0 1 [2 3 0]@2 3 0@2 7").hurry(0.65)
* .splice(8, "0 1 [2 3 0]@2 3 0@2 7")
* .hurry(0.65)
*/ */
export const splice = register( const splice = register(
'splice', 'splice',
function (npat, ipat, opat) { function (npat, ipat, opat) {
const sliced = slice(npat, ipat, opat); const sliced = slice(npat, ipat, opat);
@@ -2300,33 +2296,10 @@ export const splice = register(
false, // turns off auto-patternification false, // turns off auto-patternification
); );
// this function will be redefined in repl.mjs to use the correct cps value. const { loopAt, loopat } = register(['loopAt', 'loopat'], function (factor, pat) {
// It is still here to work in cases where repl.mjs is not used
export const { loopAt, loopat } = register(['loopAt', 'loopat'], function (factor, pat) {
return _loopAt(factor, pat, 1); return _loopAt(factor, pat, 1);
}); });
// the fit function will be redefined in repl.mjs to use the correct cps value.
// It is still here to work in cases where repl.mjs is not used
/**
* Makes the sample fit its event duration. Good for rhythmical loops like drum breaks.
* Similar to loopAt.
* @name fit
* @example
* samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' })
* s("rhodes/4").fit()
*/
export const fit = register('fit', (pat) =>
pat.withHap((hap) =>
hap.withValue((v) => ({
...v,
speed: 1 / hap.whole.duration,
unit: 'c',
})),
),
);
/** /**
* Makes the sample fit the given number of cycles and cps value, by * Makes the sample fit the given number of cycles and cps value, by
* changing the speed. Please note that at some point cps will be * changing the speed. Please note that at some point cps will be
@@ -2340,6 +2313,6 @@ export const fit = register('fit', (pat) =>
* s("rhodes").loopAtCps(4,1.5).cps(1.5) * s("rhodes").loopAtCps(4,1.5).cps(1.5)
*/ */
// TODO - global cps clock // TODO - global cps clock
export const { loopAtCps, loopatcps } = register(['loopAtCps', 'loopatcps'], function (factor, cps, pat) { const { loopAtCps, loopatcps } = register(['loopAtCps', 'loopatcps'], function (factor, cps, pat) {
return _loopAt(factor, pat, cps); return _loopAt(factor, pat, cps);
}); });
+154 -101
View File
@@ -1,6 +1,6 @@
/* /*
pianoroll.mjs - <short description TODO> pianoroll.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/pianoroll.mjs> Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/tone/pianoroll.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/>. 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/>.
*/ */
@@ -29,26 +29,129 @@ const getValue = (e) => {
return value; return value;
}; };
Pattern.prototype.pianoroll = function (options = {}) { Pattern.prototype.pianoroll = function ({
let { cycles = 4, playhead = 0.5, overscan = 1, hideNegative = false } = options; cycles = 4,
playhead = 0.5,
overscan = 1,
flipTime = 0,
flipValues = 0,
hideNegative = false,
// inactive = '#C9E597',
// inactive = '#FFCA28',
inactive = '#7491D2',
active = '#FFCA28',
// background = '#2A3236',
background = 'transparent',
smear = 0,
playheadColor = 'white',
minMidi = 10,
maxMidi = 90,
autorange = 0,
timeframe: timeframeProp,
fold = 0,
vertical = 0,
} = {}) {
const ctx = getDrawContext();
const w = ctx.canvas.width;
const h = ctx.canvas.height;
let from = -cycles * playhead; let from = -cycles * playhead;
let to = cycles * (1 - playhead); let to = cycles * (1 - playhead);
if (timeframeProp) {
console.warn('timeframe is deprecated! use from/to instead');
from = 0;
to = timeframeProp;
}
const timeAxis = vertical ? h : w;
const valueAxis = vertical ? w : h;
let timeRange = vertical ? [timeAxis, 0] : [0, timeAxis]; // pixel range for time
const timeExtent = to - from; // number of seconds that fit inside the canvas frame
const valueRange = vertical ? [0, valueAxis] : [valueAxis, 0]; // pixel range for values
let valueExtent = maxMidi - minMidi + 1; // number of "slots" for values, overwritten if autorange true
let barThickness = valueAxis / valueExtent; // pixels per value, overwritten if autorange true
let foldValues = [];
flipTime && timeRange.reverse();
flipValues && valueRange.reverse();
this.draw( this.draw(
(ctx, haps, t) => { (ctx, events, t) => {
ctx.fillStyle = background;
ctx.globalAlpha = 1; // reset!
if (!smear) {
ctx.clearRect(0, 0, w, h);
ctx.fillRect(0, 0, w, h);
}
const inFrame = (event) => const inFrame = (event) =>
(!hideNegative || event.whole.begin >= 0) && event.whole.begin <= t + to && event.endClipped >= t + from; (!hideNegative || event.whole.begin >= 0) && event.whole.begin <= t + to && event.whole.end >= t + from;
pianoroll({ events.filter(inFrame).forEach((event) => {
...options, const isActive = event.whole.begin <= t && event.whole.end > t;
time: t, ctx.fillStyle = event.context?.color || inactive;
ctx, ctx.strokeStyle = event.context?.color || active;
haps: haps.filter(inFrame), ctx.globalAlpha = event.context.velocity ?? 1;
const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange);
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
const value = getValue(event);
const valuePx = scale(
fold ? foldValues.indexOf(value) / foldValues.length : (Number(value) - minMidi) / valueExtent,
...valueRange,
);
let margin = 0;
const offset = scale(t / timeExtent, ...timeRange);
let coords;
if (vertical) {
coords = [
valuePx + 1 - (flipValues ? barThickness : 0), // x
timeAxis - offset + timePx + margin + 1 - (flipTime ? 0 : durationPx), // y
barThickness - 2, // width
durationPx - 2, // height
];
} else {
coords = [
timePx - offset + margin + 1 - (flipTime ? durationPx : 0), // x
valuePx + 1 - (flipValues ? 0 : barThickness), // y
durationPx - 2, // widith
barThickness - 2, // height
];
}
isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords);
}); });
ctx.globalAlpha = 1; // reset!
const playheadPosition = scale(-from / timeExtent, ...timeRange);
// draw playhead
ctx.strokeStyle = playheadColor;
ctx.beginPath();
if (vertical) {
ctx.moveTo(0, playheadPosition);
ctx.lineTo(valueAxis, playheadPosition);
} else {
ctx.moveTo(playheadPosition, 0);
ctx.lineTo(playheadPosition, valueAxis);
}
ctx.stroke();
}, },
{ {
from: from - overscan, from: from - overscan,
to: to + overscan, to: to + overscan,
onQuery: (events) => {
const { min, max, values } = events.reduce(
({ min, max, values }, e) => {
const v = getValue(e);
return {
min: v < min ? v : min,
max: v > max ? v : max,
values: values.includes(v) ? values : [...values, v],
};
},
{ min: Infinity, max: -Infinity, values: [] },
);
if (autorange) {
minMidi = min;
maxMidi = max;
valueExtent = maxMidi - minMidi + 1;
}
foldValues = values.sort((a, b) => String(a).localeCompare(String(b)));
barThickness = fold ? valueAxis / foldValues.length : valueAxis / valueExtent;
},
}, },
); );
return this; return this;
@@ -78,14 +181,6 @@ export function pianoroll({
timeframe: timeframeProp, timeframe: timeframeProp,
fold = 0, fold = 0,
vertical = 0, vertical = 0,
labels = false,
fill = 1,
fillActive = false,
strokeActive = true,
stroke,
hideInactive = 0,
colorizeInactive = 1,
fontFamily,
ctx, ctx,
} = {}) { } = {}) {
const w = ctx.canvas.width; const w = ctx.canvas.width;
@@ -129,75 +224,50 @@ export function pianoroll({
// foldValues = values.sort((a, b) => a - b); // foldValues = values.sort((a, b) => a - b);
foldValues = values.sort((a, b) => String(a).localeCompare(String(b))); foldValues = values.sort((a, b) => String(a).localeCompare(String(b)));
barThickness = fold ? valueAxis / foldValues.length : valueAxis / valueExtent; barThickness = fold ? valueAxis / foldValues.length : valueAxis / valueExtent;
ctx.fillStyle = background; ctx.fillStyle = background;
ctx.globalAlpha = 1; // reset! ctx.globalAlpha = 1; // reset!
if (!smear) { if (!smear) {
ctx.clearRect(0, 0, w, h); ctx.clearRect(0, 0, w, h);
ctx.fillRect(0, 0, w, h); ctx.fillRect(0, 0, w, h);
} }
haps.forEach((event) => { /* const inFrame = (event) =>
const isActive = event.whole.begin <= time && event.endClipped > time; (!hideNegative || event.whole.begin >= 0) && event.whole.begin <= time + to && event.whole.end >= time + from; */
let strokeCurrent = stroke ?? (strokeActive && isActive); haps
let fillCurrent = (!isActive && fill) || (isActive && fillActive); // .filter(inFrame)
if (hideInactive && !isActive) { .forEach((event) => {
return; const isActive = event.whole.begin <= time && event.whole.end > time;
} const color = event.value?.color || event.context?.color;
let color = event.value?.color || event.context?.color; ctx.fillStyle = color || inactive;
active = color || active; ctx.strokeStyle = color || active;
inactive = colorizeInactive ? color || inactive : inactive; ctx.globalAlpha = event.context.velocity ?? 1;
color = isActive ? active : inactive; const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange);
ctx.fillStyle = fillCurrent ? color : 'transparent'; let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
ctx.strokeStyle = color; const value = getValue(event);
ctx.globalAlpha = event.context.velocity ?? event.value?.gain ?? 1; const valuePx = scale(
const timeProgress = (event.whole.begin - (flipTime ? to : from)) / timeExtent; fold ? foldValues.indexOf(value) / foldValues.length : (Number(value) - minMidi) / valueExtent,
const timePx = scale(timeProgress, ...timeRange); ...valueRange,
let durationPx = scale(event.duration / timeExtent, 0, timeAxis); );
const value = getValue(event); let margin = 0;
const valueProgress = fold const offset = scale(time / timeExtent, ...timeRange);
? foldValues.indexOf(value) / foldValues.length let coords;
: (Number(value) - minMidi) / valueExtent; if (vertical) {
const valuePx = scale(valueProgress, ...valueRange); coords = [
let margin = 0; valuePx + 1 - (flipValues ? barThickness : 0), // x
const offset = scale(time / timeExtent, ...timeRange); timeAxis - offset + timePx + margin + 1 - (flipTime ? 0 : durationPx), // y
let coords; barThickness - 2, // width
if (vertical) { durationPx - 2, // height
coords = [ ];
valuePx + 1 - (flipValues ? barThickness : 0), // x } else {
timeAxis - offset + timePx + margin + 1 - (flipTime ? 0 : durationPx), // y coords = [
barThickness - 2, // width timePx - offset + margin + 1 - (flipTime ? durationPx : 0), // x
durationPx - 2, // height valuePx + 1 - (flipValues ? 0 : barThickness), // y
]; durationPx - 2, // widith
} else { barThickness - 2, // height
coords = [ ];
timePx - offset + margin + 1 - (flipTime ? durationPx : 0), // x }
valuePx + 1 - (flipValues ? 0 : barThickness), // y isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords);
durationPx - 2, // widith });
barThickness - 2, // height
];
}
/* const xFactor = Math.sin(performance.now() / 500) + 1;
coords[0] *= xFactor; */
if (strokeCurrent) {
ctx.strokeRect(...coords);
}
if (fillCurrent) {
ctx.fillRect(...coords);
}
//ctx.ellipse(...ellipseFromRect(...coords))
if (labels) {
const defaultLabel = event.value.note ?? event.value.s + (event.value.n ? `:${event.value.n}` : '');
const { label: inactiveLabel, activeLabel } = event.value;
const customLabel = isActive ? activeLabel || inactiveLabel : inactiveLabel;
const label = customLabel ?? defaultLabel;
let measure = vertical ? durationPx : barThickness * 0.75;
ctx.font = `${measure}px ${fontFamily || 'monospace'}`;
// font color
ctx.fillStyle = /* isActive && */ !fillCurrent ? color : 'black';
ctx.textBaseline = 'top';
ctx.fillText(label, ...coords);
}
});
ctx.globalAlpha = 1; // reset! ctx.globalAlpha = 1; // reset!
const playheadPosition = scale(-from / timeExtent, ...timeRange); const playheadPosition = scale(-from / timeExtent, ...timeRange);
// draw playhead // draw playhead
@@ -214,7 +284,7 @@ export function pianoroll({
return this; return this;
} }
export function getDrawOptions(drawTime, options = {}) { function getOptions(drawTime, options = {}) {
let [lookbehind, lookahead] = drawTime; let [lookbehind, lookahead] = drawTime;
lookbehind = Math.abs(lookbehind); lookbehind = Math.abs(lookbehind);
const cycles = lookahead + lookbehind; const cycles = lookahead + lookbehind;
@@ -223,22 +293,5 @@ export function getDrawOptions(drawTime, options = {}) {
} }
Pattern.prototype.punchcard = function (options) { Pattern.prototype.punchcard = function (options) {
return this.onPaint((ctx, time, haps, drawTime, paintOptions = {}) => return this.onPaint((ctx, time, haps, drawTime) => pianoroll({ ctx, time, haps, ...getOptions(drawTime, options) }));
pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { ...paintOptions, ...options }) }),
);
}; };
Pattern.prototype.wordfall = function (options) {
return this.punchcard({ vertical: 1, labels: 1, stroke: 0, fillActive: 1, active: 'white', ...options });
};
/* Pattern.prototype.pianoroll = function (options) {
return this.onPaint((ctx, time, haps, drawTime) =>
pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { fold: 0, ...options }) }),
);
}; */
export function drawPianoroll(options) {
const { drawTime, ...rest } = options;
pianoroll({ ...getDrawOptions(drawTime), ...rest });
}
+25 -47
View File
@@ -3,7 +3,6 @@ import { evaluate as _evaluate } from './evaluate.mjs';
import { logger } from './logger.mjs'; import { logger } from './logger.mjs';
import { setTime } from './time.mjs'; import { setTime } from './time.mjs';
import { evalScope } from './evaluate.mjs'; import { evalScope } from './evaluate.mjs';
import { register } from './pattern.mjs';
export function repl({ export function repl({
interval, interval,
@@ -17,17 +16,26 @@ export function repl({
onToggle, onToggle,
editPattern, editPattern,
}) { }) {
const id = s4();
const scheduler = new Cyclist({ const scheduler = new Cyclist({
interval, interval,
onTrigger: getTrigger({ defaultOutput, getTime }), onTrigger: async (hap, deadline, duration, cps) => {
try {
if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
await defaultOutput(hap, deadline, duration, cps);
}
if (hap.context.onTrigger) {
// call signature of output / onTrigger is different...
await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps);
}
} catch (err) {
logger(`[cyclist] error: ${err.message}`, 'error');
}
},
onError: onSchedulerError, onError: onSchedulerError,
getTime, getTime,
onToggle, onToggle,
}); });
const setPattern = (pattern, autostart = true) => {
pattern = editPattern?.(pattern) || pattern;
scheduler.setPattern(pattern, autostart);
};
setTime(() => scheduler.now()); // TODO: refactor? setTime(() => scheduler.now()); // TODO: refactor?
const evaluate = async (code, autostart = true) => { const evaluate = async (code, autostart = true) => {
if (!code) { if (!code) {
@@ -35,10 +43,12 @@ export function repl({
} }
try { try {
await beforeEval?.({ code }); await beforeEval?.({ code });
let { pattern, meta } = await _evaluate(code, transpiler); let { pattern } = await _evaluate(code, transpiler);
logger(`[eval] code updated`); logger(`[eval] code updated`);
setPattern(pattern, autostart); pattern = editPattern?.(pattern, id) || pattern;
afterEval?.({ code, pattern, meta }); scheduler.setPattern(pattern, autostart);
afterEval?.({ code, pattern });
return pattern; return pattern;
} catch (err) { } catch (err) {
// console.warn(`[repl] eval error: ${err.message}`); // console.warn(`[repl] eval error: ${err.message}`);
@@ -50,47 +60,15 @@ export function repl({
const start = () => scheduler.start(); const start = () => scheduler.start();
const pause = () => scheduler.pause(); const pause = () => scheduler.pause();
const setCps = (cps) => scheduler.setCps(cps); const setCps = (cps) => scheduler.setCps(cps);
const setCpm = (cpm) => scheduler.setCps(cpm / 60);
// the following functions use the cps value, which is why they are defined here..
const loopAt = register('loopAt', (cycles, pat) => {
return pat.loopAtCps(cycles, scheduler.cps);
});
const fit = register('fit', (pat) =>
pat.withHap((hap) =>
hap.withValue((v) => ({
...v,
speed: scheduler.cps / hap.whole.duration, // overwrite speed completely?
unit: 'c',
})),
),
);
evalScope({ evalScope({
loopAt,
fit,
setCps, setCps,
setcps: setCps, setcps: setCps,
setCpm,
setcpm: setCpm,
}); });
return { scheduler, evaluate, start, stop, pause, setCps, id };
return { scheduler, evaluate, start, stop, pause, setCps, setPattern };
} }
export const getTrigger = function s4() {
({ getTime, defaultOutput }) => return Math.floor((1 + Math.random()) * 0x10000)
async (hap, deadline, duration, cps) => { .toString(16)
try { .substring(1);
if (!hap.context.onTrigger || !hap.context.dominantTrigger) { }
await defaultOutput(hap, deadline, duration, cps);
}
if (hap.context.onTrigger) {
// call signature of output / onTrigger is different...
await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps);
}
} catch (err) {
logger(`[cyclist] error: ${err.message}`, 'error');
}
};
+1 -1
View File
@@ -27,7 +27,7 @@ export const isaw2 = isaw.toBipolar();
* *
* @return {Pattern} * @return {Pattern}
* @example * @example
* "c3 [eb3,g3] g2 [g3,bb3]".note().clip(saw.slow(4)) * "c3 [eb3,g3] g2 [g3,bb3]".legato(saw.slow(4)).note()
* @example * @example
* saw.range(0,8).segment(8).scale('C major').slow(4).note() * saw.range(0,8).segment(8).scale('C major').slow(4).note()
* *
-118
View File
@@ -1,118 +0,0 @@
import { Pattern } from './index.mjs';
// polar coords -> xy
function fromPolar(angle, radius, cx, cy) {
const radians = ((angle - 90) * Math.PI) / 180;
return [cx + Math.cos(radians) * radius, cy + Math.sin(radians) * radius];
}
const xyOnSpiral = (angle, margin, cx, cy, rotate = 0) => fromPolar((angle + rotate) * 360, margin * angle, cx, cy); // TODO: logSpiral
// draw spiral / segment of spiral
function spiralSegment(options) {
let {
ctx,
from = 0,
to = 3,
margin = 50,
cx = 100,
cy = 100,
rotate = 0,
thickness = margin / 2,
color = '#0000ff30',
cap = 'round',
stretch = 1,
fromOpacity = 1,
toOpacity = 1,
} = options;
from *= stretch;
to *= stretch;
rotate *= stretch;
ctx.lineWidth = thickness;
ctx.lineCap = cap;
ctx.strokeStyle = color;
ctx.globalAlpha = fromOpacity;
ctx.beginPath();
let [sx, sy] = xyOnSpiral(from, margin, cx, cy, rotate);
ctx.moveTo(sx, sy);
const increment = 1 / 60;
let angle = from;
while (angle <= to) {
const [x, y] = xyOnSpiral(angle, margin, cx, cy, rotate);
//ctx.lineWidth = angle*thickness;
ctx.globalAlpha = ((angle - from) / (to - from)) * toOpacity;
ctx.lineTo(x, y);
angle += increment;
}
ctx.stroke();
}
Pattern.prototype.spiral = function (options = {}) {
const {
stretch = 1,
size = 80,
thickness = size / 2,
cap = 'butt', // round butt squar,
inset = 3, // start angl,
playheadColor = '#ffffff90',
playheadLength = 0.02,
playheadThickness = thickness,
padding = 0,
steady = 1,
inactiveColor = '#ffffff20',
colorizeInactive = 0,
fade = true,
// logSpiral = true,
} = options;
function spiral({ ctx, time, haps, drawTime }) {
const [w, h] = [ctx.canvas.width, ctx.canvas.height];
ctx.clearRect(0, 0, w * 2, h * 2);
const [cx, cy] = [w / 2, h / 2];
const settings = {
margin: size / stretch,
cx,
cy,
stretch,
cap,
thickness,
};
const playhead = {
...settings,
thickness: playheadThickness,
from: inset - playheadLength,
to: inset,
color: playheadColor,
};
const [min] = drawTime;
const rotate = steady * time;
haps.forEach((hap) => {
const isActive = hap.whole.begin <= time && hap.endClipped > time;
const from = hap.whole.begin - time + inset;
const to = hap.endClipped - time + inset - padding;
const { color } = hap.context;
const opacity = fade ? 1 - Math.abs((hap.whole.begin - time) / min) : 1;
spiralSegment({
ctx,
...settings,
from,
to,
rotate,
color: colorizeInactive || isActive ? color : inactiveColor,
fromOpacity: opacity,
toOpacity: opacity,
});
});
spiralSegment({
ctx,
...playhead,
rotate,
});
}
return this.onPaint((ctx, time, haps, drawTime) => spiral({ ctx, time, haps, drawTime }));
};
-40
View File
@@ -1,40 +0,0 @@
/*test for issue 302 support alternative solmization types */
import { sol2note } from '../util.mjs';
import { test } from 'vitest';
import assert from 'assert';
test('solmization - letters', () => {
const result = sol2note(60, 'letters');
const expected = 'C4';
assert.equal(result, expected);
});
test('solmization - solfeggio', () => {
const result = sol2note(60, 'solfeggio');
const expected = 'Do4';
assert.equal(result, expected);
});
test('solmization - indian', () => {
const result = sol2note(60, 'indian');
const expected = 'Sa4';
assert.equal(result, expected);
});
test('solmization - german', () => {
const result = sol2note(60, 'german');
const expected = 'C4';
assert.equal(result, expected);
});
test('solmization - byzantine', () => {
const result = sol2note(60, 'byzantine');
const expected = 'Ni4';
assert.equal(result, expected);
});
test('solmization - japanese', () => {
const result = sol2note(60, 'japanese');
const expected = 'I4';
assert.equal(result, expected);
});
+1 -1
View File
@@ -1,6 +1,6 @@
/* /*
ui.mjs - <short description TODO> ui.mjs - <short description TODO>
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/ui.mjs> Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/tone/ui.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/>. 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/>.
*/ */
+8 -70
View File
@@ -6,29 +6,26 @@ This program is free software: you can redistribute it and/or modify it under th
// returns true if the given string is a note // returns true if the given string is a note
export const isNoteWithOctave = (name) => /^[a-gA-G][#bs]*[0-9]$/.test(name); export const isNoteWithOctave = (name) => /^[a-gA-G][#bs]*[0-9]$/.test(name);
export const isNote = (name) => /^[a-gA-G][#bsf]*[0-9]?$/.test(name); export const isNote = (name) => /^[a-gA-G][#bs]*[0-9]?$/.test(name);
export const tokenizeNote = (note) => { export const tokenizeNote = (note) => {
if (typeof note !== 'string') { if (typeof note !== 'string') {
return []; return [];
} }
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)([0-9]*)$/)?.slice(1) || []; const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bs]*)([0-9])?$/)?.slice(1) || [];
if (!pc) { if (!pc) {
return []; return [];
} }
return [pc, acc, oct ? Number(oct) : undefined]; return [pc, acc, oct ? Number(oct) : undefined];
}; };
const chromas = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 };
const accs = { '#': 1, b: -1, s: 1, f: -1 };
// turns the given note into its midi number representation // turns the given note into its midi number representation
export const noteToMidi = (note, defaultOctave = 3) => { export const noteToMidi = (note) => {
const [pc, acc, oct = defaultOctave] = tokenizeNote(note); const [pc, acc, oct = 3] = tokenizeNote(note);
if (!pc) { if (!pc) {
throw new Error('not a note: "' + note + '"'); throw new Error('not a note: "' + note + '"');
} }
const chroma = chromas[pc.toLowerCase()]; const chroma = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 }[pc.toLowerCase()];
const offset = acc?.split('').reduce((o, char) => o + accs[char], 0) || 0; const offset = acc?.split('').reduce((o, char) => o + { '#': 1, b: -1, s: 1 }[char], 0) || 0;
return (Number(oct) + 1) * 12 + chroma + offset; return (Number(oct) + 1) * 12 + chroma + offset;
}; };
export const midiToFreq = (n) => { export const midiToFreq = (n) => {
@@ -70,14 +67,13 @@ export const getFreq = (noteOrMidi) => {
return midiToFreq(noteToMidi(noteOrMidi)); return midiToFreq(noteToMidi(noteOrMidi));
}; };
const pcs = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'];
/** /**
* @deprecated only used in workshop (first-notes) * @deprecated does not appear to be referenced or invoked anywhere in the codebase
* @noAutocomplete * @noAutocomplete
*/ */
export const midi2note = (n) => { export const midi2note = (n) => {
const oct = Math.floor(n / 12) - 1; const oct = Math.floor(n / 12) - 1;
const pc = pcs[n % 12]; const pc = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'][n % 12];
return pc + oct; return pc + oct;
}; };
@@ -216,61 +212,3 @@ export const splitAt = function (index, value) {
}; };
export const zipWith = (f, xs, ys) => xs.map((n, i) => f(n, ys[i])); export const zipWith = (f, xs, ys) => xs.map((n, i) => f(n, ys[i]));
export const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
/* solmization, not used yet */
const solfeggio = ['Do', 'Reb', 'Re', 'Mib', 'Mi', 'Fa', 'Solb', 'Sol', 'Lab', 'La', 'Sib', 'Si']; /*solffegio notes*/
const indian = [
'Sa',
'Re',
'Ga',
'Ma',
'Pa',
'Dha',
'Ni',
]; /*indian musical notes, seems like they do not use flats or sharps*/
const german = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Hb', 'H']; /*german & dutch musical notes*/
const byzantine = [
'Ni',
'Pab',
'Pa',
'Voub',
'Vou',
'Ga',
'Dib',
'Di',
'Keb',
'Ke',
'Zob',
'Zo',
]; /*byzantine musical notes*/
const japanese = [
'I',
'Ro',
'Ha',
'Ni',
'Ho',
'He',
'To',
]; /*traditional japanese musical notes, seems like they do not use falts or sharps*/
const english = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'];
export const sol2note = (n, notation = 'letters') => {
const pc =
notation === 'solfeggio'
? solfeggio /*check if its is any of the following*/
: notation === 'indian'
? indian
: notation === 'german'
? german
: notation === 'byzantine'
? byzantine
: notation === 'japanese'
? japanese
: english; /*if not use standard version*/
const note = pc[n % 12]; /*calculating the midi value to the note*/
const oct = Math.floor(n / 12) - 1;
return note + oct;
};
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@strudel.cycles/csound", "name": "@strudel.cycles/csound",
"version": "0.9.0", "version": "0.7.1",
"description": "csound bindings for strudel", "description": "csound bindings for strudel",
"main": "index.mjs", "main": "index.mjs",
"publishConfig": { "publishConfig": {
@@ -32,11 +32,11 @@
}, },
"homepage": "https://github.com/tidalcycles/strudel#readme", "homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": { "dependencies": {
"@csound/browser": "6.18.7", "@csound/browser": "6.18.5",
"@strudel.cycles/core": "workspace:*", "@strudel.cycles/core": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*" "@strudel.cycles/webaudio": "workspace:*"
}, },
"devDependencies": { "devDependencies": {
"vite": "^4.3.3" "vite": "^3.2.2"
} }
} }
-3
View File
@@ -1,3 +0,0 @@
# @strudel/desktopbridge
This package contains utilities used to communicate with the Tauri backend
-11
View File
@@ -1,11 +0,0 @@
import { listen } from '@tauri-apps/api/event';
import { logger } from '../core/logger.mjs';
// listen for log events from the Tauri backend and log in the UI
await listen('log-event', (e) => {
if (e.payload == null) {
return;
}
const { message, message_type } = e.payload;
logger(message, message_type);
});

Some files were not shown because too many files have changed in this diff Show More