Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1f54d93a00 | |||
| 284f6293a2 | |||
| a2b99f94bd | |||
| 8a0f9c6e36 | |||
| d38a8ab1fa | |||
| 6a451be50c | |||
| dac2996b3d | |||
| d689d71ee6 | |||
| a4a6321afb | |||
| 8b13badb67 | |||
| 15a6747b2c | |||
| a96ff66f63 | |||
| 123323e32b | |||
| 8ca850f9fe | |||
| d77725cf1d | |||
| b016d115f5 | |||
| 1a57a86497 | |||
| d433e68fb2 | |||
| c90bc80e6d | |||
| d3c4fd70dc | |||
| 87cab96893 | |||
| 0248d15470 | |||
| d4801c545e | |||
| ce28acaf18 | |||
| bf0336404d | |||
| 7c3d8e5c8d | |||
| 3dad55e266 | |||
| 3d9ac37efe | |||
| d4619362bd |
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -22,3 +22,5 @@ dist-ssr
|
|||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
|
public/samples
|
||||||
@@ -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..
|
||||||
@@ -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>
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 163 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 224 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 244 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 103 KiB |
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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 -> 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;
|
||||||
@@ -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;
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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()}>
|
||||||
|
<
|
||||||
|
</div>
|
||||||
|
<div className="fixed right-0 p-4 h-full items-start flex hover:bg-[#00000050]" onClick={() => next()}>
|
||||||
|
>
|
||||||
|
</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;
|
||||||
@@ -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;
|
||||||
@@ -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 -> 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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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>,
|
||||||
|
);
|
||||||
@@ -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/'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# What's bad
|
||||||
|
|
||||||
|
- Dynamic Typing in JS
|
||||||
|
- Fractions and Hot Paths
|
||||||
|
- Verbose function composition
|
||||||
|
- Method-chaining tradeoffs
|
||||||
@@ -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>
|
||||||
@@ -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
|
||||||
|
/>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# Strudel Docs
|
||||||
@@ -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 />
|
||||||
@@ -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
|
||||||
|
/>
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# The journey
|
||||||
|
|
||||||
|

|
||||||
@@ -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>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
@@ -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
|
||||||
|
|
||||||
|

|
||||||
@@ -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" /> */}
|
||||||
|
|
||||||
|

|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# REPL: System Overview
|
||||||
|
|
||||||
|
<center>
|
||||||
|
<img src="./img/strudelflow.png" className="w-[900px]" />
|
||||||
|
</center>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Strudel Docs
|
||||||
|
|
||||||
|

|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Pattern Swatches
|
||||||
|
|
||||||
|
fork strudel - publish your own patterns with a fixed version
|
||||||
|
|
||||||
|

|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import TranspilationDemo from '../TranspilationDemo';
|
||||||
|
|
||||||
|
# User Code: Transpilation
|
||||||
|
|
||||||
|
<TranspilationDemo />
|
||||||
@@ -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>
|
||||||
@@ -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)`}
|
||||||
|
/>
|
||||||
@@ -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;
|
||||||
@@ -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')],
|
||||||
|
};
|
||||||
@@ -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
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -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.28.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
|
||||||
@@ -1,199 +0,0 @@
|
|||||||
import { EditorState } from '@codemirror/state';
|
|
||||||
import { EditorView, keymap, Decoration, lineNumbers, highlightActiveLineGutter } from '@codemirror/view';
|
|
||||||
import { defaultKeymap } from '@codemirror/commands';
|
|
||||||
import { syntaxHighlighting, defaultHighlightStyle } from '@codemirror/language';
|
|
||||||
import { javascript } from '@codemirror/lang-javascript';
|
|
||||||
import { StateField, StateEffect } from '@codemirror/state';
|
|
||||||
import { oneDark } from './themes/one-dark';
|
|
||||||
import { repl, Drawer } from '@strudel.cycles/core';
|
|
||||||
|
|
||||||
// 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(),
|
|
||||||
highlightField,
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// highlighting
|
|
||||||
//
|
|
||||||
|
|
||||||
export const setHighlights = StateEffect.define();
|
|
||||||
export const highlightField = StateField.define({
|
|
||||||
create() {
|
|
||||||
return Decoration.none;
|
|
||||||
},
|
|
||||||
update(highlights, tr) {
|
|
||||||
try {
|
|
||||||
for (let e of tr.effects) {
|
|
||||||
if (e.is(setHighlights)) {
|
|
||||||
const { haps } = e.value;
|
|
||||||
const marks =
|
|
||||||
haps
|
|
||||||
.map((hap) =>
|
|
||||||
(hap.context.locations || []).map(({ start, end }) => {
|
|
||||||
// const color = hap.context.color || e.value.color || '#FFCA28';
|
|
||||||
let from = tr.newDoc.line(start.line).from + start.column;
|
|
||||||
let to = tr.newDoc.line(end.line).from + end.column;
|
|
||||||
const l = tr.newDoc.length;
|
|
||||||
if (from > l || to > l) {
|
|
||||||
return; // dont mark outside of range, as it will throw an error
|
|
||||||
}
|
|
||||||
const mark = Decoration.mark({
|
|
||||||
attributes: { style: `outline: 2px solid #FFCA28;` },
|
|
||||||
});
|
|
||||||
return mark.range(from, to);
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.flat()
|
|
||||||
.filter(Boolean) || [];
|
|
||||||
highlights = Decoration.set(marks, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return highlights;
|
|
||||||
} catch (err) {
|
|
||||||
// console.warn('highlighting error', err);
|
|
||||||
return Decoration.set([]);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
provide: (f) => EditorView.decorations.from(f),
|
|
||||||
});
|
|
||||||
|
|
||||||
// helper to simply trigger highlighting for given haps
|
|
||||||
export function highlightHaps(view, haps) {
|
|
||||||
view.dispatch({ effects: setHighlights.of({ haps }) });
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// flash
|
|
||||||
//
|
|
||||||
|
|
||||||
export const setFlash = StateEffect.define();
|
|
||||||
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) {
|
|
||||||
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);
|
|
||||||
};
|
|
||||||
|
|
||||||
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);
|
|
||||||
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) {
|
|
||||||
highlightHaps(this.editor, haps);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@strudel/codemirror",
|
|
||||||
"version": "0.8.4",
|
|
||||||
"description": "Codemirror Extensions for Strudel",
|
|
||||||
"main": "codemirror.mjs",
|
|
||||||
"publishConfig": {
|
|
||||||
"main": "dist/index.js",
|
|
||||||
"module": "dist/index.mjs"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"build": "vite build",
|
|
||||||
"prepublishOnly": "npm run build"
|
|
||||||
},
|
|
||||||
"type": "module",
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git+https://github.com/tidalcycles/strudel.git"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"tidalcycles",
|
|
||||||
"strudel",
|
|
||||||
"pattern",
|
|
||||||
"livecoding",
|
|
||||||
"algorave"
|
|
||||||
],
|
|
||||||
"author": "Felix Roos <flix91@gmail.com>",
|
|
||||||
"contributors": [
|
|
||||||
"Alex McLean <alex@slab.org>"
|
|
||||||
],
|
|
||||||
"license": "AGPL-3.0-or-later",
|
|
||||||
"bugs": {
|
|
||||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
|
||||||
},
|
|
||||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
|
||||||
"dependencies": {
|
|
||||||
"@codemirror/commands": "^6.2.4",
|
|
||||||
"@codemirror/lang-javascript": "^6.1.7",
|
|
||||||
"@codemirror/language": "^6.6.0",
|
|
||||||
"@codemirror/state": "^6.2.0",
|
|
||||||
"@codemirror/view": "^6.10.0",
|
|
||||||
"@lezer/highlight": "^1.1.4",
|
|
||||||
"@strudel.cycles/core": "workspace:*"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"vite": "^4.3.3"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,19 +0,0 @@
|
|||||||
import { defineConfig } from 'vite';
|
|
||||||
import { dependencies } from './package.json';
|
|
||||||
import { resolve } from 'path';
|
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [],
|
|
||||||
build: {
|
|
||||||
lib: {
|
|
||||||
entry: resolve(__dirname, 'codemirror.mjs'),
|
|
||||||
formats: ['es', 'cjs'],
|
|
||||||
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
|
|
||||||
},
|
|
||||||
rollupOptions: {
|
|
||||||
external: [...Object.keys(dependencies)],
|
|
||||||
},
|
|
||||||
target: 'esnext',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -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';
|
||||||
|
|||||||
@@ -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 = {};
|
||||||
@@ -222,7 +222,6 @@ const generic_params = [
|
|||||||
*
|
*
|
||||||
* @name legato
|
* @name legato
|
||||||
* @param {number | Pattern} duration between 0 and 1, where 1 is the length of the whole hap time
|
* @param {number | Pattern} duration between 0 and 1, where 1 is the length of the whole hap time
|
||||||
* @noAutocomplete
|
|
||||||
* @example
|
* @example
|
||||||
* "c4 eb4 g4 bb4".legato("<0.125 .25 .5 .75 1 2 4>")
|
* "c4 eb4 g4 bb4".legato("<0.125 .25 .5 .75 1 2 4>")
|
||||||
*
|
*
|
||||||
@@ -750,14 +749,13 @@ 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'],
|
||||||
@@ -812,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;
|
||||||
|
|||||||
@@ -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/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ export const getDrawContext = (id = 'test-canvas') => {
|
|||||||
canvas.id = id;
|
canvas.id = id;
|
||||||
canvas.width = window.innerWidth;
|
canvas.width = window.innerWidth;
|
||||||
canvas.height = window.innerHeight;
|
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);
|
||||||
}
|
}
|
||||||
return canvas.getContext('2d');
|
return canvas.getContext('2d');
|
||||||
@@ -65,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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
# Logs
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
pnpm-debug.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
|
|
||||||
node_modules
|
|
||||||
dist
|
|
||||||
dist-ssr
|
|
||||||
*.local
|
|
||||||
|
|
||||||
# Editor directories and files
|
|
||||||
.vscode/*
|
|
||||||
!.vscode/extensions.json
|
|
||||||
.idea
|
|
||||||
.DS_Store
|
|
||||||
*.suo
|
|
||||||
*.ntvs*
|
|
||||||
*.njsproj
|
|
||||||
*.sln
|
|
||||||
*.sw?
|
|
||||||
@@ -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:*",
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/core",
|
"name": "@strudel.cycles/core",
|
||||||
"version": "0.8.2",
|
"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.28.0"
|
"vitest": "^0.25.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1582,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
|
||||||
|
|
||||||
@@ -1695,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));
|
||||||
@@ -1724,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));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2053,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'],
|
||||||
@@ -2203,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
|
||||||
@@ -2276,7 +2251,7 @@ const _loopAt = function (factor, pat, cps = 1) {
|
|||||||
* 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)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
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) =>
|
||||||
@@ -2304,7 +2279,7 @@ export const slice = register(
|
|||||||
* s("breaks165").splice(8, "0 1 [2 3 0]@2 3 0@2 7").hurry(0.65)
|
* s("breaks165").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);
|
||||||
@@ -2321,10 +2296,7 @@ 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);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2341,6 +2313,6 @@ export const { loopAt, loopat } = register(['loopAt', 'loopat'], function (facto
|
|||||||
* 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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ Pattern.prototype.pianoroll = function ({
|
|||||||
timeframe: timeframeProp,
|
timeframe: timeframeProp,
|
||||||
fold = 0,
|
fold = 0,
|
||||||
vertical = 0,
|
vertical = 0,
|
||||||
labels = 0,
|
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const ctx = getDrawContext();
|
const ctx = getDrawContext();
|
||||||
const w = ctx.canvas.width;
|
const w = ctx.canvas.width;
|
||||||
@@ -83,12 +82,12 @@ Pattern.prototype.pianoroll = function ({
|
|||||||
ctx.fillRect(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;
|
||||||
events.filter(inFrame).forEach((event) => {
|
events.filter(inFrame).forEach((event) => {
|
||||||
const isActive = event.whole.begin <= t && event.endClipped > t;
|
const isActive = event.whole.begin <= t && event.whole.end > t;
|
||||||
ctx.fillStyle = event.context?.color || inactive;
|
ctx.fillStyle = event.context?.color || inactive;
|
||||||
ctx.strokeStyle = event.context?.color || active;
|
ctx.strokeStyle = event.context?.color || active;
|
||||||
ctx.globalAlpha = event.context.velocity ?? event.value?.gain ?? 1;
|
ctx.globalAlpha = event.context.velocity ?? 1;
|
||||||
const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange);
|
const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange);
|
||||||
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
|
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
|
||||||
const value = getValue(event);
|
const value = getValue(event);
|
||||||
@@ -115,14 +114,6 @@ Pattern.prototype.pianoroll = function ({
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords);
|
isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords);
|
||||||
if (labels) {
|
|
||||||
const label = event.value.note ?? event.value.s + (event.value.n ? `:${event.value.n}` : '');
|
|
||||||
ctx.font = `${barThickness * 0.75}px monospace`;
|
|
||||||
ctx.strokeStyle = 'black';
|
|
||||||
ctx.fillStyle = isActive ? 'white' : '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);
|
||||||
@@ -190,7 +181,6 @@ export function pianoroll({
|
|||||||
timeframe: timeframeProp,
|
timeframe: timeframeProp,
|
||||||
fold = 0,
|
fold = 0,
|
||||||
vertical = 0,
|
vertical = 0,
|
||||||
labels = false,
|
|
||||||
ctx,
|
ctx,
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const w = ctx.canvas.width;
|
const w = ctx.canvas.width;
|
||||||
@@ -250,7 +240,7 @@ export function pianoroll({
|
|||||||
const color = event.value?.color || event.context?.color;
|
const color = event.value?.color || event.context?.color;
|
||||||
ctx.fillStyle = color || inactive;
|
ctx.fillStyle = color || inactive;
|
||||||
ctx.strokeStyle = color || active;
|
ctx.strokeStyle = color || active;
|
||||||
ctx.globalAlpha = event.context.velocity ?? event.value?.gain ?? 1;
|
ctx.globalAlpha = event.context.velocity ?? 1;
|
||||||
const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange);
|
const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange);
|
||||||
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
|
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
|
||||||
const value = getValue(event);
|
const value = getValue(event);
|
||||||
@@ -277,14 +267,6 @@ export function pianoroll({
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords);
|
isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords);
|
||||||
if (labels) {
|
|
||||||
const label = event.value.note ?? event.value.s + (event.value.n ? `:${event.value.n}` : '');
|
|
||||||
ctx.font = `${barThickness * 0.75}px monospace`;
|
|
||||||
ctx.strokeStyle = 'black';
|
|
||||||
ctx.fillStyle = isActive ? 'white' : '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);
|
||||||
@@ -302,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;
|
||||||
@@ -311,18 +293,5 @@ export function getDrawOptions(drawTime, options = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Pattern.prototype.punchcard = function (options) {
|
Pattern.prototype.punchcard = function (options) {
|
||||||
return this.onPaint((ctx, time, haps, drawTime) =>
|
return this.onPaint((ctx, time, haps, drawTime) => pianoroll({ ctx, time, haps, ...getOptions(drawTime, options) }));
|
||||||
pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, 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 });
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
@@ -38,7 +46,8 @@ export function repl({
|
|||||||
let { pattern } = 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;
|
||||||
|
scheduler.setPattern(pattern, autostart);
|
||||||
afterEval?.({ code, pattern });
|
afterEval?.({ code, pattern });
|
||||||
return pattern;
|
return pattern;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -51,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');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -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()
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,117 +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 }) {
|
|
||||||
ctx.clearRect(0, 0, ctx.canvas.clientWidth, ctx.canvas.clientHeight);
|
|
||||||
const [cx, cy] = [ctx.canvas.width / 2, ctx.canvas.height / 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 }));
|
|
||||||
};
|
|
||||||
@@ -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);
|
|
||||||
});
|
|
||||||
@@ -6,12 +6,12 @@ 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 [];
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,7 @@ export const noteToMidi = (note) => {
|
|||||||
throw new Error('not a note: "' + note + '"');
|
throw new Error('not a note: "' + note + '"');
|
||||||
}
|
}
|
||||||
const chroma = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 }[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 + { '#': 1, b: -1, s: 1, f: -1 }[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) => {
|
||||||
@@ -67,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;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -213,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;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/csound",
|
"name": "@strudel.cycles/csound",
|
||||||
"version": "0.8.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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/eval",
|
"name": "@strudel.cycles/eval",
|
||||||
"version": "0.8.0",
|
"version": "0.7.1",
|
||||||
"description": "Code evaluator for strudel",
|
"description": "Code evaluator for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@strudel.cycles/mini": "workspace:*",
|
"@strudel.cycles/mini": "workspace:*",
|
||||||
"vite": "^4.3.3",
|
"vite": "^3.2.2",
|
||||||
"vitest": "^0.28.0"
|
"vitest": "^0.25.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ Pattern.prototype.midi = function (output) {
|
|||||||
hap.ensureObjectValue();
|
hap.ensureObjectValue();
|
||||||
|
|
||||||
// calculate time
|
// calculate time
|
||||||
const timingOffset = WebMidi.time - getAudioContext().getOutputTimestamp().contextTime * 1000;
|
const timingOffset = WebMidi.time - getAudioContext().currentTime * 1000;
|
||||||
time = time * 1000 + timingOffset;
|
time = time * 1000 + timingOffset;
|
||||||
|
|
||||||
// destructure value
|
// destructure value
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/midi",
|
"name": "@strudel.cycles/midi",
|
||||||
"version": "0.8.0",
|
"version": "0.7.1",
|
||||||
"description": "Midi API for strudel",
|
"description": "Midi API for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
@@ -31,9 +31,9 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel.cycles/core": "workspace:*",
|
"@strudel.cycles/core": "workspace:*",
|
||||||
"@strudel.cycles/webaudio": "workspace:*",
|
"@strudel.cycles/webaudio": "workspace:*",
|
||||||
"webmidi": "^3.1.5"
|
"webmidi": "^3.0.21"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^4.3.3"
|
"vite": "^3.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Generated by Peggy 3.0.2.
|
// Generated by Peggy 2.0.1.
|
||||||
//
|
//
|
||||||
// https://peggyjs.org/
|
// https://peggyjs.org/
|
||||||
|
|
||||||
@@ -43,19 +43,16 @@ peg$SyntaxError.prototype.format = function(sources) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
var s = this.location.start;
|
var s = this.location.start;
|
||||||
var offset_s = (this.location.source && (typeof this.location.source.offset === "function"))
|
var loc = this.location.source + ":" + s.line + ":" + s.column;
|
||||||
? this.location.source.offset(s)
|
|
||||||
: s;
|
|
||||||
var loc = this.location.source + ":" + offset_s.line + ":" + offset_s.column;
|
|
||||||
if (src) {
|
if (src) {
|
||||||
var e = this.location.end;
|
var e = this.location.end;
|
||||||
var filler = peg$padEnd("", offset_s.line.toString().length, ' ');
|
var filler = peg$padEnd("", s.line.toString().length, ' ');
|
||||||
var line = src[s.line - 1];
|
var line = src[s.line - 1];
|
||||||
var last = s.line === e.line ? e.column : line.length + 1;
|
var last = s.line === e.line ? e.column : line.length + 1;
|
||||||
var hatLen = (last - s.column) || 1;
|
var hatLen = (last - s.column) || 1;
|
||||||
str += "\n --> " + loc + "\n"
|
str += "\n --> " + loc + "\n"
|
||||||
+ filler + " |\n"
|
+ filler + " |\n"
|
||||||
+ offset_s.line + " | " + line + "\n"
|
+ s.line + " | " + line + "\n"
|
||||||
+ filler + " | " + peg$padEnd("", s.column - 1, ' ')
|
+ filler + " | " + peg$padEnd("", s.column - 1, ' ')
|
||||||
+ peg$padEnd("", hatLen, "^");
|
+ peg$padEnd("", hatLen, "^");
|
||||||
} else {
|
} else {
|
||||||
@@ -425,11 +422,11 @@ function peg$parse(input, options) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function peg$computeLocation(startPos, endPos, offset) {
|
function peg$computeLocation(startPos, endPos) {
|
||||||
var startPosDetails = peg$computePosDetails(startPos);
|
var startPosDetails = peg$computePosDetails(startPos);
|
||||||
var endPosDetails = peg$computePosDetails(endPos);
|
var endPosDetails = peg$computePosDetails(endPos);
|
||||||
|
|
||||||
var res = {
|
return {
|
||||||
source: peg$source,
|
source: peg$source,
|
||||||
start: {
|
start: {
|
||||||
offset: startPos,
|
offset: startPos,
|
||||||
@@ -442,11 +439,6 @@ function peg$parse(input, options) {
|
|||||||
column: endPosDetails.column
|
column: endPosDetails.column
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (offset && peg$source && (typeof peg$source.offset === "function")) {
|
|
||||||
res.start = peg$source.offset(res.start);
|
|
||||||
res.end = peg$source.offset(res.end);
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function peg$fail(expected) {
|
function peg$fail(expected) {
|
||||||
|
|||||||
@@ -190,8 +190,3 @@ export function minify(thing) {
|
|||||||
}
|
}
|
||||||
return strudel.reify(thing);
|
return strudel.reify(thing);
|
||||||
}
|
}
|
||||||
|
|
||||||
// calling this function will cause patterns to parse strings as mini notation by default
|
|
||||||
export function miniAllStrings() {
|
|
||||||
strudel.setStringParser(mini);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/mini",
|
"name": "@strudel.cycles/mini",
|
||||||
"version": "0.8.2",
|
"version": "0.7.2",
|
||||||
"description": "Mini notation for strudel",
|
"description": "Mini notation for strudel",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -35,8 +35,8 @@
|
|||||||
"@strudel.cycles/core": "workspace:*"
|
"@strudel.cycles/core": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"peggy": "^3.0.2",
|
"peggy": "^2.0.1",
|
||||||
"vite": "^4.3.3",
|
"vite": "^3.2.2",
|
||||||
"vitest": "^0.28.0"
|
"vitest": "^0.25.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/osc",
|
"name": "@strudel.cycles/osc",
|
||||||
"version": "0.8.0",
|
"version": "0.7.1",
|
||||||
"description": "OSC messaging for strudel",
|
"description": "OSC messaging for strudel",
|
||||||
"main": "osc.mjs",
|
"main": "osc.mjs",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
"osc-js": "^2.4.0"
|
"osc-js": "^2.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"pkg": "^5.8.1",
|
"pkg": "^5.7.0",
|
||||||
"vite": "^4.3.3"
|
"vite": "^3.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||