Merge remote-tracking branch 'origin/main' into in-source-doc
This commit is contained in:
commit
b50daed0d0
112 changed files with 16311 additions and 41178 deletions
|
|
@ -14,4 +14,6 @@ export * from './signal.mjs';
|
|||
export * from './state.mjs';
|
||||
export * from './timespan.mjs';
|
||||
export * from './util.mjs';
|
||||
export * from './speak.mjs';
|
||||
export * as gist from './gist.js';
|
||||
// export * from './value.mjs';
|
||||
|
|
|
|||
2
packages/core/package-lock.json
generated
2
packages/core/package-lock.json
generated
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@strudel.cycles/core",
|
||||
"version": "0.0.5",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@strudel.cycles/core",
|
||||
"version": "0.0.5",
|
||||
"version": "0.1.0",
|
||||
"description": "Port of Tidal Cycles to JavaScript",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
|
|
|
|||
|
|
@ -450,7 +450,7 @@ export class Pattern {
|
|||
// set context type to midi to let the player know its meant as midi number and not as frequency
|
||||
return new Hap(hap.whole, hap.part, toMidi(hap.value), { ...hap.context, type: 'midi' });
|
||||
}
|
||||
if (dropfail) {
|
||||
if (dropfails) {
|
||||
// return 'nothing'
|
||||
return undefined;
|
||||
}
|
||||
|
|
@ -461,10 +461,6 @@ export class Pattern {
|
|||
throw new Error('cannot parse as number: "' + hap.value + '"');
|
||||
return hap;
|
||||
});
|
||||
if (dropfail) {
|
||||
return result._removeUndefineds();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -744,6 +740,14 @@ export class Pattern {
|
|||
return this._fast(Fraction(1).div(factor));
|
||||
}
|
||||
|
||||
_inside(factor, f) {
|
||||
return f(this._slow(factor))._fast(factor);
|
||||
}
|
||||
|
||||
_outside(factor, f) {
|
||||
return f(this._fast(factor))._slow(factor);
|
||||
}
|
||||
|
||||
_ply(factor) {
|
||||
return this.fmap((x) => pure(x)._fast(factor))._squeezeJoin();
|
||||
}
|
||||
|
|
@ -870,6 +874,16 @@ export class Pattern {
|
|||
return slowcatPrime(...pats);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new pattern where every other cycle is played once, twice as
|
||||
* fast, and offset in time by one quarter of a cycle. Creates a kind of
|
||||
* breakbeat feel.
|
||||
* @returns Pattern
|
||||
*/
|
||||
brak() {
|
||||
return this.when(slowcat(false, true), (x) => fastcat(x, silence)._late(0.25));
|
||||
}
|
||||
|
||||
rev() {
|
||||
const pat = this;
|
||||
const query = function (state) {
|
||||
|
|
@ -1007,6 +1021,10 @@ export class Pattern {
|
|||
_velocity(velocity) {
|
||||
return this._withContext((context) => ({ ...context, velocity: (context.velocity || 1) * velocity }));
|
||||
}
|
||||
|
||||
_loopAt(factor,cps=1) {
|
||||
return this.speed((1/factor)*cps).unit("c").slow(factor)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO - adopt value.mjs fully..
|
||||
|
|
@ -1430,6 +1448,10 @@ Pattern.prototype.chunkBack = function (...args) {
|
|||
args = args.map(reify);
|
||||
return patternify2(Pattern.prototype._chunkBack)(...args, this);
|
||||
};
|
||||
Pattern.prototype.loopAt = function (...args) {
|
||||
args = args.map(reify);
|
||||
return patternify2(Pattern.prototype._loopAt)(...args, this);
|
||||
};
|
||||
Pattern.prototype.zoom = function (...args) {
|
||||
args = args.map(reify);
|
||||
return patternify2(Pattern.prototype._zoom)(...args, this);
|
||||
|
|
@ -1438,6 +1460,14 @@ Pattern.prototype.compress = function (...args) {
|
|||
args = args.map(reify);
|
||||
return patternify2(Pattern.prototype._compress)(...args, this);
|
||||
};
|
||||
Pattern.prototype.outside = function (...args) {
|
||||
args = args.map(reify);
|
||||
return patternify2(Pattern.prototype._outside)(...args, this);
|
||||
};
|
||||
Pattern.prototype.inside = function (...args) {
|
||||
args = args.map(reify);
|
||||
return patternify2(Pattern.prototype._inside)(...args, this);
|
||||
};
|
||||
|
||||
// call this after all Patter.prototype.define calls have been executed! (right before evaluate)
|
||||
Pattern.prototype.bootstrap = function () {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,13 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||
|
||||
import { Pattern, patternify2 } from './index.mjs';
|
||||
|
||||
const synth = window?.speechSynthesis;
|
||||
let synth;
|
||||
try {
|
||||
synth = window?.speechSynthesis;
|
||||
} catch (err) {
|
||||
console.warn('cannot use window: not in browser?');
|
||||
}
|
||||
|
||||
let allVoices = synth?.getVoices();
|
||||
// console.log('voices', allVoices);
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ import {
|
|||
tri2,
|
||||
id,
|
||||
ply,
|
||||
rev
|
||||
} from '../index.mjs';
|
||||
|
||||
import { steady } from '../signal.mjs';
|
||||
|
|
@ -277,10 +278,7 @@ describe('Pattern', function () {
|
|||
);
|
||||
});
|
||||
it('can SqueezeOut() structure', () => {
|
||||
sameFirst(
|
||||
sequence(1, [2, 3]).keepifSqueezeOut(true, true, false),
|
||||
sequence([1, [2, 3]], [1, [2, 3]], silence),
|
||||
);
|
||||
sameFirst(sequence(1, [2, 3]).keepifSqueezeOut(true, true, false), sequence([1, [2, 3]], [1, [2, 3]], silence));
|
||||
});
|
||||
});
|
||||
describe('sub()', function () {
|
||||
|
|
@ -439,6 +437,20 @@ describe('Pattern', function () {
|
|||
// mini('eb3 [c3 g3]/2 ') always plays [c3 g3]
|
||||
});
|
||||
});
|
||||
describe('inside', () => {
|
||||
it('can rev inside a cycle', () => {
|
||||
sameFirst(sequence('a', 'b', 'c', 'd').inside(2, rev),
|
||||
sequence('b', 'a', 'd', 'c')
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('outside', () => {
|
||||
it('can rev outside a cycle', () => {
|
||||
sameFirst(sequence('a', 'b', 'c', 'd')._slow(2).outside(2, rev),
|
||||
sequence('d', 'c')
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('_filterValues()', function () {
|
||||
it('Filters true', function () {
|
||||
assert.equal(
|
||||
|
|
@ -589,6 +601,11 @@ describe('Pattern', function () {
|
|||
);
|
||||
});
|
||||
});
|
||||
describe('brak()', () => {
|
||||
it('Can make something a bit breakbeaty', () => {
|
||||
sameFirst(sequence('a', 'b').brak()._fast(2), sequence('a', 'b', fastcat(silence, 'a'), fastcat('b', silence)));
|
||||
});
|
||||
});
|
||||
describe('timeCat()', function () {
|
||||
it('Can concatenate patterns with different relative durations', function () {
|
||||
assert.deepStrictEqual(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@strudel.cycles/embed",
|
||||
"version": "0.0.2",
|
||||
"version": "0.1.0",
|
||||
"description": "Embeddable Web Component to load a Strudel REPL into an iframe",
|
||||
"main": "embed.js",
|
||||
"type": "module",
|
||||
|
|
@ -20,6 +20,5 @@
|
|||
"bugs": {
|
||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {}
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ npm i @strudel.cycles/eval --save
|
|||
|
||||
## Example
|
||||
|
||||
<!-- TODO: -extend +evalScope -->
|
||||
|
||||
```js
|
||||
import { evaluate, extend } from '@strudel.cycles/eval';
|
||||
import * as strudel from '@strudel.cycles/core';
|
||||
|
|
|
|||
|
|
@ -7,17 +7,39 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||
import shapeshifter from './shapeshifter.mjs';
|
||||
import * as strudel from '@strudel.cycles/core';
|
||||
|
||||
const { isPattern } = strudel;
|
||||
const { isPattern, Pattern } = strudel;
|
||||
|
||||
export const extend = (...args) => {
|
||||
// TODO: find a way to make args available to eval without adding it to global scope...
|
||||
// sadly, "with" does not work in strict mode
|
||||
console.warn('@strudel.cycles/eval extend is deprecated, please use evalScope instead');
|
||||
Object.assign(globalThis, ...args);
|
||||
};
|
||||
|
||||
let scoped = false;
|
||||
export const evalScope = async (...args) => {
|
||||
if (scoped) {
|
||||
console.warn('@strudel.cycles/eval evalScope was called more than once.');
|
||||
}
|
||||
scoped = true;
|
||||
const results = await Promise.allSettled(args);
|
||||
const modules = results.filter((result) => result.status === 'fulfilled').map((r) => r.value);
|
||||
results.forEach((result, i) => {
|
||||
if (result.status === 'rejected') {
|
||||
console.warn(`evalScope: module with index ${i} could not be loaded:`, result.reason);
|
||||
}
|
||||
});
|
||||
Object.assign(globalThis, ...modules, Pattern.prototype.bootstrap());
|
||||
};
|
||||
|
||||
function safeEval(str) {
|
||||
return Function('"use strict";return (' + str + ')')();
|
||||
}
|
||||
|
||||
export const evaluate = async (code) => {
|
||||
if (!scoped) {
|
||||
await evalScope(); // at least scope Pattern.prototype.boostrap
|
||||
}
|
||||
const shapeshifted = shapeshifter(code); // transform syntactically correct js code to semantically usable code
|
||||
let evaluated = await eval(shapeshifted);
|
||||
let evaluated = await safeEval(shapeshifted);
|
||||
if (!isPattern(evaluated)) {
|
||||
console.log('evaluated', evaluated);
|
||||
const message = `got "${typeof evaluated}" instead of pattern`;
|
||||
|
|
|
|||
1
packages/eval/index.mjs
Normal file
1
packages/eval/index.mjs
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './evaluate.mjs';
|
||||
2
packages/eval/package-lock.json
generated
2
packages/eval/package-lock.json
generated
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@strudel.cycles/eval",
|
||||
"version": "0.0.5",
|
||||
"version": "0.1.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
{
|
||||
"name": "@strudel.cycles/eval",
|
||||
"version": "0.0.5",
|
||||
"version": "0.1.1",
|
||||
"description": "Code evaluator for strudel",
|
||||
"main": "evaluate.mjs",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
"directories": {
|
||||
"test": "test"
|
||||
},
|
||||
|
|
@ -13,7 +14,6 @@
|
|||
"type": "git",
|
||||
"url": "git+https://github.com/tidalcycles/strudel.git"
|
||||
},
|
||||
"type": "module",
|
||||
"keywords": [
|
||||
"tidalcycles",
|
||||
"strudel",
|
||||
|
|
@ -28,7 +28,7 @@
|
|||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "^0.0.5",
|
||||
"@strudel.cycles/core": "^0.1.0",
|
||||
"estraverse": "^5.3.0",
|
||||
"shift-ast": "^6.1.0",
|
||||
"shift-codegen": "^7.0.3",
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import * as strudel from '@strudel.cycles/core';
|
|||
const { fastcat } = strudel;
|
||||
|
||||
extend({ mini }, strudel);
|
||||
// TODO: test evalScope
|
||||
|
||||
describe('evaluate', () => {
|
||||
const ev = async (code) => (await evaluate(code)).pattern._firstCycleValues;
|
||||
|
|
|
|||
3
packages/midi/index.mjs
Normal file
3
packages/midi/index.mjs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import './midi.mjs';
|
||||
|
||||
export * from './midi.mjs';
|
||||
2
packages/midi/package-lock.json
generated
2
packages/midi/package-lock.json
generated
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@strudel.cycles/midi",
|
||||
"version": "0.0.6",
|
||||
"version": "0.1.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"name": "@strudel.cycles/midi",
|
||||
"version": "0.0.6",
|
||||
"version": "0.1.1",
|
||||
"description": "Midi API for strudel",
|
||||
"main": "midi.mjs",
|
||||
"main": "index.mjs",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tidalcycles/strudel.git"
|
||||
|
|
@ -21,7 +21,7 @@
|
|||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/tone": "^0.0.6",
|
||||
"@strudel.cycles/tone": "^0.1.1",
|
||||
"tone": "^14.7.77",
|
||||
"webmidi": "^2.5.2"
|
||||
}
|
||||
|
|
|
|||
2
packages/mini/index.mjs
Normal file
2
packages/mini/index.mjs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from './mini.mjs';
|
||||
export * from './krill-parser.js';
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"name": "@strudel.cycles/mini",
|
||||
"version": "0.0.7",
|
||||
"version": "0.1.1",
|
||||
"description": "Mini notation for strudel",
|
||||
"main": "mini.mjs",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "mocha --colors"
|
||||
|
|
@ -25,8 +25,8 @@
|
|||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "^0.0.5",
|
||||
"@strudel.cycles/eval": "^0.0.5",
|
||||
"@strudel.cycles/tone": "^0.0.6"
|
||||
"@strudel.cycles/core": "^0.1.0",
|
||||
"@strudel.cycles/eval": "^0.1.1",
|
||||
"@strudel.cycles/tone": "^0.1.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,15 +10,19 @@ import { Pattern } from '@strudel.cycles/core';
|
|||
const comm = new OSC();
|
||||
comm.open();
|
||||
const latency = 0.1;
|
||||
let startedAt = -1;
|
||||
|
||||
Pattern.prototype.osc = function () {
|
||||
return this._withHap((hap) => {
|
||||
const onTrigger = (time, hap, currentTime) => {
|
||||
const onTrigger = (time, hap, currentTime, cps, cycle, delta) => {
|
||||
// time should be audio time of onset
|
||||
// currentTime should be current time of audio context (slightly before time)
|
||||
const keyvals = Object.entries(hap.value).flat();
|
||||
const offset = (time - currentTime + latency) * 1000;
|
||||
const ts = Math.floor(Date.now() + offset);
|
||||
if (startedAt < 0) {
|
||||
startedAt = Date.now() - (currentTime * 1000);
|
||||
}
|
||||
const controls = Object.assign({}, { cps: cps, cycle: cycle, delta: delta }, hap.value);
|
||||
const keyvals = Object.entries(controls).flat();
|
||||
const ts = Math.floor(startedAt + ((time + latency) * 1000));
|
||||
const message = new OSC.Message('/dirt/play', ...keyvals);
|
||||
const bundle = new OSC.Bundle([message], ts);
|
||||
bundle.timestamp(ts); // workaround for https://github.com/adzialocha/osc-js/issues/60
|
||||
|
|
|
|||
2
packages/osc/package-lock.json
generated
2
packages/osc/package-lock.json
generated
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@strudel.cycles/osc",
|
||||
"version": "0.0.2",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@strudel.cycles/osc",
|
||||
"version": "0.0.2",
|
||||
"version": "0.1.0",
|
||||
"description": "OSC messaging for strudel",
|
||||
"main": "osc.mjs",
|
||||
"scripts": {
|
||||
|
|
@ -21,7 +21,9 @@
|
|||
"algorave"
|
||||
],
|
||||
"author": "Felix Roos <flix91@gmail.com>",
|
||||
"contributors": ["Alex McLean <alex@slab.org>"],
|
||||
"contributors": [
|
||||
"Alex McLean <alex@slab.org>"
|
||||
],
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"bugs": {
|
||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||
|
|
|
|||
25
packages/react/.gitignore
vendored
Normal file
25
packages/react/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
!dist
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
4
packages/react/README.md
Normal file
4
packages/react/README.md
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# @strudel.cycles/react
|
||||
|
||||
This package contains react hooks and components for strudel.
|
||||
Example coming soon
|
||||
3
packages/react/dist/index.cjs.js
vendored
Normal file
3
packages/react/dist/index.cjs.js
vendored
Normal file
File diff suppressed because one or more lines are too long
620
packages/react/dist/index.es.js
vendored
Normal file
620
packages/react/dist/index.es.js
vendored
Normal file
|
|
@ -0,0 +1,620 @@
|
|||
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
||||
import { CodeMirror as CodeMirror$1 } from 'react-codemirror6';
|
||||
import { EditorView, Decoration } from '@codemirror/view';
|
||||
import { StateEffect, StateField } from '@codemirror/state';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { HighlightStyle, tags } from '@codemirror/highlight';
|
||||
import { useInView } from 'react-hook-inview';
|
||||
import { evaluate } from '@strudel.cycles/eval';
|
||||
import { getPlayableNoteValue } from '@strudel.cycles/core/util.mjs';
|
||||
import { Tone } from '@strudel.cycles/tone';
|
||||
import { TimeSpan, State } from '@strudel.cycles/core';
|
||||
import { WebMidi, enableWebMidi } from '@strudel.cycles/midi';
|
||||
|
||||
/*
|
||||
Credits for color palette:
|
||||
|
||||
Author: Mattia Astorino (http://github.com/equinusocio)
|
||||
Website: https://material-theme.site/
|
||||
*/
|
||||
|
||||
const ivory = '#abb2bf',
|
||||
stone = '#7d8799', // Brightened compared to original to increase contrast
|
||||
invalid = '#ffffff',
|
||||
darkBackground = '#21252b',
|
||||
highlightBackground = 'rgba(0, 0, 0, 0.5)',
|
||||
// background = '#292d3e',
|
||||
background = 'transparent',
|
||||
tooltipBackground = '#353a42',
|
||||
selection = 'rgba(128, 203, 196, 0.2)',
|
||||
cursor = '#ffcc00';
|
||||
|
||||
/// The editor theme styles for Material Palenight.
|
||||
const materialPalenightTheme = EditorView.theme(
|
||||
{
|
||||
// done
|
||||
'&': {
|
||||
color: '#ffffff',
|
||||
backgroundColor: background,
|
||||
fontSize: '15px',
|
||||
'z-index': 11,
|
||||
},
|
||||
|
||||
// done
|
||||
'.cm-content': {
|
||||
caretColor: cursor,
|
||||
lineHeight: '22px',
|
||||
},
|
||||
'.cm-line': {
|
||||
background: '#2C323699',
|
||||
},
|
||||
// done
|
||||
'&.cm-focused .cm-cursor': {
|
||||
borderLeftColor: cursor,
|
||||
},
|
||||
|
||||
'&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': {
|
||||
backgroundColor: selection,
|
||||
},
|
||||
|
||||
'.cm-panels': { backgroundColor: darkBackground, color: '#ffffff' },
|
||||
'.cm-panels.cm-panels-top': { borderBottom: '2px solid black' },
|
||||
'.cm-panels.cm-panels-bottom': { borderTop: '2px solid black' },
|
||||
|
||||
// done, use onedarktheme
|
||||
'.cm-searchMatch': {
|
||||
backgroundColor: '#72a1ff59',
|
||||
outline: '1px solid #457dff',
|
||||
},
|
||||
'.cm-searchMatch.cm-searchMatch-selected': {
|
||||
backgroundColor: '#6199ff2f',
|
||||
},
|
||||
|
||||
'.cm-activeLine': { backgroundColor: highlightBackground },
|
||||
'.cm-selectionMatch': { backgroundColor: '#aafe661a' },
|
||||
|
||||
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
|
||||
backgroundColor: '#bad0f847',
|
||||
outline: '1px solid #515a6b',
|
||||
},
|
||||
|
||||
// done
|
||||
'.cm-gutters': {
|
||||
background: '#2C323699',
|
||||
color: '#676e95',
|
||||
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 Material Palenight theme.
|
||||
const materialPalenightHighlightStyle = HighlightStyle.define([
|
||||
{ tag: tags.keyword, color: '#c792ea' },
|
||||
{ tag: tags.operator, color: '#89ddff' },
|
||||
{ tag: tags.special(tags.variableName), color: '#eeffff' },
|
||||
{ tag: tags.typeName, color: '#f07178' },
|
||||
{ tag: tags.atom, color: '#f78c6c' },
|
||||
{ tag: tags.number, color: '#ff5370' },
|
||||
{ tag: tags.definition(tags.variableName), color: '#82aaff' },
|
||||
{ tag: tags.string, color: '#c3e88d' },
|
||||
{ tag: tags.special(tags.string), color: '#f07178' },
|
||||
{ tag: tags.comment, color: stone },
|
||||
{ tag: tags.variableName, color: '#f07178' },
|
||||
{ tag: tags.tagName, color: '#ff5370' },
|
||||
{ tag: tags.bracket, color: '#a2a1a4' },
|
||||
{ tag: tags.meta, color: '#ffcb6b' },
|
||||
{ tag: tags.attributeName, color: '#c792ea' },
|
||||
{ tag: tags.propertyName, color: '#c792ea' },
|
||||
{ tag: tags.className, color: '#decb6b' },
|
||||
{ tag: tags.invalid, color: invalid },
|
||||
]);
|
||||
|
||||
/// Extension to enable the Material Palenight theme (both the editor theme and
|
||||
/// the highlight style).
|
||||
// : Extension
|
||||
const materialPalenight = [materialPalenightTheme, materialPalenightHighlightStyle];
|
||||
|
||||
const setHighlights = StateEffect.define();
|
||||
const highlightField = StateField.define({
|
||||
create() {
|
||||
return Decoration.none;
|
||||
},
|
||||
update(highlights, tr) {
|
||||
try {
|
||||
for (let e of tr.effects) {
|
||||
if (e.is(setHighlights)) {
|
||||
highlights = Decoration.set(e.value.flatMap((hap) => (hap.context.locations || []).map(({ start, end }) => {
|
||||
const color = hap.context.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;
|
||||
}
|
||||
const mark = Decoration.mark({ attributes: { style: `outline: 1px solid ${color}` } });
|
||||
return mark.range(from, to);
|
||||
})).filter(Boolean), true);
|
||||
}
|
||||
}
|
||||
return highlights;
|
||||
} catch (err) {
|
||||
return highlights;
|
||||
}
|
||||
},
|
||||
provide: (f) => EditorView.decorations.from(f)
|
||||
});
|
||||
function CodeMirror({ value, onChange, onViewChanged, onCursor, options, editorDidMount }) {
|
||||
return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(CodeMirror$1, {
|
||||
onViewChange: onViewChanged,
|
||||
style: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flex: "1 0 auto"
|
||||
},
|
||||
value,
|
||||
onChange,
|
||||
extensions: [
|
||||
javascript(),
|
||||
materialPalenight,
|
||||
highlightField
|
||||
]
|
||||
}));
|
||||
}
|
||||
|
||||
/*
|
||||
useCycle.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/useCycle.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/>.
|
||||
*/
|
||||
|
||||
/* export declare interface UseCycleProps {
|
||||
onEvent: ToneEventCallback<any>;
|
||||
onQuery?: (state: State) => Hap[];
|
||||
onSchedule?: (events: Hap[], cycle: number) => void;
|
||||
onDraw?: ToneEventCallback<any>;
|
||||
ready?: boolean; // if false, query will not be called on change props
|
||||
} */
|
||||
|
||||
// function useCycle(props: UseCycleProps) {
|
||||
function useCycle(props) {
|
||||
// onX must use useCallback!
|
||||
const { onEvent, onQuery, onSchedule, ready = true, onDraw } = props;
|
||||
const [started, setStarted] = useState(false);
|
||||
const cycleDuration = 1;
|
||||
const activeCycle = () => Math.floor(Tone.getTransport().seconds / cycleDuration);
|
||||
|
||||
// pull events with onQuery + count up to next cycle
|
||||
const query = (cycle = activeCycle()) => {
|
||||
const timespan = new TimeSpan(cycle, cycle + 1);
|
||||
const events = onQuery?.(new State(timespan)) || [];
|
||||
onSchedule?.(events, cycle);
|
||||
// cancel events after current query. makes sure no old events are player for rescheduled cycles
|
||||
// console.log('schedule', cycle);
|
||||
// query next cycle in the middle of the current
|
||||
const cancelFrom = timespan.begin.valueOf();
|
||||
Tone.getTransport().cancel(cancelFrom);
|
||||
// const queryNextTime = (cycle + 1) * cycleDuration - 0.1;
|
||||
const queryNextTime = (cycle + 1) * cycleDuration - 0.5;
|
||||
|
||||
// if queryNextTime would be before current time, execute directly (+0.1 for safety that it won't miss)
|
||||
const t = Math.max(Tone.getTransport().seconds, queryNextTime) + 0.1;
|
||||
Tone.getTransport().schedule(() => {
|
||||
query(cycle + 1);
|
||||
}, t);
|
||||
|
||||
// schedule events for next cycle
|
||||
events
|
||||
?.filter((event) => event.part.begin.equals(event.whole?.begin))
|
||||
.forEach((event) => {
|
||||
Tone.getTransport().schedule((time) => {
|
||||
onEvent(time, event, Tone.getContext().currentTime);
|
||||
Tone.Draw.schedule(() => {
|
||||
// do drawing or DOM manipulation here
|
||||
onDraw?.(time, event);
|
||||
}, time);
|
||||
}, event.part.begin.valueOf());
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
ready && query();
|
||||
}, [onEvent, onSchedule, onQuery, onDraw, ready]);
|
||||
|
||||
const start = async () => {
|
||||
setStarted(true);
|
||||
await Tone.start();
|
||||
Tone.getTransport().start('+0.1');
|
||||
};
|
||||
const stop = () => {
|
||||
Tone.getTransport().pause();
|
||||
setStarted(false);
|
||||
};
|
||||
const toggle = () => (started ? stop() : start());
|
||||
return {
|
||||
start,
|
||||
stop,
|
||||
onEvent,
|
||||
started,
|
||||
setStarted,
|
||||
toggle,
|
||||
query,
|
||||
activeCycle,
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
usePostMessage.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/usePostMessage.mjs>
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
function usePostMessage(listener) {
|
||||
useEffect(() => {
|
||||
window.addEventListener('message', listener);
|
||||
return () => window.removeEventListener('message', listener);
|
||||
}, [listener]);
|
||||
return useCallback((data) => window.postMessage(data, '*'), []);
|
||||
}
|
||||
|
||||
/*
|
||||
useRepl.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/useRepl.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/>.
|
||||
*/
|
||||
|
||||
let s4 = () => {
|
||||
return Math.floor((1 + Math.random()) * 0x10000)
|
||||
.toString(16)
|
||||
.substring(1);
|
||||
};
|
||||
const generateHash = (code) => encodeURIComponent(btoa(code));
|
||||
|
||||
function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawProp }) {
|
||||
const id = useMemo(() => s4(), []);
|
||||
const [code, setCode] = useState(tune);
|
||||
const [activeCode, setActiveCode] = useState();
|
||||
const [log, setLog] = useState('');
|
||||
const [error, setError] = useState();
|
||||
const [pending, setPending] = useState(false);
|
||||
const [hash, setHash] = useState('');
|
||||
const [pattern, setPattern] = useState();
|
||||
const dirty = useMemo(() => code !== activeCode || error, [code, activeCode, error]);
|
||||
const pushLog = useCallback((message) => setLog((log) => log + `${log ? '\n\n' : ''}${message}`), []);
|
||||
|
||||
// below block allows disabling the highlighting by including "strudel disable-highlighting" in the code (as comment)
|
||||
const onDraw = useMemo(() => {
|
||||
if (activeCode && !activeCode.includes('strudel disable-highlighting')) {
|
||||
return (time, event) => onDrawProp?.(time, event, activeCode);
|
||||
}
|
||||
}, [activeCode, onDrawProp]);
|
||||
|
||||
// cycle hook to control scheduling
|
||||
const cycle = useCycle({
|
||||
onDraw,
|
||||
onEvent: useCallback(
|
||||
(time, event, currentTime) => {
|
||||
try {
|
||||
onEvent?.(event);
|
||||
if (event.context.logs?.length) {
|
||||
event.context.logs.forEach(pushLog);
|
||||
}
|
||||
const { onTrigger, velocity } = event.context;
|
||||
if (!onTrigger) {
|
||||
if (defaultSynth) {
|
||||
const note = getPlayableNoteValue(event);
|
||||
defaultSynth.triggerAttackRelease(note, event.duration.valueOf(), time, velocity);
|
||||
} else {
|
||||
throw new Error('no defaultSynth passed to useRepl.');
|
||||
}
|
||||
/* console.warn('no instrument chosen', event);
|
||||
throw new Error(`no instrument chosen for ${JSON.stringify(event)}`); */
|
||||
} else {
|
||||
onTrigger(
|
||||
time,
|
||||
event,
|
||||
currentTime,
|
||||
1 /* cps */,
|
||||
event.wholeOrPart().begin.valueOf(),
|
||||
event.duration.valueOf(),
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(err);
|
||||
err.message = 'unplayable event: ' + err?.message;
|
||||
pushLog(err.message); // not with setError, because then we would have to setError(undefined) on next playable event
|
||||
}
|
||||
},
|
||||
[onEvent, pushLog, defaultSynth],
|
||||
),
|
||||
onQuery: useCallback(
|
||||
(state) => {
|
||||
try {
|
||||
return pattern?.query(state) || [];
|
||||
} catch (err) {
|
||||
console.warn(err);
|
||||
err.message = 'query error: ' + err.message;
|
||||
setError(err);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
[pattern],
|
||||
),
|
||||
onSchedule: useCallback((_events, cycle) => logCycle(_events, cycle), []),
|
||||
ready: !!pattern && !!activeCode,
|
||||
});
|
||||
|
||||
const broadcast = usePostMessage(({ data: { from, type } }) => {
|
||||
if (type === 'start' && from !== id) {
|
||||
// console.log('message', from, type);
|
||||
cycle.setStarted(false);
|
||||
setActiveCode(undefined);
|
||||
}
|
||||
});
|
||||
|
||||
const activateCode = useCallback(
|
||||
async (_code = code) => {
|
||||
if (activeCode && !dirty) {
|
||||
setError(undefined);
|
||||
cycle.start();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setPending(true);
|
||||
const parsed = await evaluate(_code);
|
||||
cycle.start();
|
||||
broadcast({ type: 'start', from: id });
|
||||
setPattern(() => parsed.pattern);
|
||||
if (autolink) {
|
||||
window.location.hash = '#' + encodeURIComponent(btoa(code));
|
||||
}
|
||||
setHash(generateHash(code));
|
||||
setError(undefined);
|
||||
setActiveCode(_code);
|
||||
setPending(false);
|
||||
} catch (err) {
|
||||
err.message = 'evaluation error: ' + err.message;
|
||||
console.warn(err);
|
||||
setError(err);
|
||||
}
|
||||
},
|
||||
[activeCode, dirty, code, cycle, autolink, id, broadcast],
|
||||
);
|
||||
// logs events of cycle
|
||||
const logCycle = (_events, cycle) => {
|
||||
if (_events.length) ;
|
||||
};
|
||||
|
||||
const togglePlay = () => {
|
||||
if (!cycle.started) {
|
||||
activateCode();
|
||||
} else {
|
||||
cycle.stop();
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
pending,
|
||||
code,
|
||||
setCode,
|
||||
pattern,
|
||||
error,
|
||||
cycle,
|
||||
setPattern,
|
||||
dirty,
|
||||
log,
|
||||
togglePlay,
|
||||
setActiveCode,
|
||||
activateCode,
|
||||
activeCode,
|
||||
pushLog,
|
||||
hash,
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
cx.js - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/cx.js>
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
function cx(...classes) {
|
||||
// : Array<string | undefined>
|
||||
return classes.filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
let highlights = []; // actively highlighted events
|
||||
let lastEnd;
|
||||
|
||||
function useHighlighting({ view, pattern, active }) {
|
||||
useEffect(() => {
|
||||
if (view) {
|
||||
if (pattern && active) {
|
||||
let frame = requestAnimationFrame(updateHighlights);
|
||||
|
||||
function updateHighlights() {
|
||||
try {
|
||||
const audioTime = Tone.getTransport().seconds;
|
||||
const span = [lastEnd || audioTime, audioTime + 1 / 60];
|
||||
lastEnd = audioTime + 1 / 60;
|
||||
highlights = highlights.filter((hap) => hap.whole.end > audioTime); // keep only highlights that are still active
|
||||
const haps = pattern.queryArc(...span).filter((hap) => hap.hasOnset());
|
||||
highlights = highlights.concat(haps); // add potential new onsets
|
||||
view.dispatch({ effects: setHighlights.of(highlights) }); // highlight all still active + new active haps
|
||||
} catch (err) {
|
||||
// console.log('error in updateHighlights', err);
|
||||
view.dispatch({ effects: setHighlights.of([]) });
|
||||
}
|
||||
frame = requestAnimationFrame(updateHighlights);
|
||||
}
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(frame);
|
||||
};
|
||||
} else {
|
||||
highlights = [];
|
||||
view.dispatch({ effects: setHighlights.of([]) });
|
||||
}
|
||||
}
|
||||
}, [pattern, active, view]);
|
||||
}
|
||||
|
||||
var tailwind = '';
|
||||
|
||||
const container = "_container_10e1g_1";
|
||||
const header = "_header_10e1g_5";
|
||||
const buttons = "_buttons_10e1g_9";
|
||||
const button = "_button_10e1g_9";
|
||||
const buttonDisabled = "_buttonDisabled_10e1g_17";
|
||||
const error = "_error_10e1g_21";
|
||||
const body = "_body_10e1g_25";
|
||||
var styles = {
|
||||
container: container,
|
||||
header: header,
|
||||
buttons: buttons,
|
||||
button: button,
|
||||
buttonDisabled: buttonDisabled,
|
||||
error: error,
|
||||
body: body
|
||||
};
|
||||
|
||||
function Icon({ type }) {
|
||||
return /* @__PURE__ */ React.createElement("svg", {
|
||||
xmlns: "http://www.w3.org/2000/svg",
|
||||
className: "sc-h-5 sc-w-5",
|
||||
viewBox: "0 0 20 20",
|
||||
fill: "currentColor"
|
||||
}, {
|
||||
refresh: /* @__PURE__ */ React.createElement("path", {
|
||||
fillRule: "evenodd",
|
||||
d: "M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z",
|
||||
clipRule: "evenodd"
|
||||
}),
|
||||
play: /* @__PURE__ */ React.createElement("path", {
|
||||
fillRule: "evenodd",
|
||||
d: "M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z",
|
||||
clipRule: "evenodd"
|
||||
}),
|
||||
pause: /* @__PURE__ */ React.createElement("path", {
|
||||
fillRule: "evenodd",
|
||||
d: "M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z",
|
||||
clipRule: "evenodd"
|
||||
})
|
||||
}[type]);
|
||||
}
|
||||
|
||||
function MiniRepl({ tune, defaultSynth, hideOutsideView = false }) {
|
||||
const { code, setCode, pattern, activateCode, error, cycle, dirty, togglePlay } = useRepl({
|
||||
tune,
|
||||
defaultSynth,
|
||||
autolink: false
|
||||
});
|
||||
const [view, setView] = useState();
|
||||
const [ref, isVisible] = useInView({
|
||||
threshold: 0.01
|
||||
});
|
||||
const wasVisible = useRef();
|
||||
const show = useMemo(() => {
|
||||
if (isVisible || !hideOutsideView) {
|
||||
wasVisible.current = true;
|
||||
}
|
||||
return isVisible || wasVisible.current;
|
||||
}, [isVisible, hideOutsideView]);
|
||||
useHighlighting({ view, pattern, active: cycle.started });
|
||||
return /* @__PURE__ */ React.createElement("div", {
|
||||
className: styles.container,
|
||||
ref
|
||||
}, /* @__PURE__ */ React.createElement("div", {
|
||||
className: styles.header
|
||||
}, /* @__PURE__ */ React.createElement("div", {
|
||||
className: styles.buttons
|
||||
}, /* @__PURE__ */ React.createElement("button", {
|
||||
className: cx(styles.button, cycle.started ? "sc-animate-pulse" : ""),
|
||||
onClick: () => togglePlay()
|
||||
}, /* @__PURE__ */ React.createElement(Icon, {
|
||||
type: cycle.started ? "pause" : "play"
|
||||
})), /* @__PURE__ */ React.createElement("button", {
|
||||
className: cx(dirty ? styles.button : styles.buttonDisabled),
|
||||
onClick: () => activateCode()
|
||||
}, /* @__PURE__ */ React.createElement(Icon, {
|
||||
type: "refresh"
|
||||
}))), error && /* @__PURE__ */ React.createElement("div", {
|
||||
className: styles.error
|
||||
}, error.message)), /* @__PURE__ */ React.createElement("div", {
|
||||
className: styles.body
|
||||
}, show && /* @__PURE__ */ React.createElement(CodeMirror, {
|
||||
value: code,
|
||||
onChange: setCode,
|
||||
onViewChanged: setView
|
||||
})));
|
||||
}
|
||||
|
||||
/*
|
||||
useWebMidi.js - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/useWebMidi.js>
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
function useWebMidi(props) {
|
||||
const { ready, connected, disconnected } = props;
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [outputs, setOutputs] = useState(WebMidi?.outputs || []);
|
||||
useEffect(() => {
|
||||
enableWebMidi()
|
||||
.then(() => {
|
||||
// Reacting when a new device becomes available
|
||||
WebMidi.addListener('connected', (e) => {
|
||||
setOutputs([...WebMidi.outputs]);
|
||||
connected?.(WebMidi, e);
|
||||
});
|
||||
// Reacting when a device becomes unavailable
|
||||
WebMidi.addListener('disconnected', (e) => {
|
||||
setOutputs([...WebMidi.outputs]);
|
||||
disconnected?.(WebMidi, e);
|
||||
});
|
||||
ready?.(WebMidi);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
//throw new Error("Web Midi could not be enabled...");
|
||||
console.warn('Web Midi could not be enabled..');
|
||||
return;
|
||||
}
|
||||
});
|
||||
}, [ready, connected, disconnected, outputs]);
|
||||
const outputByName = (name) => WebMidi.getOutputByName(name);
|
||||
return { loading, outputs, outputByName };
|
||||
}
|
||||
|
||||
export { CodeMirror, MiniRepl, cx, useCycle, useHighlighting, usePostMessage, useRepl, useWebMidi };
|
||||
1
packages/react/dist/style.css
vendored
Normal file
1
packages/react/dist/style.css
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
*,:before,:after{--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.sc-h-5{height:1.25rem}.sc-w-5{width:1.25rem}@keyframes sc-pulse{50%{opacity:.5}}.sc-animate-pulse{animation:sc-pulse 2s cubic-bezier(.4,0,.6,1) infinite}._container_10e1g_1{overflow:hidden;border-radius:.375rem;--tw-bg-opacity: 1;background-color:rgb(68 76 87 / var(--tw-bg-opacity))}._header_10e1g_5{display:flex;justify-content:space-between;border-top-width:1px;--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity))}._buttons_10e1g_9{display:flex}._button_10e1g_9{display:flex;width:4rem;cursor:pointer;align-items:center;justify-content:center;border-right-width:1px;--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity));padding:.25rem;--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}._button_10e1g_9:hover{--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity))}._buttonDisabled_10e1g_17{display:flex;width:4rem;cursor:pointer;cursor:not-allowed;align-items:center;justify-content:center;--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity));padding:.25rem;--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}._error_10e1g_21{padding:.25rem;text-align:right;font-size:.875rem;line-height:1.25rem;--tw-text-opacity: 1;color:rgb(254 202 202 / var(--tw-text-opacity))}._body_10e1g_25{position:relative;overflow:auto}
|
||||
13
packages/react/index.html
Normal file
13
packages/react/index.html
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<!-- <link rel="icon" type="image/svg+xml" href="/src/favicon.svg" /> -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Strudel React Components</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
62
packages/react/package.json
Normal file
62
packages/react/package.json
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
{
|
||||
"name": "@strudel.cycles/react",
|
||||
"version": "0.1.2",
|
||||
"description": "React components for strudel",
|
||||
"main": "dist/index.cjs.js",
|
||||
"module": "dist/index.es.js",
|
||||
"exports": {
|
||||
".": {
|
||||
"require": "./dist/index.cjs.js",
|
||||
"import": "./dist/index.es.js"
|
||||
},
|
||||
"./dist/style.css": "./dist/style.css"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tidalcycles/strudel.git"
|
||||
},
|
||||
"keywords": [
|
||||
"tidalcycles",
|
||||
"strudel",
|
||||
"pattern",
|
||||
"livecoding",
|
||||
"algorave"
|
||||
],
|
||||
"author": "Felix Roos <flix91@gmail.com>",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"bugs": {
|
||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@codemirror/lang-javascript": "^0.19.0",
|
||||
"@strudel.cycles/core": "*",
|
||||
"@strudel.cycles/eval": "^0.1.1",
|
||||
"@strudel.cycles/tone": "^0.1.1",
|
||||
"react-codemirror6": "^1.1.0",
|
||||
"react-hook-inview": "^4.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^17.0.2",
|
||||
"@types/react-dom": "^17.0.2",
|
||||
"@vitejs/plugin-react": "^1.3.0",
|
||||
"autoprefixer": "^10.4.7",
|
||||
"postcss": "^8.4.13",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"tailwindcss": "^3.0.24",
|
||||
"vite": "^2.9.9"
|
||||
}
|
||||
}
|
||||
12
packages/react/postcss.config.js
Normal file
12
packages/react/postcss.config.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
postcss.config.js - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/postcss.config.js>
|
||||
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/>.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
28
packages/react/src/App.jsx
Normal file
28
packages/react/src/App.jsx
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import React from 'react';
|
||||
import { MiniRepl } from './components/MiniRepl';
|
||||
import 'tailwindcss/tailwind.css';
|
||||
import { Tone, getDefaultSynth } from '@strudel.cycles/tone';
|
||||
import { evalScope } from '@strudel.cycles/eval';
|
||||
|
||||
const defaultSynth = getDefaultSynth();
|
||||
|
||||
evalScope(
|
||||
Tone,
|
||||
import('@strudel.cycles/core'),
|
||||
import('@strudel.cycles/tone'),
|
||||
import('@strudel.cycles/tonal'),
|
||||
import('@strudel.cycles/mini'),
|
||||
import('@strudel.cycles/midi'),
|
||||
import('@strudel.cycles/xen'),
|
||||
import('@strudel.cycles/webaudio'),
|
||||
);
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div>
|
||||
<MiniRepl tune={`"c3"`} defaultSynth={defaultSynth} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
169
packages/react/src/components/CodeMirror6.jsx
Normal file
169
packages/react/src/components/CodeMirror6.jsx
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
import React from 'react';
|
||||
import { CodeMirror as _CodeMirror } from 'react-codemirror6';
|
||||
// import { CodeMirrorLite as _CodeMirror } from 'react-codemirror6/dist/lite';
|
||||
import { EditorView, Decoration } from '@codemirror/view';
|
||||
import { StateField, StateEffect } from '@codemirror/state';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
// import { materialPalenight } from 'codemirror6-themes';
|
||||
import { materialPalenight } from '../themes/material-palenight';
|
||||
|
||||
export const setHighlights = StateEffect.define();
|
||||
const highlightField = StateField.define({
|
||||
create() {
|
||||
return Decoration.none;
|
||||
},
|
||||
update(highlights, tr) {
|
||||
try {
|
||||
for (let e of tr.effects) {
|
||||
if (e.is(setHighlights)) {
|
||||
highlights = Decoration.set(
|
||||
e.value
|
||||
.flatMap((hap) =>
|
||||
(hap.context.locations || []).map(({ start, end }) => {
|
||||
const color = hap.context.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: 1px solid ${color}` } });
|
||||
return mark.range(from, to);
|
||||
}),
|
||||
)
|
||||
.filter(Boolean),
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
return highlights;
|
||||
} catch (err) {
|
||||
// console.warn('highlighting error', err);
|
||||
return highlights;
|
||||
}
|
||||
},
|
||||
provide: (f) => EditorView.decorations.from(f),
|
||||
});
|
||||
|
||||
export default function CodeMirror({ value, onChange, onViewChanged, onCursor, options, editorDidMount }) {
|
||||
return (
|
||||
<>
|
||||
<_CodeMirror
|
||||
onViewChange={onViewChanged}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flex: '1 0 auto',
|
||||
}}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
extensions={[
|
||||
javascript(),
|
||||
materialPalenight,
|
||||
highlightField,
|
||||
// theme, language, ...
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
let parenMark;
|
||||
export const markParens = (editor, data) => {
|
||||
const v = editor.getDoc().getValue();
|
||||
const marked = getCurrentParenArea(v, data);
|
||||
parenMark?.clear();
|
||||
parenMark = editor.getDoc().markText(...marked, { css: 'background-color: #00007720' }); //
|
||||
};
|
||||
|
||||
// returns { line, ch } from absolute character offset
|
||||
export function offsetToPosition(offset, code) {
|
||||
const lines = code.split('\n');
|
||||
let line = 0;
|
||||
let ch = 0;
|
||||
for (let i = 0; i < offset; i++) {
|
||||
if (ch === lines[line].length) {
|
||||
line++;
|
||||
ch = 0;
|
||||
} else {
|
||||
ch++;
|
||||
}
|
||||
}
|
||||
return { line, ch };
|
||||
}
|
||||
|
||||
// returns absolute character offset from { line, ch }
|
||||
export function positionToOffset(position, code) {
|
||||
const lines = code.split('\n');
|
||||
if (position.line > lines.length) {
|
||||
// throw new Error('positionToOffset: position.line > lines.length');
|
||||
return 0;
|
||||
}
|
||||
let offset = 0;
|
||||
for (let i = 0; i < position.line; i++) {
|
||||
offset += lines[i].length + 1;
|
||||
}
|
||||
offset += position.ch;
|
||||
return offset;
|
||||
}
|
||||
|
||||
// given code and caret position, the functions returns the indices of the parens we are in
|
||||
export function getCurrentParenArea(code, caretPosition) {
|
||||
const caret = positionToOffset(caretPosition, code);
|
||||
let open, i, begin, end;
|
||||
// walk left
|
||||
i = caret;
|
||||
open = 0;
|
||||
while (i > 0) {
|
||||
if (code[i - 1] === '(') {
|
||||
open--;
|
||||
} else if (code[i - 1] === ')') {
|
||||
open++;
|
||||
}
|
||||
if (open === -1) {
|
||||
break;
|
||||
}
|
||||
i--;
|
||||
}
|
||||
begin = i;
|
||||
// walk right
|
||||
i = caret;
|
||||
open = 0;
|
||||
while (i < code.length) {
|
||||
if (code[i] === '(') {
|
||||
open--;
|
||||
} else if (code[i] === ')') {
|
||||
open++;
|
||||
}
|
||||
if (open === 1) {
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
end = i;
|
||||
return [begin, end].map((o) => offsetToPosition(o, code));
|
||||
}
|
||||
|
||||
/*
|
||||
export const markEvent = (editor) => (time, event) => {
|
||||
const locs = event.context.locations;
|
||||
if (!locs || !editor) {
|
||||
return;
|
||||
}
|
||||
const col = event.context?.color || '#FFCA28';
|
||||
// mark active event
|
||||
const marks = locs.map(({ start, end }) =>
|
||||
editor.getDoc().markText(
|
||||
{ line: start.line - 1, ch: start.column },
|
||||
{ line: end.line - 1, ch: end.column },
|
||||
//{ css: 'background-color: #FFCA28; color: black' } // background-color is now used by parent marking
|
||||
{ css: 'outline: 1px solid ' + col + '; box-sizing:border-box' },
|
||||
//{ css: `background-color: ${col};border-radius:5px` },
|
||||
),
|
||||
);
|
||||
//Tone.Transport.schedule(() => { // problem: this can be cleared by scheduler...
|
||||
setTimeout(() => {
|
||||
marks.forEach((mark) => mark.clear());
|
||||
// }, '+' + event.duration * 0.5);
|
||||
}, event.duration * 1000);
|
||||
}; */
|
||||
33
packages/react/src/components/Icon.tsx
Normal file
33
packages/react/src/components/Icon.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import React from 'react';
|
||||
|
||||
export function Icon({ type }) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="sc-h-5 sc-w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
{
|
||||
{
|
||||
refresh: (
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
),
|
||||
play: (
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
),
|
||||
pause: (
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
),
|
||||
}[type]
|
||||
}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
48
packages/react/src/components/MiniRepl.jsx
Normal file
48
packages/react/src/components/MiniRepl.jsx
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import React, { useState, useMemo, useRef } from 'react';
|
||||
import { useInView } from 'react-hook-inview';
|
||||
import useRepl from '../hooks/useRepl.mjs';
|
||||
import cx from '../cx';
|
||||
import useHighlighting from '../hooks/useHighlighting.mjs';
|
||||
import CodeMirror6 from './CodeMirror6';
|
||||
import 'tailwindcss/tailwind.css';
|
||||
import './style.css';
|
||||
import styles from './MiniRepl.module.css';
|
||||
import { Icon } from './Icon';
|
||||
|
||||
export function MiniRepl({ tune, defaultSynth, hideOutsideView = false }) {
|
||||
const { code, setCode, pattern, activateCode, error, cycle, dirty, togglePlay } = useRepl({
|
||||
tune,
|
||||
defaultSynth,
|
||||
autolink: false,
|
||||
});
|
||||
const [view, setView] = useState();
|
||||
const [ref, isVisible] = useInView({
|
||||
threshold: 0.01,
|
||||
});
|
||||
const wasVisible = useRef();
|
||||
const show = useMemo(() => {
|
||||
if (isVisible || !hideOutsideView) {
|
||||
wasVisible.current = true;
|
||||
}
|
||||
return isVisible || wasVisible.current;
|
||||
}, [isVisible, hideOutsideView]);
|
||||
useHighlighting({ view, pattern, active: cycle.started });
|
||||
return (
|
||||
<div className={styles.container} ref={ref}>
|
||||
<div className={styles.header}>
|
||||
<div className={styles.buttons}>
|
||||
<button className={cx(styles.button, cycle.started ? 'sc-animate-pulse' : '')} onClick={() => togglePlay()}>
|
||||
<Icon type={cycle.started ? 'pause' : 'play'} />
|
||||
</button>
|
||||
<button className={cx(dirty ? styles.button : styles.buttonDisabled)} onClick={() => activateCode()}>
|
||||
<Icon type="refresh" />
|
||||
</button>
|
||||
</div>
|
||||
{error && <div className={styles.error}>{error.message}</div>}
|
||||
</div>
|
||||
<div className={styles.body} >
|
||||
{show && <CodeMirror6 value={code} onChange={setCode} onViewChanged={setView} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
27
packages/react/src/components/MiniRepl.module.css
Normal file
27
packages/react/src/components/MiniRepl.module.css
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
.container {
|
||||
@apply sc-rounded-md sc-overflow-hidden sc-bg-[#444C57];
|
||||
}
|
||||
|
||||
.header {
|
||||
@apply sc-flex sc-justify-between sc-bg-slate-700 sc-border-t sc-border-slate-500;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
@apply sc-flex;
|
||||
}
|
||||
|
||||
.button {
|
||||
@apply sc-cursor-pointer sc-w-16 sc-flex sc-items-center sc-justify-center sc-p-1 sc-bg-slate-700 sc-border-r sc-border-slate-500 sc-text-white hover:sc-bg-slate-600;
|
||||
}
|
||||
|
||||
.buttonDisabled {
|
||||
@apply sc-cursor-pointer sc-w-16 sc-flex sc-items-center sc-justify-center sc-p-1 sc-bg-slate-600 sc-text-slate-400 sc-cursor-not-allowed;
|
||||
}
|
||||
|
||||
.error {
|
||||
@apply sc-text-right sc-p-1 sc-text-sm sc-text-red-200;
|
||||
}
|
||||
|
||||
.body {
|
||||
@apply sc-overflow-auto sc-relative;
|
||||
}
|
||||
3
packages/react/src/components/style.css
Normal file
3
packages/react/src/components/style.css
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
.cm-editor {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
10
packages/react/src/cx.js
Normal file
10
packages/react/src/cx.js
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
cx.js - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/cx.js>
|
||||
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/>.
|
||||
*/
|
||||
|
||||
export default function cx(...classes) {
|
||||
// : Array<string | undefined>
|
||||
return classes.filter(Boolean).join(' ');
|
||||
}
|
||||
86
packages/react/src/hooks/useCycle.mjs
Normal file
86
packages/react/src/hooks/useCycle.mjs
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
useCycle.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/useCycle.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/>.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Tone } from '@strudel.cycles/tone';
|
||||
import { State, TimeSpan } from '@strudel.cycles/core';
|
||||
|
||||
/* export declare interface UseCycleProps {
|
||||
onEvent: ToneEventCallback<any>;
|
||||
onQuery?: (state: State) => Hap[];
|
||||
onSchedule?: (events: Hap[], cycle: number) => void;
|
||||
onDraw?: ToneEventCallback<any>;
|
||||
ready?: boolean; // if false, query will not be called on change props
|
||||
} */
|
||||
|
||||
// function useCycle(props: UseCycleProps) {
|
||||
function useCycle(props) {
|
||||
// onX must use useCallback!
|
||||
const { onEvent, onQuery, onSchedule, ready = true, onDraw } = props;
|
||||
const [started, setStarted] = useState(false);
|
||||
const cycleDuration = 1;
|
||||
const activeCycle = () => Math.floor(Tone.getTransport().seconds / cycleDuration);
|
||||
|
||||
// pull events with onQuery + count up to next cycle
|
||||
const query = (cycle = activeCycle()) => {
|
||||
const timespan = new TimeSpan(cycle, cycle + 1);
|
||||
const events = onQuery?.(new State(timespan)) || [];
|
||||
onSchedule?.(events, cycle);
|
||||
// cancel events after current query. makes sure no old events are player for rescheduled cycles
|
||||
// console.log('schedule', cycle);
|
||||
// query next cycle in the middle of the current
|
||||
const cancelFrom = timespan.begin.valueOf();
|
||||
Tone.getTransport().cancel(cancelFrom);
|
||||
// const queryNextTime = (cycle + 1) * cycleDuration - 0.1;
|
||||
const queryNextTime = (cycle + 1) * cycleDuration - 0.5;
|
||||
|
||||
// if queryNextTime would be before current time, execute directly (+0.1 for safety that it won't miss)
|
||||
const t = Math.max(Tone.getTransport().seconds, queryNextTime) + 0.1;
|
||||
Tone.getTransport().schedule(() => {
|
||||
query(cycle + 1);
|
||||
}, t);
|
||||
|
||||
// schedule events for next cycle
|
||||
events
|
||||
?.filter((event) => event.part.begin.equals(event.whole?.begin))
|
||||
.forEach((event) => {
|
||||
Tone.getTransport().schedule((time) => {
|
||||
onEvent(time, event, Tone.getContext().currentTime);
|
||||
Tone.Draw.schedule(() => {
|
||||
// do drawing or DOM manipulation here
|
||||
onDraw?.(time, event);
|
||||
}, time);
|
||||
}, event.part.begin.valueOf());
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
ready && query();
|
||||
}, [onEvent, onSchedule, onQuery, onDraw, ready]);
|
||||
|
||||
const start = async () => {
|
||||
setStarted(true);
|
||||
await Tone.start();
|
||||
Tone.getTransport().start('+0.1');
|
||||
};
|
||||
const stop = () => {
|
||||
Tone.getTransport().pause();
|
||||
setStarted(false);
|
||||
};
|
||||
const toggle = () => (started ? stop() : start());
|
||||
return {
|
||||
start,
|
||||
stop,
|
||||
onEvent,
|
||||
started,
|
||||
setStarted,
|
||||
toggle,
|
||||
query,
|
||||
activeCycle,
|
||||
};
|
||||
}
|
||||
|
||||
export default useCycle;
|
||||
41
packages/react/src/hooks/useHighlighting.mjs
Normal file
41
packages/react/src/hooks/useHighlighting.mjs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { useEffect } from 'react';
|
||||
import { setHighlights } from '../components/CodeMirror6';
|
||||
import { Tone } from '@strudel.cycles/tone';
|
||||
|
||||
let highlights = []; // actively highlighted events
|
||||
let lastEnd;
|
||||
|
||||
function useHighlighting({ view, pattern, active }) {
|
||||
useEffect(() => {
|
||||
if (view) {
|
||||
if (pattern && active) {
|
||||
let frame = requestAnimationFrame(updateHighlights);
|
||||
|
||||
function updateHighlights() {
|
||||
try {
|
||||
const audioTime = Tone.getTransport().seconds;
|
||||
const span = [lastEnd || audioTime, audioTime + 1 / 60];
|
||||
lastEnd = audioTime + 1 / 60;
|
||||
highlights = highlights.filter((hap) => hap.whole.end > audioTime); // keep only highlights that are still active
|
||||
const haps = pattern.queryArc(...span).filter((hap) => hap.hasOnset());
|
||||
highlights = highlights.concat(haps); // add potential new onsets
|
||||
view.dispatch({ effects: setHighlights.of(highlights) }); // highlight all still active + new active haps
|
||||
} catch (err) {
|
||||
// console.log('error in updateHighlights', err);
|
||||
view.dispatch({ effects: setHighlights.of([]) });
|
||||
}
|
||||
frame = requestAnimationFrame(updateHighlights);
|
||||
}
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(frame);
|
||||
};
|
||||
} else {
|
||||
highlights = [];
|
||||
view.dispatch({ effects: setHighlights.of([]) });
|
||||
}
|
||||
}
|
||||
}, [pattern, active, view]);
|
||||
}
|
||||
|
||||
export default useHighlighting;
|
||||
17
packages/react/src/hooks/usePostMessage.mjs
Normal file
17
packages/react/src/hooks/usePostMessage.mjs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
usePostMessage.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/usePostMessage.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/>.
|
||||
*/
|
||||
|
||||
import { useEffect, useCallback } from 'react';
|
||||
|
||||
function usePostMessage(listener) {
|
||||
useEffect(() => {
|
||||
window.addEventListener('message', listener);
|
||||
return () => window.removeEventListener('message', listener);
|
||||
}, [listener]);
|
||||
return useCallback((data) => window.postMessage(data, '*'), []);
|
||||
}
|
||||
|
||||
export default usePostMessage;
|
||||
164
packages/react/src/hooks/useRepl.mjs
Normal file
164
packages/react/src/hooks/useRepl.mjs
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
/*
|
||||
useRepl.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/useRepl.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/>.
|
||||
*/
|
||||
|
||||
import { useCallback, useState, useMemo } from 'react';
|
||||
import { evaluate } from '@strudel.cycles/eval';
|
||||
import { getPlayableNoteValue } from '@strudel.cycles/core/util.mjs';
|
||||
import useCycle from './useCycle.mjs';
|
||||
import usePostMessage from './usePostMessage.mjs';
|
||||
|
||||
let s4 = () => {
|
||||
return Math.floor((1 + Math.random()) * 0x10000)
|
||||
.toString(16)
|
||||
.substring(1);
|
||||
};
|
||||
const generateHash = (code) => encodeURIComponent(btoa(code));
|
||||
|
||||
function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawProp }) {
|
||||
const id = useMemo(() => s4(), []);
|
||||
const [code, setCode] = useState(tune);
|
||||
const [activeCode, setActiveCode] = useState();
|
||||
const [log, setLog] = useState('');
|
||||
const [error, setError] = useState();
|
||||
const [pending, setPending] = useState(false);
|
||||
const [hash, setHash] = useState('');
|
||||
const [pattern, setPattern] = useState();
|
||||
const dirty = useMemo(() => code !== activeCode || error, [code, activeCode, error]);
|
||||
const pushLog = useCallback((message) => setLog((log) => log + `${log ? '\n\n' : ''}${message}`), []);
|
||||
|
||||
// below block allows disabling the highlighting by including "strudel disable-highlighting" in the code (as comment)
|
||||
const onDraw = useMemo(() => {
|
||||
if (activeCode && !activeCode.includes('strudel disable-highlighting')) {
|
||||
return (time, event) => onDrawProp?.(time, event, activeCode);
|
||||
}
|
||||
}, [activeCode, onDrawProp]);
|
||||
|
||||
// cycle hook to control scheduling
|
||||
const cycle = useCycle({
|
||||
onDraw,
|
||||
onEvent: useCallback(
|
||||
(time, event, currentTime) => {
|
||||
try {
|
||||
onEvent?.(event);
|
||||
if (event.context.logs?.length) {
|
||||
event.context.logs.forEach(pushLog);
|
||||
}
|
||||
const { onTrigger, velocity } = event.context;
|
||||
if (!onTrigger) {
|
||||
if (defaultSynth) {
|
||||
const note = getPlayableNoteValue(event);
|
||||
defaultSynth.triggerAttackRelease(note, event.duration.valueOf(), time, velocity);
|
||||
} else {
|
||||
throw new Error('no defaultSynth passed to useRepl.');
|
||||
}
|
||||
/* console.warn('no instrument chosen', event);
|
||||
throw new Error(`no instrument chosen for ${JSON.stringify(event)}`); */
|
||||
} else {
|
||||
onTrigger(
|
||||
time,
|
||||
event,
|
||||
currentTime,
|
||||
1 /* cps */,
|
||||
event.wholeOrPart().begin.valueOf(),
|
||||
event.duration.valueOf(),
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(err);
|
||||
err.message = 'unplayable event: ' + err?.message;
|
||||
pushLog(err.message); // not with setError, because then we would have to setError(undefined) on next playable event
|
||||
}
|
||||
},
|
||||
[onEvent, pushLog, defaultSynth],
|
||||
),
|
||||
onQuery: useCallback(
|
||||
(state) => {
|
||||
try {
|
||||
return pattern?.query(state) || [];
|
||||
} catch (err) {
|
||||
console.warn(err);
|
||||
err.message = 'query error: ' + err.message;
|
||||
setError(err);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
[pattern],
|
||||
),
|
||||
onSchedule: useCallback((_events, cycle) => logCycle(_events, cycle), []),
|
||||
ready: !!pattern && !!activeCode,
|
||||
});
|
||||
|
||||
const broadcast = usePostMessage(({ data: { from, type } }) => {
|
||||
if (type === 'start' && from !== id) {
|
||||
// console.log('message', from, type);
|
||||
cycle.setStarted(false);
|
||||
setActiveCode(undefined);
|
||||
}
|
||||
});
|
||||
|
||||
const activateCode = useCallback(
|
||||
async (_code = code) => {
|
||||
if (activeCode && !dirty) {
|
||||
setError(undefined);
|
||||
cycle.start();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setPending(true);
|
||||
const parsed = await evaluate(_code);
|
||||
cycle.start();
|
||||
broadcast({ type: 'start', from: id });
|
||||
setPattern(() => parsed.pattern);
|
||||
if (autolink) {
|
||||
window.location.hash = '#' + encodeURIComponent(btoa(code));
|
||||
}
|
||||
setHash(generateHash(code));
|
||||
setError(undefined);
|
||||
setActiveCode(_code);
|
||||
setPending(false);
|
||||
} catch (err) {
|
||||
err.message = 'evaluation error: ' + err.message;
|
||||
console.warn(err);
|
||||
setError(err);
|
||||
}
|
||||
},
|
||||
[activeCode, dirty, code, cycle, autolink, id, broadcast],
|
||||
);
|
||||
// logs events of cycle
|
||||
const logCycle = (_events, cycle) => {
|
||||
if (_events.length) {
|
||||
// pushLog(`# cycle ${cycle}\n` + _events.map((e: any) => e.show()).join('\n'));
|
||||
}
|
||||
};
|
||||
|
||||
const togglePlay = () => {
|
||||
if (!cycle.started) {
|
||||
activateCode();
|
||||
} else {
|
||||
cycle.stop();
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
pending,
|
||||
code,
|
||||
setCode,
|
||||
pattern,
|
||||
error,
|
||||
cycle,
|
||||
setPattern,
|
||||
dirty,
|
||||
log,
|
||||
togglePlay,
|
||||
setActiveCode,
|
||||
activateCode,
|
||||
activeCode,
|
||||
pushLog,
|
||||
hash,
|
||||
};
|
||||
}
|
||||
|
||||
export default useRepl;
|
||||
41
packages/react/src/hooks/useWebMidi.mjs
Normal file
41
packages/react/src/hooks/useWebMidi.mjs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
useWebMidi.js - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/useWebMidi.js>
|
||||
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 { useEffect, useState } from 'react';
|
||||
import { enableWebMidi, WebMidi } from '@strudel.cycles/midi'
|
||||
|
||||
export function useWebMidi(props) {
|
||||
const { ready, connected, disconnected } = props;
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [outputs, setOutputs] = useState(WebMidi?.outputs || []);
|
||||
useEffect(() => {
|
||||
enableWebMidi()
|
||||
.then(() => {
|
||||
// Reacting when a new device becomes available
|
||||
WebMidi.addListener('connected', (e) => {
|
||||
setOutputs([...WebMidi.outputs]);
|
||||
connected?.(WebMidi, e);
|
||||
});
|
||||
// Reacting when a device becomes unavailable
|
||||
WebMidi.addListener('disconnected', (e) => {
|
||||
setOutputs([...WebMidi.outputs]);
|
||||
disconnected?.(WebMidi, e);
|
||||
});
|
||||
ready?.(WebMidi);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
//throw new Error("Web Midi could not be enabled...");
|
||||
console.warn('Web Midi could not be enabled..');
|
||||
return;
|
||||
}
|
||||
});
|
||||
}, [ready, connected, disconnected, outputs]);
|
||||
const outputByName = (name) => WebMidi.getOutputByName(name);
|
||||
return { loading, outputs, outputByName };
|
||||
}
|
||||
10
packages/react/src/index.js
Normal file
10
packages/react/src/index.js
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
// import 'tailwindcss/tailwind.css';
|
||||
|
||||
export { default as CodeMirror } from './components/CodeMirror6';
|
||||
export * from './components/MiniRepl';
|
||||
export { default as useCycle } from './hooks/useCycle';
|
||||
export { default as useHighlighting } from './hooks/useHighlighting';
|
||||
export { default as usePostMessage } from './hooks/usePostMessage';
|
||||
export { default as useRepl } from './hooks/useRepl';
|
||||
export { default as cx } from './cx';
|
||||
export { useWebMidi } from './hooks/useWebMidi';
|
||||
5
packages/react/src/main.jsx
Normal file
5
packages/react/src/main.jsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import App from './App'
|
||||
|
||||
ReactDOM.render(<React.StrictMode><App /></React.StrictMode>,document.getElementById('root'))
|
||||
135
packages/react/src/themes/material-palenight.js
Normal file
135
packages/react/src/themes/material-palenight.js
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
import { EditorView } from '@codemirror/view';
|
||||
import { HighlightStyle, tags as t } from '@codemirror/highlight';
|
||||
|
||||
/*
|
||||
Credits for color palette:
|
||||
|
||||
Author: Mattia Astorino (http://github.com/equinusocio)
|
||||
Website: https://material-theme.site/
|
||||
*/
|
||||
|
||||
const ivory = '#abb2bf',
|
||||
stone = '#7d8799', // Brightened compared to original to increase contrast
|
||||
invalid = '#ffffff',
|
||||
darkBackground = '#21252b',
|
||||
highlightBackground = 'rgba(0, 0, 0, 0.5)',
|
||||
// background = '#292d3e',
|
||||
background = 'transparent',
|
||||
tooltipBackground = '#353a42',
|
||||
selection = 'rgba(128, 203, 196, 0.2)',
|
||||
cursor = '#ffcc00';
|
||||
|
||||
/// The editor theme styles for Material Palenight.
|
||||
export const materialPalenightTheme = EditorView.theme(
|
||||
{
|
||||
// done
|
||||
'&': {
|
||||
color: '#ffffff',
|
||||
backgroundColor: background,
|
||||
fontSize: '15px',
|
||||
'z-index': 11,
|
||||
},
|
||||
|
||||
// done
|
||||
'.cm-content': {
|
||||
caretColor: cursor,
|
||||
lineHeight: '22px',
|
||||
},
|
||||
'.cm-line': {
|
||||
background: '#2C323699',
|
||||
},
|
||||
// done
|
||||
'&.cm-focused .cm-cursor': {
|
||||
borderLeftColor: cursor,
|
||||
},
|
||||
|
||||
'&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': {
|
||||
backgroundColor: selection,
|
||||
},
|
||||
|
||||
'.cm-panels': { backgroundColor: darkBackground, color: '#ffffff' },
|
||||
'.cm-panels.cm-panels-top': { borderBottom: '2px solid black' },
|
||||
'.cm-panels.cm-panels-bottom': { borderTop: '2px solid black' },
|
||||
|
||||
// done, use onedarktheme
|
||||
'.cm-searchMatch': {
|
||||
backgroundColor: '#72a1ff59',
|
||||
outline: '1px solid #457dff',
|
||||
},
|
||||
'.cm-searchMatch.cm-searchMatch-selected': {
|
||||
backgroundColor: '#6199ff2f',
|
||||
},
|
||||
|
||||
'.cm-activeLine': { backgroundColor: highlightBackground },
|
||||
'.cm-selectionMatch': { backgroundColor: '#aafe661a' },
|
||||
|
||||
'&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
|
||||
backgroundColor: '#bad0f847',
|
||||
outline: '1px solid #515a6b',
|
||||
},
|
||||
|
||||
// done
|
||||
'.cm-gutters': {
|
||||
background: '#2C323699',
|
||||
color: '#676e95',
|
||||
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 Material Palenight theme.
|
||||
export const materialPalenightHighlightStyle = HighlightStyle.define([
|
||||
{ tag: t.keyword, color: '#c792ea' },
|
||||
{ tag: t.operator, color: '#89ddff' },
|
||||
{ tag: t.special(t.variableName), color: '#eeffff' },
|
||||
{ tag: t.typeName, color: '#f07178' },
|
||||
{ tag: t.atom, color: '#f78c6c' },
|
||||
{ tag: t.number, color: '#ff5370' },
|
||||
{ tag: t.definition(t.variableName), color: '#82aaff' },
|
||||
{ tag: t.string, color: '#c3e88d' },
|
||||
{ tag: t.special(t.string), color: '#f07178' },
|
||||
{ tag: t.comment, color: stone },
|
||||
{ tag: t.variableName, color: '#f07178' },
|
||||
{ tag: t.tagName, color: '#ff5370' },
|
||||
{ tag: t.bracket, color: '#a2a1a4' },
|
||||
{ tag: t.meta, color: '#ffcb6b' },
|
||||
{ tag: t.attributeName, color: '#c792ea' },
|
||||
{ tag: t.propertyName, color: '#c792ea' },
|
||||
{ tag: t.className, color: '#decb6b' },
|
||||
{ tag: t.invalid, color: invalid },
|
||||
]);
|
||||
|
||||
/// Extension to enable the Material Palenight theme (both the editor theme and
|
||||
/// the highlight style).
|
||||
// : Extension
|
||||
export const materialPalenight = [materialPalenightTheme, materialPalenightHighlightStyle];
|
||||
11
packages/react/tailwind.config.js
Normal file
11
packages/react/tailwind.config.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
module.exports = {
|
||||
content: ['./src/**/*.{js,jsx,ts,tsx}'],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
corePlugins: {
|
||||
preflight: false,
|
||||
},
|
||||
prefix: 'sc-',
|
||||
};
|
||||
42
packages/react/vite.config.js
Normal file
42
packages/react/vite.config.js
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import { peerDependencies, dependencies } from './package.json';
|
||||
import { resolve } from 'path';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
react({
|
||||
jsxRuntime: 'classic',
|
||||
}),
|
||||
],
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'src', 'index.js'),
|
||||
formats: ['es', 'cjs'],
|
||||
fileName: (ext) => `index.${ext}.js`,
|
||||
// for UMD name: 'GlobalName'
|
||||
},
|
||||
rollupOptions: {
|
||||
external: [
|
||||
...Object.keys(peerDependencies),
|
||||
...Object.keys(dependencies),
|
||||
// TODO: find out which of below names are obsolete now
|
||||
'@strudel.cycles/tone',
|
||||
'@strudel.cycles/eval',
|
||||
'@strudel.cycles/core',
|
||||
'@strudel.cycles/core/util.mjs',
|
||||
'@strudel.cycles/mini',
|
||||
'@strudel.cycles/tonal',
|
||||
'@strudel.cycles/midi',
|
||||
'@strudel.cycles/xen',
|
||||
'@strudel.cycles/serial',
|
||||
'@strudel.cycles/webaudio',
|
||||
'@codemirror/view',
|
||||
'@codemirror/highlight',
|
||||
'@codemirror/state'
|
||||
],
|
||||
},
|
||||
target: 'esnext',
|
||||
},
|
||||
});
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@strudel.cycles/serial",
|
||||
"version": "0.0.6",
|
||||
"version": "0.1.0",
|
||||
"description": "Webserial API for strudel",
|
||||
"main": "serial.mjs",
|
||||
"repository": {
|
||||
|
|
@ -19,6 +19,5 @@
|
|||
"bugs": {
|
||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {}
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme"
|
||||
}
|
||||
|
|
|
|||
2
packages/tonal/index.mjs
Normal file
2
packages/tonal/index.mjs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
import './tonal.mjs';
|
||||
import './voicings.mjs';
|
||||
2
packages/tonal/package-lock.json
generated
2
packages/tonal/package-lock.json
generated
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@strudel.cycles/tonal",
|
||||
"version": "0.0.5",
|
||||
"version": "0.1.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"name": "@strudel.cycles/tonal",
|
||||
"version": "0.0.5",
|
||||
"version": "0.1.1",
|
||||
"description": "Tonal functions for strudel",
|
||||
"main": "tonal.mjs",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "mocha --colors"
|
||||
|
|
@ -25,7 +25,7 @@
|
|||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "^0.0.5",
|
||||
"@strudel.cycles/core": "^0.1.0",
|
||||
"@tonaljs/tonal": "^4.6.5",
|
||||
"webmidi": "^3.0.15"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ import { Note, Interval, Scale } from '@tonaljs/tonal';
|
|||
import { Pattern, mod } from '@strudel.cycles/core';
|
||||
|
||||
// transpose note inside scale by offset steps
|
||||
// function scaleTranspose(scale: string, offset: number, note: string) {
|
||||
export function scaleTranspose(scale, offset, note) {
|
||||
// function scaleOffset(scale: string, offset: number, note: string) {
|
||||
function scaleOffset(scale, offset, note) {
|
||||
let [tonic, scaleName] = Scale.tokenize(scale);
|
||||
let { notes } = Scale.get(`${tonic} ${scaleName}`);
|
||||
notes = notes.map((note) => Note.get(note).pc); // use only pc!
|
||||
|
|
@ -71,7 +71,7 @@ Pattern.prototype._scaleTranspose = function (offset /* : number | string */) {
|
|||
if (typeof hap.value !== 'string') {
|
||||
throw new Error('can only use scaleTranspose with notes');
|
||||
}
|
||||
return hap.withValue(() => scaleTranspose(hap.context.scale, Number(offset), hap.value));
|
||||
return hap.withValue(() => scaleOffset(hap.context.scale, Number(offset), hap.value));
|
||||
});
|
||||
};
|
||||
Pattern.prototype._scale = function (scale /* : string */) {
|
||||
|
|
@ -81,7 +81,7 @@ Pattern.prototype._scale = function (scale /* : string */) {
|
|||
if (!isNaN(asNumber)) {
|
||||
let [tonic, scaleName] = Scale.tokenize(scale);
|
||||
const { pc, oct = 3 } = Note.get(tonic);
|
||||
note = scaleTranspose(pc + ' ' + scaleName, asNumber, pc + oct);
|
||||
note = scaleOffset(pc + ' ' + scaleName, asNumber, pc + oct);
|
||||
}
|
||||
return hap.withValue(() => note).setContext({ ...hap.context, scale });
|
||||
});
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ Pattern.prototype.draw = function (callback, cycleSpan, lookaheadCycles = 1) {
|
|||
const end = (currentCycle + lookaheadCycles) * cycleSpan;
|
||||
events = this._asNumber(true) // true = silent error
|
||||
.query(new State(new TimeSpan(begin, end)))
|
||||
.filter(Boolean)
|
||||
.filter((event) => event.part.begin.equals(event.whole.begin));
|
||||
}
|
||||
}
|
||||
|
|
@ -47,7 +48,7 @@ Pattern.prototype.draw = function (callback, cycleSpan, lookaheadCycles = 1) {
|
|||
return this;
|
||||
};
|
||||
|
||||
export const cleanup = () => {
|
||||
export const cleanupDraw = () => {
|
||||
const ctx = getDrawContext();
|
||||
ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
|
||||
if (window.strudelAnimation) {
|
||||
|
|
|
|||
5
packages/tone/index.mjs
Normal file
5
packages/tone/index.mjs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import './pianoroll.mjs';
|
||||
|
||||
export * from './tone.mjs';
|
||||
export * from './draw.mjs';
|
||||
export * from './ui.mjs';
|
||||
2
packages/tone/package-lock.json
generated
2
packages/tone/package-lock.json
generated
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@strudel.cycles/tone",
|
||||
"version": "0.0.6",
|
||||
"version": "0.1.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"name": "@strudel.cycles/tone",
|
||||
"version": "0.0.6",
|
||||
"version": "0.1.1",
|
||||
"description": "Tone.js API for strudel",
|
||||
"main": "tone.mjs",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "^0.0.5",
|
||||
"@strudel.cycles/core": "^0.1.0",
|
||||
"@tonejs/piano": "^0.2.1",
|
||||
"chord-voicings": "^0.0.1",
|
||||
"tone": "^14.7.77"
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ export const backgroundImage = function (src, animateOptions = {}) {
|
|||
);
|
||||
};
|
||||
|
||||
export const cleanup = () => {
|
||||
export const cleanupUi = () => {
|
||||
const container = document.getElementById('code');
|
||||
if (container) {
|
||||
container.style = '';
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ const urlifyFunction = (func) => URL.createObjectURL(new Blob([stringifyFunction
|
|||
const createWorker = (func) => new Worker(urlifyFunction(func));
|
||||
|
||||
// this class is basically the tale of two clocks
|
||||
class ClockWorker {
|
||||
export class ClockWorker {
|
||||
worker;
|
||||
audioContext;
|
||||
interval = 0.2; // query span
|
||||
|
|
@ -72,4 +72,3 @@ class ClockWorker {
|
|||
}
|
||||
}
|
||||
|
||||
export default ClockWorker;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,6 @@ 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/>.
|
||||
*/
|
||||
|
||||
export { default as ClockWorker } from './clockworker.mjs';
|
||||
export { default as Scheduler } from './scheduler.mjs';
|
||||
export * from './clockworker.mjs';
|
||||
export * from './scheduler.mjs';
|
||||
export * from './webaudio.mjs';
|
||||
|
|
|
|||
2
packages/webaudio/package-lock.json
generated
2
packages/webaudio/package-lock.json
generated
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@strudel.cycles/webaudio",
|
||||
"version": "0.0.5",
|
||||
"version": "0.1.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
{
|
||||
"name": "@strudel.cycles/webaudio",
|
||||
"version": "0.0.6",
|
||||
"version": "0.1.1",
|
||||
"description": "Web Audio helpers for Strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
"directories": {
|
||||
"example": "examples"
|
||||
},
|
||||
|
|
@ -27,6 +28,6 @@
|
|||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "^0.0.5"
|
||||
"@strudel.cycles/core": "^0.1.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ 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/>.
|
||||
*/
|
||||
|
||||
import ClockWorker from './clockworker.mjs';
|
||||
import { ClockWorker } from './clockworker.mjs';
|
||||
import { State, TimeSpan } from '@strudel.cycles/core';
|
||||
|
||||
class Scheduler {
|
||||
export class Scheduler {
|
||||
worker;
|
||||
pattern;
|
||||
constructor({ audioContext, interval = 0.2, onEvent }) {
|
||||
|
|
@ -41,5 +41,3 @@ class Scheduler {
|
|||
this.pattern = pat;
|
||||
}
|
||||
}
|
||||
|
||||
export default Scheduler;
|
||||
|
|
|
|||
|
|
@ -4,16 +4,17 @@ 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/>.
|
||||
*/
|
||||
|
||||
import { Pattern, getFrequency, patternify2 } from '@strudel.cycles/core';
|
||||
// import { Pattern, getFrequency, patternify2 } from '@strudel.cycles/core';
|
||||
import * as strudel from '@strudel.cycles/core';
|
||||
import { Tone } from '@strudel.cycles/tone';
|
||||
const { Pattern, getFrequency, patternify2 } = strudel;
|
||||
|
||||
// let audioContext;
|
||||
let audioContext;
|
||||
export const getAudioContext = () => {
|
||||
return Tone.getContext().rawContext;
|
||||
/* if (!audioContext) {
|
||||
if (!audioContext) {
|
||||
audioContext = new AudioContext();
|
||||
}
|
||||
return audioContext; */
|
||||
return audioContext;
|
||||
};
|
||||
|
||||
const lookahead = 0.2;
|
||||
|
|
|
|||
2
packages/xen/index.mjs
Normal file
2
packages/xen/index.mjs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
import './xen.mjs';
|
||||
import './tune.mjs';
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"name": "@strudel.cycles/xen",
|
||||
"version": "0.0.5",
|
||||
"version": "0.1.1",
|
||||
"description": "Xenharmonic API for strudel",
|
||||
"main": "xen.mjs",
|
||||
"main": "index.mjs",
|
||||
"scripts": {
|
||||
"test": "mocha --colors"
|
||||
},
|
||||
|
|
@ -24,6 +24,6 @@
|
|||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "^0.0.5"
|
||||
"@strudel.cycles/core": "^0.1.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue