@brett_lamy/docstream 1.9.1 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -0
- package/dist/docs/DocsRenderer.d.ts.map +1 -1
- package/dist/docs/DocsRenderer.js +13 -1
- package/dist/docs/DocsRenderer.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/stl/StlViewer.d.ts +13 -0
- package/dist/stl/StlViewer.d.ts.map +1 -0
- package/dist/stl/StlViewer.js +277 -0
- package/dist/stl/StlViewer.js.map +1 -0
- package/dist/stl/index.d.ts +5 -0
- package/dist/stl/index.d.ts.map +1 -0
- package/dist/stl/index.js +3 -0
- package/dist/stl/index.js.map +1 -0
- package/dist/stl/parse.d.ts +13 -0
- package/dist/stl/parse.d.ts.map +1 -0
- package/dist/stl/parse.js +69 -0
- package/dist/stl/parse.js.map +1 -0
- package/dist/strudel/StepGrid.d.ts +10 -0
- package/dist/strudel/StepGrid.d.ts.map +1 -0
- package/dist/strudel/StepGrid.js +78 -0
- package/dist/strudel/StepGrid.js.map +1 -0
- package/dist/strudel/StrudelPlayer.d.ts +21 -0
- package/dist/strudel/StrudelPlayer.d.ts.map +1 -0
- package/dist/strudel/StrudelPlayer.js +207 -0
- package/dist/strudel/StrudelPlayer.js.map +1 -0
- package/dist/strudel/StrudelRack.d.ts +24 -0
- package/dist/strudel/StrudelRack.d.ts.map +1 -0
- package/dist/strudel/StrudelRack.js +123 -0
- package/dist/strudel/StrudelRack.js.map +1 -0
- package/dist/strudel/ast/edit.d.ts +41 -0
- package/dist/strudel/ast/edit.d.ts.map +1 -0
- package/dist/strudel/ast/edit.js +78 -0
- package/dist/strudel/ast/edit.js.map +1 -0
- package/dist/strudel/ast/index.d.ts +7 -0
- package/dist/strudel/ast/index.d.ts.map +1 -0
- package/dist/strudel/ast/index.js +4 -0
- package/dist/strudel/ast/index.js.map +1 -0
- package/dist/strudel/ast/mini.d.ts +86 -0
- package/dist/strudel/ast/mini.d.ts.map +1 -0
- package/dist/strudel/ast/mini.js +269 -0
- package/dist/strudel/ast/mini.js.map +1 -0
- package/dist/strudel/ast/program.d.ts +93 -0
- package/dist/strudel/ast/program.d.ts.map +1 -0
- package/dist/strudel/ast/program.js +288 -0
- package/dist/strudel/ast/program.js.map +1 -0
- package/dist/strudel/controls.d.ts +19 -0
- package/dist/strudel/controls.d.ts.map +1 -0
- package/dist/strudel/controls.js +109 -0
- package/dist/strudel/controls.js.map +1 -0
- package/dist/strudel/engine.d.ts +43 -0
- package/dist/strudel/engine.d.ts.map +1 -0
- package/dist/strudel/engine.js +79 -0
- package/dist/strudel/engine.js.map +1 -0
- package/dist/strudel/index.d.ts +13 -0
- package/dist/strudel/index.d.ts.map +1 -0
- package/dist/strudel/index.js +8 -0
- package/dist/strudel/index.js.map +1 -0
- package/dist/strudel/url.d.ts +5 -0
- package/dist/strudel/url.d.ts.map +1 -0
- package/dist/strudel/url.js +9 -0
- package/dist/strudel/url.js.map +1 -0
- package/dist/styles.css +564 -0
- package/package.json +15 -1
- package/src/docs/DocsRenderer.tsx +13 -1
- package/src/env.d.ts +3 -0
- package/src/index.ts +5 -0
- package/src/stl/StlViewer.tsx +334 -0
- package/src/stl/index.ts +4 -0
- package/src/stl/parse.ts +72 -0
- package/src/strudel/StepGrid.tsx +130 -0
- package/src/strudel/StrudelPlayer.tsx +317 -0
- package/src/strudel/StrudelRack.tsx +312 -0
- package/src/strudel/ast/edit.ts +95 -0
- package/src/strudel/ast/index.ts +19 -0
- package/src/strudel/ast/mini.ts +319 -0
- package/src/strudel/ast/program.ts +348 -0
- package/src/strudel/controls.tsx +145 -0
- package/src/strudel/engine.ts +112 -0
- package/src/strudel/index.ts +20 -0
- package/src/strudel/url.ts +9 -0
- package/src/styles.css +564 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/* Exact source edits on a parsed song: each helper returns new code that differs only in the characters the
|
|
2
|
+
change touches, so comments, formatting and everything the rack doesn't understand survive. */
|
|
3
|
+
import type { MiniAtom } from "./mini"
|
|
4
|
+
import { findCall, type StrudelArg, type StrudelChain, type StrudelSong, type StrudelTrack } from "./program"
|
|
5
|
+
|
|
6
|
+
export interface CodeEdit {
|
|
7
|
+
start: number
|
|
8
|
+
end: number
|
|
9
|
+
text: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Apply non-overlapping edits (in any order). */
|
|
13
|
+
export function applyEdits(code: string, edits: CodeEdit[]): string {
|
|
14
|
+
const sorted = [...edits].sort((a, b) => b.start - a.start)
|
|
15
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
16
|
+
if (sorted[i]!.end > sorted[i - 1]!.start) throw new Error("Overlapping edits")
|
|
17
|
+
}
|
|
18
|
+
return sorted.reduce((out, e) => out.slice(0, e.start) + e.text + out.slice(e.end), code)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** A compact literal: at most 4 significant decimals, no trailing zeros, `.5` style like hand-written Strudel. */
|
|
22
|
+
export function formatNumber(value: number): string {
|
|
23
|
+
if (!Number.isFinite(value)) return "0"
|
|
24
|
+
const abs = Math.abs(value)
|
|
25
|
+
const digits = abs >= 1000 ? 0 : abs >= 100 ? 1 : abs >= 10 ? 2 : abs >= 1 ? 3 : 4
|
|
26
|
+
let s = String(+value.toFixed(digits))
|
|
27
|
+
if (s.startsWith("0.")) s = s.slice(1)
|
|
28
|
+
else if (s.startsWith("-0.")) s = "-" + s.slice(2)
|
|
29
|
+
return s
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Replace one argument's source text. */
|
|
33
|
+
export function setArg(code: string, arg: StrudelArg, text: string): string {
|
|
34
|
+
return applyEdits(code, [{ start: arg.start, end: arg.end, text }])
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Set a method's first argument (`.lpf(800)`), adding `.name(value)` at the end of the chain when it
|
|
39
|
+
* isn't there yet. `value` is source text: pass `formatNumber(n)` or a quoted string.
|
|
40
|
+
*/
|
|
41
|
+
export function setMethod(code: string, chain: StrudelChain, name: string, value: string): string {
|
|
42
|
+
const call = findCall(chain, name)
|
|
43
|
+
if (!call) return applyEdits(code, [{ start: chain.end, end: chain.end, text: `.${name}(${value})` }])
|
|
44
|
+
const first = call.args[0]
|
|
45
|
+
if (first) return applyEdits(code, [{ start: first.start, end: first.end, text: value }])
|
|
46
|
+
return applyEdits(code, [{ start: call.argsRange.start, end: call.argsRange.end, text: value }])
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Remove every `.name(...)` from the chain. */
|
|
50
|
+
export function removeMethod(code: string, chain: StrudelChain, name: string): string {
|
|
51
|
+
const edits = chain.methods.filter((m) => m.name === name).map((m) => ({ ...m.methodRange, text: "" }))
|
|
52
|
+
return edits.length ? applyEdits(code, edits) : code
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Set the tempo in cycles per second, keeping `setcpm` code in cpm; adds `setcps(…)` when there is none. */
|
|
56
|
+
export function setTempo(song: StrudelSong, cps: number): string {
|
|
57
|
+
const { code, tempo } = song
|
|
58
|
+
if (!tempo) return `setcps(${formatNumber(cps)})\n${code}`
|
|
59
|
+
const value = tempo.unit === "cpm" ? formatNumber(cps * 60) : formatNumber(cps)
|
|
60
|
+
const first = tempo.call.args[0]
|
|
61
|
+
return first
|
|
62
|
+
? applyEdits(code, [{ start: first.start, end: first.end, text: value }])
|
|
63
|
+
: applyEdits(code, [{ start: tempo.call.argsRange.start, end: tempo.call.argsRange.end, text: value }])
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Rewrite one atom of a mini-notation string (e.g. a step of a drum row: "hh" ⇄ "~"). */
|
|
67
|
+
export function setMiniAtom(code: string, mini: Extract<StrudelArg, { kind: "mini" }>, atom: MiniAtom, text: string): string {
|
|
68
|
+
return applyEdits(code, [{ start: mini.contentStart + atom.start, end: mini.contentStart + atom.end, text }])
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Replace a mini-notation string's contents. */
|
|
72
|
+
export function setMiniSource(code: string, mini: Extract<StrudelArg, { kind: "mini" }>, source: string): string {
|
|
73
|
+
return applyEdits(code, [{ start: mini.contentStart, end: mini.end - 1, text: source }])
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface StrudelMix {
|
|
77
|
+
/** Track indexes to silence. */
|
|
78
|
+
muted?: ReadonlySet<number>
|
|
79
|
+
/** When non-empty, only these tracks play. */
|
|
80
|
+
solo?: ReadonlySet<number>
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Whether a track sounds under a mix (its own `_$:` mute included). */
|
|
84
|
+
export function isAudible(track: StrudelTrack, mix: StrudelMix = {}): boolean {
|
|
85
|
+
if (track.muted || mix.muted?.has(track.index)) return false
|
|
86
|
+
return !mix.solo?.size || mix.solo.has(track.index)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The code to play for a mix: silenced tracks become `silence`; the written code is untouched. */
|
|
90
|
+
export function mixCode(song: StrudelSong, mix: StrudelMix = {}): string {
|
|
91
|
+
const edits = song.tracks
|
|
92
|
+
.filter((t) => !t.muted && !isAudible(t, mix))
|
|
93
|
+
.map((t) => ({ start: t.start, end: t.end, text: "silence" }))
|
|
94
|
+
return edits.length ? applyEdits(song.code, edits) : song.code
|
|
95
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export { MiniSyntaxError, miniAtoms, miniLayers, parseMini } from "./mini"
|
|
2
|
+
export type {
|
|
3
|
+
MiniAlternation,
|
|
4
|
+
MiniAtom,
|
|
5
|
+
MiniGroup,
|
|
6
|
+
MiniLayers,
|
|
7
|
+
MiniNode,
|
|
8
|
+
MiniOp,
|
|
9
|
+
MiniPolymeter,
|
|
10
|
+
MiniSequence,
|
|
11
|
+
MiniSlice,
|
|
12
|
+
MiniStep,
|
|
13
|
+
MiniSubcycle,
|
|
14
|
+
Range,
|
|
15
|
+
} from "./mini"
|
|
16
|
+
export { StrudelSyntaxError, chainCalls, findCall, parseStrudel } from "./program"
|
|
17
|
+
export type { StrudelArg, StrudelCall, StrudelChain, StrudelSong, StrudelTempo, StrudelTrack, TrackKind } from "./program"
|
|
18
|
+
export { applyEdits, formatNumber, isAudible, mixCode, removeMethod, setArg, setMethod, setMiniAtom, setMiniSource, setTempo } from "./edit"
|
|
19
|
+
export type { CodeEdit, StrudelMix } from "./edit"
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/* Strudel mini-notation ("bd*2 [~ sd] <c e g>") as a syntax tree with source ranges.
|
|
2
|
+
Written from the grammar Strudel documents (krill), not from its parser: every node carries `start`/`end`
|
|
3
|
+
offsets into the string it was parsed from, so tools can rewrite exactly the characters a node covers. */
|
|
4
|
+
|
|
5
|
+
export interface Range {
|
|
6
|
+
start: number
|
|
7
|
+
end: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** A word: a sample or note name, a number, or a rest (`~`, `-`). */
|
|
11
|
+
export interface MiniAtom extends Range {
|
|
12
|
+
type: "atom"
|
|
13
|
+
value: string
|
|
14
|
+
rest: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** `[a b c]`: the steps share their parent's step. */
|
|
18
|
+
export interface MiniSubcycle extends Range {
|
|
19
|
+
type: "subcycle"
|
|
20
|
+
body: MiniGroup
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** `<a b c>`: one step per cycle (layers with `,` each alternate on their own). */
|
|
24
|
+
export interface MiniAlternation extends Range {
|
|
25
|
+
type: "alternation"
|
|
26
|
+
layers: MiniSequence[]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** `{a b c, d e}%4`: layers stepped at a shared rate (the first layer's, or `%steps`). */
|
|
30
|
+
export interface MiniPolymeter extends Range {
|
|
31
|
+
type: "polymeter"
|
|
32
|
+
layers: MiniSequence[]
|
|
33
|
+
steps: MiniStep | null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type MiniSlice = MiniAtom | MiniSubcycle | MiniAlternation | MiniPolymeter
|
|
37
|
+
|
|
38
|
+
export type MiniOp =
|
|
39
|
+
| (Range & { op: "fast" | "slow"; amount: MiniStep })
|
|
40
|
+
| (Range & { op: "replicate"; count: number | null })
|
|
41
|
+
| (Range & { op: "weight"; amount: number | null })
|
|
42
|
+
| (Range & { op: "degrade"; amount: number | null })
|
|
43
|
+
| (Range & { op: "euclid"; pulses: MiniStep; steps: MiniStep; rotation: MiniStep | null })
|
|
44
|
+
| (Range & { op: "tail"; value: MiniStep })
|
|
45
|
+
| (Range & { op: "range"; to: MiniStep })
|
|
46
|
+
|
|
47
|
+
/** One step of a sequence: a slice and the operators written after it (`bd*2`, `c@3`, `hh?`). */
|
|
48
|
+
export interface MiniStep extends Range {
|
|
49
|
+
type: "step"
|
|
50
|
+
slice: MiniSlice
|
|
51
|
+
ops: MiniOp[]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Steps played one after another within the cycle. `^` marks the sequence that sets the step count. */
|
|
55
|
+
export interface MiniSequence extends Range {
|
|
56
|
+
type: "sequence"
|
|
57
|
+
steps: MiniStep[]
|
|
58
|
+
marked: boolean
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Sequences played together (`,`), picked at random each cycle (`|`), or concatenated as feet (`.`). */
|
|
62
|
+
export interface MiniLayers extends Range {
|
|
63
|
+
type: "stack" | "choose" | "feet"
|
|
64
|
+
layers: MiniSequence[]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type MiniGroup = MiniSequence | MiniLayers
|
|
68
|
+
export type MiniNode = MiniGroup | MiniStep | MiniSlice
|
|
69
|
+
|
|
70
|
+
export class MiniSyntaxError extends Error {
|
|
71
|
+
constructor(message: string, readonly offset: number) {
|
|
72
|
+
super(`${message} at ${offset}`)
|
|
73
|
+
this.name = "MiniSyntaxError"
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const WS = /[ \n\r\t ]/
|
|
78
|
+
const WORD = /[\p{L}\p{Nl}0-9~\-#.^_]/u
|
|
79
|
+
const NUMBER = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][-+]?[0-9]+)?/
|
|
80
|
+
|
|
81
|
+
/** Parse a mini-notation string. Throws `MiniSyntaxError` (with the offset) on invalid input. */
|
|
82
|
+
export function parseMini(source: string): MiniGroup {
|
|
83
|
+
let i = 0
|
|
84
|
+
const peek = (n = 0) => source[i + n] ?? ""
|
|
85
|
+
const skip = () => {
|
|
86
|
+
while (i < source.length && WS.test(source[i]!)) i++
|
|
87
|
+
}
|
|
88
|
+
const fail = (what: string): never => {
|
|
89
|
+
throw new MiniSyntaxError(i < source.length ? `Unexpected "${source[i]}", expected ${what}` : `Expected ${what}`, i)
|
|
90
|
+
}
|
|
91
|
+
const expect = (ch: string) => {
|
|
92
|
+
skip()
|
|
93
|
+
if (peek() !== ch) fail(`"${ch}"`)
|
|
94
|
+
i++
|
|
95
|
+
}
|
|
96
|
+
const number = (): number | null => {
|
|
97
|
+
const m = NUMBER.exec(source.slice(i))
|
|
98
|
+
if (!m) return null
|
|
99
|
+
i += m[0].length
|
|
100
|
+
return parseFloat(m[0])
|
|
101
|
+
}
|
|
102
|
+
// A separator `.` (feet) is a lone dot between spaces; inside a word it is part of the word ("0.5", "a.b").
|
|
103
|
+
const atFeetDot = () => peek() === "." && peek(1) !== "." && (i === 0 || WS.test(source[i - 1]!)) && (peek(1) === "" || WS.test(peek(1)))
|
|
104
|
+
|
|
105
|
+
function word(): MiniAtom | null {
|
|
106
|
+
const start = i
|
|
107
|
+
while (i < source.length && WORD.test(source[i]!)) {
|
|
108
|
+
// ".." starts a range operator, not part of the word.
|
|
109
|
+
if (source[i] === "." && source[i + 1] === ".") break
|
|
110
|
+
i++
|
|
111
|
+
}
|
|
112
|
+
const value = source.slice(start, i)
|
|
113
|
+
if (!value) return null
|
|
114
|
+
if (value === "." || value === "_") {
|
|
115
|
+
i = start
|
|
116
|
+
return null
|
|
117
|
+
}
|
|
118
|
+
return { type: "atom", value, rest: value === "~" || value === "-", start, end: i }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function slice(): MiniSlice | null {
|
|
122
|
+
skip()
|
|
123
|
+
const start = i
|
|
124
|
+
const ch = peek()
|
|
125
|
+
if (ch === "[") {
|
|
126
|
+
i++
|
|
127
|
+
const body = group(["]"])
|
|
128
|
+
expect("]")
|
|
129
|
+
return { type: "subcycle", body, start, end: i }
|
|
130
|
+
}
|
|
131
|
+
if (ch === "<") {
|
|
132
|
+
i++
|
|
133
|
+
const layers = commaLayers([">"])
|
|
134
|
+
expect(">")
|
|
135
|
+
return { type: "alternation", layers, start, end: i }
|
|
136
|
+
}
|
|
137
|
+
if (ch === "{") {
|
|
138
|
+
i++
|
|
139
|
+
const layers = commaLayers(["}"])
|
|
140
|
+
expect("}")
|
|
141
|
+
let steps: MiniStep | null = null
|
|
142
|
+
if (peek() === "%") {
|
|
143
|
+
i++
|
|
144
|
+
steps = bare() ?? fail("a step count after %")
|
|
145
|
+
}
|
|
146
|
+
return { type: "polymeter", layers, steps, start, end: i }
|
|
147
|
+
}
|
|
148
|
+
if (atFeetDot()) return null
|
|
149
|
+
return word()
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// `*`, `/`, `:`, `..` and `%` take a bare slice: in "bd*2!" the `!` replicates bd, not 2.
|
|
153
|
+
function bare(): MiniStep | null {
|
|
154
|
+
const s = slice()
|
|
155
|
+
return s && { type: "step", slice: s, ops: [], start: s.start, end: s.end }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// A slice with its operators. `@`, `_` and `!` may stand apart ("a _ _", "a ! !"); the rest attach directly.
|
|
159
|
+
function step(): MiniStep | null {
|
|
160
|
+
const s = slice()
|
|
161
|
+
if (!s) return null
|
|
162
|
+
const ops: MiniOp[] = []
|
|
163
|
+
for (;;) {
|
|
164
|
+
const before = i
|
|
165
|
+
skip()
|
|
166
|
+
const start = i
|
|
167
|
+
const ch = peek()
|
|
168
|
+
if (ch === "@" || (ch === "_" && !WORD.test(peek(1)))) {
|
|
169
|
+
i++
|
|
170
|
+
ops.push({ op: "weight", amount: ch === "@" ? number() : null, start, end: i })
|
|
171
|
+
continue
|
|
172
|
+
}
|
|
173
|
+
if (ch === "!") {
|
|
174
|
+
i++
|
|
175
|
+
ops.push({ op: "replicate", count: number(), start, end: i })
|
|
176
|
+
continue
|
|
177
|
+
}
|
|
178
|
+
i = before
|
|
179
|
+
const at = i
|
|
180
|
+
const c = peek()
|
|
181
|
+
if (c === "*" || c === "/") {
|
|
182
|
+
i++
|
|
183
|
+
const amount = bare() ?? fail("an amount")
|
|
184
|
+
ops.push({ op: c === "*" ? "fast" : "slow", amount, start: at, end: i })
|
|
185
|
+
} else if (c === "?") {
|
|
186
|
+
i++
|
|
187
|
+
ops.push({ op: "degrade", amount: number(), start: at, end: i })
|
|
188
|
+
} else if (c === ":") {
|
|
189
|
+
i++
|
|
190
|
+
const value = bare() ?? fail("a value after :")
|
|
191
|
+
ops.push({ op: "tail", value, start: at, end: i })
|
|
192
|
+
} else if (c === "." && peek(1) === ".") {
|
|
193
|
+
i += 2
|
|
194
|
+
const to = bare() ?? fail("a range end")
|
|
195
|
+
ops.push({ op: "range", to, start: at, end: i })
|
|
196
|
+
} else if (c === "(") {
|
|
197
|
+
i++
|
|
198
|
+
const pulses = step() ?? fail("pulses")
|
|
199
|
+
expect(",")
|
|
200
|
+
const steps = step() ?? fail("steps")
|
|
201
|
+
skip()
|
|
202
|
+
let rotation: MiniStep | null = null
|
|
203
|
+
if (peek() === ",") {
|
|
204
|
+
i++
|
|
205
|
+
rotation = step()
|
|
206
|
+
}
|
|
207
|
+
expect(")")
|
|
208
|
+
ops.push({ op: "euclid", pulses, steps, rotation, start: at, end: i })
|
|
209
|
+
} else {
|
|
210
|
+
break
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return { type: "step", slice: s, ops, start: s.start, end: i }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function sequence(closers: string[]): MiniSequence {
|
|
217
|
+
skip()
|
|
218
|
+
const start = i
|
|
219
|
+
let marked = false
|
|
220
|
+
if (peek() === "^") {
|
|
221
|
+
marked = true
|
|
222
|
+
i++
|
|
223
|
+
}
|
|
224
|
+
const steps: MiniStep[] = []
|
|
225
|
+
for (;;) {
|
|
226
|
+
skip()
|
|
227
|
+
const ch = peek()
|
|
228
|
+
if (!ch || closers.includes(ch) || ch === "," || ch === "|" || atFeetDot()) break
|
|
229
|
+
const s = step()
|
|
230
|
+
if (!s) fail("a step")
|
|
231
|
+
steps.push(s!)
|
|
232
|
+
}
|
|
233
|
+
if (!steps.length) fail("a step")
|
|
234
|
+
return { type: "sequence", steps, marked, start, end: steps[steps.length - 1]!.end }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function commaLayers(closers: string[]): MiniSequence[] {
|
|
238
|
+
const layers = [sequence(closers)]
|
|
239
|
+
for (;;) {
|
|
240
|
+
skip()
|
|
241
|
+
if (peek() !== ",") return layers
|
|
242
|
+
i++
|
|
243
|
+
layers.push(sequence(closers))
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function group(closers: string[]): MiniGroup {
|
|
248
|
+
const first = sequence(closers)
|
|
249
|
+
skip()
|
|
250
|
+
const sep = atFeetDot() ? "." : peek()
|
|
251
|
+
if (sep !== "," && sep !== "|" && sep !== ".") return first
|
|
252
|
+
const type = sep === "," ? "stack" : sep === "|" ? "choose" : "feet"
|
|
253
|
+
const layers = [first]
|
|
254
|
+
for (;;) {
|
|
255
|
+
skip()
|
|
256
|
+
const next = atFeetDot() ? "." : peek()
|
|
257
|
+
if (next !== sep) break
|
|
258
|
+
i++
|
|
259
|
+
layers.push(sequence(closers))
|
|
260
|
+
}
|
|
261
|
+
return { type, layers, start: first.start, end: layers[layers.length - 1]!.end }
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const root = group([])
|
|
265
|
+
skip()
|
|
266
|
+
if (i < source.length) fail("end of pattern")
|
|
267
|
+
return root
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Every atom in the tree, in source order. `{ ops: false }` skips operator arguments (the 2 in `bd*2`, the 3,8
|
|
272
|
+
* in `bd(3,8)`), leaving only the values the pattern plays.
|
|
273
|
+
*/
|
|
274
|
+
export function miniAtoms(node: MiniNode, { ops = true }: { ops?: boolean } = {}): MiniAtom[] {
|
|
275
|
+
const out: MiniAtom[] = []
|
|
276
|
+
const visit = (n: MiniNode | MiniOp | null | undefined) => {
|
|
277
|
+
if (!n) return
|
|
278
|
+
if ("op" in n) {
|
|
279
|
+
if (!ops) return
|
|
280
|
+
if (n.op === "fast" || n.op === "slow") visit(n.amount)
|
|
281
|
+
else if (n.op === "euclid") (visit(n.pulses), visit(n.steps), visit(n.rotation))
|
|
282
|
+
else if (n.op === "tail") visit(n.value)
|
|
283
|
+
else if (n.op === "range") visit(n.to)
|
|
284
|
+
return
|
|
285
|
+
}
|
|
286
|
+
switch (n.type) {
|
|
287
|
+
case "atom":
|
|
288
|
+
out.push(n)
|
|
289
|
+
break
|
|
290
|
+
case "step":
|
|
291
|
+
visit(n.slice)
|
|
292
|
+
n.ops.forEach(visit)
|
|
293
|
+
break
|
|
294
|
+
case "sequence":
|
|
295
|
+
n.steps.forEach(visit)
|
|
296
|
+
break
|
|
297
|
+
case "stack":
|
|
298
|
+
case "choose":
|
|
299
|
+
case "feet":
|
|
300
|
+
case "alternation":
|
|
301
|
+
n.layers.forEach(visit)
|
|
302
|
+
break
|
|
303
|
+
case "polymeter":
|
|
304
|
+
n.layers.forEach(visit)
|
|
305
|
+
visit(n.steps)
|
|
306
|
+
break
|
|
307
|
+
case "subcycle":
|
|
308
|
+
visit(n.body)
|
|
309
|
+
break
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
visit(node)
|
|
313
|
+
return out
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** The layers of a group: a sequence is one layer. */
|
|
317
|
+
export function miniLayers(group: MiniGroup): MiniSequence[] {
|
|
318
|
+
return group.type === "sequence" ? [group] : group.layers
|
|
319
|
+
}
|