@vibuca/synth8-core 0.1.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sven Hemmer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,124 @@
1
+ # @vibuca/synth8-core
2
+
3
+ Core parser and compiler for Synt8, an MIT-licensed pattern music toolkit written in TypeScript.
4
+
5
+ Synt8 defines its own small pattern DSL for rhythmic and melodic music patterns.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @vibuca/synth8-core
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```ts
16
+ import { compile } from "@vibuca/synth8-core";
17
+
18
+ const pattern = compile(`
19
+ song(
20
+ beat("kick+hihat snare hihat snare"),
21
+ melody("c4 e4 g4 _")
22
+ )
23
+ `);
24
+
25
+ console.log(pattern);
26
+ ```
27
+
28
+ ## Supported syntax
29
+
30
+ ### Beat patterns
31
+
32
+ ```ts
33
+ beat("kick snare hihat hihat")
34
+ ```
35
+
36
+ ### Melody patterns
37
+
38
+ ```ts
39
+ melody("c4 e4 g4 _")
40
+ ```
41
+
42
+ ### Songs
43
+
44
+ ```ts
45
+ song(
46
+ beat("kick snare"),
47
+ melody("c4 e4")
48
+ )
49
+ ```
50
+
51
+ ### Rate
52
+
53
+ ```ts
54
+ beat("kick snare").rate(2)
55
+ melody("c4 e4 g4").rate(2)
56
+ ```
57
+
58
+ ### Rests
59
+
60
+ ```ts
61
+ beat("kick _ snare hihat")
62
+ melody("c4 _ e4 g4")
63
+ ```
64
+
65
+ ### Groups
66
+
67
+ ```ts
68
+ beat("kick [snare hihat] kick")
69
+ melody("c4 [e4 g4] c5")
70
+ ```
71
+
72
+ ### Parallel drum hits
73
+
74
+ ```ts
75
+ beat("kick+hihat snare hihat snare")
76
+ ```
77
+
78
+ ## Output
79
+
80
+ `compile()` returns a pattern:
81
+
82
+ ```ts
83
+ type Pattern = {
84
+ length: number;
85
+ events: Event[];
86
+ };
87
+ ```
88
+
89
+ Events are stored in beats, not seconds.
90
+
91
+ ```ts
92
+ type Event =
93
+ | {
94
+ time: number;
95
+ dur: number;
96
+ type: "drum";
97
+ value: string;
98
+ }
99
+ | {
100
+ time: number;
101
+ dur: number;
102
+ type: "note";
103
+ value: string;
104
+ };
105
+ ```
106
+
107
+ ## Supported drum sounds
108
+
109
+ ```txt
110
+ kick
111
+ snare
112
+ clap
113
+ hihat
114
+ openhat
115
+ tom
116
+ rim
117
+ cowbell
118
+ ```
119
+
120
+ Use `_` for rests.
121
+
122
+ ## License
123
+
124
+ MIT
@@ -0,0 +1,2 @@
1
+ import { Pattern } from '../model';
2
+ export declare const compile: (source: string) => Pattern;
@@ -0,0 +1 @@
1
+ export * from './compiler';
@@ -0,0 +1,4 @@
1
+ export declare const VERSION = "0.1.0";
2
+ export * from './compiler';
3
+ export * from './parser';
4
+ export * from './model';
package/dist/index.js ADDED
@@ -0,0 +1,302 @@
1
+ //#region src/parser/beat-pattern-parser.ts
2
+ var e = (e) => {
3
+ let t = e.split("+").filter(Boolean);
4
+ return t.length === 1 ? {
5
+ kind: "BeatSound",
6
+ value: t[0]
7
+ } : {
8
+ kind: "BeatParallel",
9
+ sounds: t.map((e) => ({
10
+ kind: "BeatSound",
11
+ value: e
12
+ }))
13
+ };
14
+ }, t = (t) => {
15
+ let n = 0, r = (i) => {
16
+ let a = [];
17
+ for (; n < t.length;) {
18
+ let o = t[n];
19
+ if (/\s/.test(o)) {
20
+ n++;
21
+ continue;
22
+ }
23
+ if (i && o === i) return n++, a;
24
+ if (o === "[") {
25
+ n++, a.push({
26
+ kind: "BeatGroup",
27
+ steps: r("]")
28
+ });
29
+ continue;
30
+ }
31
+ if (o === "]") throw Error("Unexpected closing group ']'.");
32
+ let s = "";
33
+ for (; n < t.length && !/\s/.test(t[n]) && t[n] !== "[" && t[n] !== "]";) s += t[n], n++;
34
+ s.length > 0 && a.push(e(s));
35
+ }
36
+ if (i) throw Error("Unterminated group '['.");
37
+ return a;
38
+ };
39
+ return r();
40
+ }, n = /^[a-gA-G](#|b)?[0-8]$/, r = (e) => e === "_" || n.test(e);
41
+ //#endregion
42
+ //#region src/parser/melody-pattern-parser.ts
43
+ function i(e) {
44
+ let t = [...e], n = 0;
45
+ function i() {
46
+ for (; t[n] === " ";) n++;
47
+ }
48
+ function a(e) {
49
+ let o = [];
50
+ for (; n < t.length;) {
51
+ if (i(), e && t[n] === e) return n++, o;
52
+ if (t[n] === "[") {
53
+ n++, o.push({
54
+ kind: "MelodyGroup",
55
+ notes: a("]")
56
+ });
57
+ continue;
58
+ }
59
+ let s = "";
60
+ for (; n < t.length && t[n] !== " " && t[n] !== "[" && t[n] !== "]";) s += t[n++];
61
+ if (!s) break;
62
+ if (!r(s)) throw Error(`Unknown note: ${s}`);
63
+ o.push({
64
+ kind: "MelodyNote",
65
+ value: s
66
+ });
67
+ }
68
+ if (e) throw Error("Unclosed melody group");
69
+ return o;
70
+ }
71
+ return a();
72
+ }
73
+ //#endregion
74
+ //#region src/parser/tokenizer.ts
75
+ var a = (e) => {
76
+ let t = [], n = 0;
77
+ for (; n < e.length;) {
78
+ let r = e[n];
79
+ if (/\s/.test(r)) {
80
+ n++;
81
+ continue;
82
+ }
83
+ if (/[a-zA-Z_]/.test(r)) {
84
+ let r = "";
85
+ for (; /[a-zA-Z0-9_]/.test(e[n] ?? "");) r += e[n], n++;
86
+ t.push({
87
+ type: "identifier",
88
+ value: r
89
+ });
90
+ continue;
91
+ }
92
+ if (r === "\"") {
93
+ n++;
94
+ let r = "";
95
+ for (; n < e.length && e[n] !== "\"";) r += e[n], n++;
96
+ if (e[n] !== "\"") throw Error("Unterminated string literal.");
97
+ n++, t.push({
98
+ type: "string",
99
+ value: r
100
+ });
101
+ continue;
102
+ }
103
+ if (/\d/.test(r)) {
104
+ let r = "";
105
+ for (; /[\d.]/.test(e[n] ?? "");) r += e[n], n++;
106
+ t.push({
107
+ type: "number",
108
+ value: Number(r)
109
+ });
110
+ continue;
111
+ }
112
+ if (r === "(" || r === ")" || r === "." || r === ",") {
113
+ t.push({
114
+ type: "symbol",
115
+ value: r
116
+ }), n++;
117
+ continue;
118
+ }
119
+ throw Error(`Unexpected character: ${r}`);
120
+ }
121
+ return t;
122
+ }, o = class {
123
+ tokens;
124
+ index = 0;
125
+ constructor(e) {
126
+ this.tokens = e;
127
+ }
128
+ parse() {
129
+ let e = this.parseExpression();
130
+ if (!this.isAtEnd()) throw Error("Unexpected tokens after expression.");
131
+ return e;
132
+ }
133
+ parseExpression() {
134
+ let e = this.peek();
135
+ if (e?.type !== "identifier") throw Error("Expected expression.");
136
+ if (e.value === "beat") return this.parseBeatExpression();
137
+ if (e.value === "melody") return this.parseMelodyExpression();
138
+ if (e.value === "song") return this.parseSongExpression();
139
+ throw Error(`Unknown expression: ${e.value}`);
140
+ }
141
+ parseOptionalRate() {
142
+ let e = 1;
143
+ if (this.matchSymbol(".") && (this.expectIdentifier("rate"), this.expectSymbol("("), e = this.expectNumber(), this.expectSymbol(")")), !Number.isFinite(e) || e <= 0) throw Error(`Invalid rate: ${e}`);
144
+ return e;
145
+ }
146
+ parseBeatExpression() {
147
+ this.expectIdentifier("beat"), this.expectSymbol("(");
148
+ let e = this.expectString();
149
+ this.expectSymbol(")");
150
+ let n = this.parseOptionalRate();
151
+ return {
152
+ kind: "BeatExpression",
153
+ steps: t(e),
154
+ rate: n
155
+ };
156
+ }
157
+ parseMelodyExpression() {
158
+ this.expectIdentifier("melody"), this.expectSymbol("(");
159
+ let e = this.expectString();
160
+ this.expectSymbol(")");
161
+ let t = this.parseOptionalRate();
162
+ return {
163
+ kind: "MelodyExpression",
164
+ notes: i(e),
165
+ rate: t
166
+ };
167
+ }
168
+ parseSongExpression() {
169
+ this.expectIdentifier("song"), this.expectSymbol("(");
170
+ let e = [];
171
+ if (!this.matchSymbol(")")) {
172
+ do
173
+ e.push(this.parseExpression());
174
+ while (this.matchSymbol(","));
175
+ this.expectSymbol(")");
176
+ }
177
+ if (e.length === 0) throw Error("song() requires at least one track.");
178
+ return {
179
+ kind: "SongExpression",
180
+ tracks: e
181
+ };
182
+ }
183
+ expectIdentifier(e) {
184
+ let t = this.advance();
185
+ if (t?.type !== "identifier" || t.value !== e) throw Error(`Expected identifier "${e}".`);
186
+ }
187
+ expectString() {
188
+ let e = this.advance();
189
+ if (e?.type !== "string") throw Error("Expected string.");
190
+ return e.value;
191
+ }
192
+ expectNumber() {
193
+ let e = this.advance();
194
+ if (e?.type !== "number") throw Error("Expected number.");
195
+ return e.value;
196
+ }
197
+ expectSymbol(e) {
198
+ let t = this.advance();
199
+ if (t?.type !== "symbol" || t.value !== e) throw Error(`Expected "${e}".`);
200
+ }
201
+ matchSymbol(e) {
202
+ let t = this.peek();
203
+ return t?.type === "symbol" && t.value === e ? (this.advance(), !0) : !1;
204
+ }
205
+ advance() {
206
+ return this.tokens[this.index++];
207
+ }
208
+ peek() {
209
+ return this.tokens[this.index];
210
+ }
211
+ isAtEnd() {
212
+ return this.index >= this.tokens.length;
213
+ }
214
+ }, s = (e) => new o(a(e)).parse(), c = "_", l = new Set([
215
+ "kick",
216
+ "snare",
217
+ "clap",
218
+ "hihat",
219
+ "openhat",
220
+ "tom",
221
+ "rim",
222
+ "cowbell"
223
+ ]), u = (e) => {
224
+ if (e !== c && !l.has(e)) throw Error(`Unknown drum sound: ${e}`);
225
+ }, d = (e) => {
226
+ if (e.kind === "BeatGroup") {
227
+ for (let t of e.steps) d(t);
228
+ return;
229
+ }
230
+ if (e.kind === "BeatParallel") {
231
+ for (let t of e.sounds) u(t.value);
232
+ return;
233
+ }
234
+ u(e.value);
235
+ }, f = (e, t, n) => {
236
+ if (e.length === 0) return [];
237
+ let r = [], i = n / e.length;
238
+ return e.forEach((e, n) => {
239
+ let a = t + n * i;
240
+ switch (e.kind) {
241
+ case "BeatGroup":
242
+ r.push(...f(e.steps, a, i));
243
+ break;
244
+ case "BeatParallel":
245
+ for (let t of e.sounds) t.value !== c && r.push({
246
+ time: a,
247
+ dur: i,
248
+ type: "drum",
249
+ value: t.value
250
+ });
251
+ break;
252
+ case "BeatSound":
253
+ if (e.value === c) break;
254
+ r.push({
255
+ time: a,
256
+ dur: i,
257
+ type: "drum",
258
+ value: e.value
259
+ });
260
+ break;
261
+ }
262
+ }), r;
263
+ }, p = (e) => {
264
+ switch (e.kind) {
265
+ case "BeatExpression": {
266
+ for (let t of e.steps) d(t);
267
+ let t = 1 / e.rate, n = e.steps.length * t;
268
+ return {
269
+ length: n,
270
+ events: f(e.steps, 0, n)
271
+ };
272
+ }
273
+ case "MelodyExpression": {
274
+ let t = 1 / e.rate, n = e.notes.length * t;
275
+ return {
276
+ length: n,
277
+ events: m(e.notes, 0, n)
278
+ };
279
+ }
280
+ case "SongExpression": {
281
+ let t = e.tracks.map(p);
282
+ return {
283
+ length: Math.max(...t.map((e) => e.length)),
284
+ events: t.flatMap((e) => e.events)
285
+ };
286
+ }
287
+ default: throw Error("Unknown AST node");
288
+ }
289
+ }, m = (e, t, n) => {
290
+ let r = n / e.length;
291
+ return e.flatMap((e, n) => {
292
+ let i = t + n * r;
293
+ return e.kind === "MelodyNote" ? e.value === "_" ? [] : [{
294
+ time: i,
295
+ dur: r,
296
+ type: "note",
297
+ value: e.value
298
+ }] : m(e.notes, i, r);
299
+ });
300
+ }, h = (e) => p(s(e)), g = "0.1.0";
301
+ //#endregion
302
+ export { g as VERSION, h as compile, s as parse, a as tokenize };
@@ -0,0 +1,37 @@
1
+ export type AstNode = BeatExpression | MelodyExpression | SongExpression;
2
+ export type BeatExpression = {
3
+ kind: "BeatExpression";
4
+ steps: BeatStep[];
5
+ rate: number;
6
+ };
7
+ export type SongExpression = {
8
+ kind: "SongExpression";
9
+ tracks: AstNode[];
10
+ };
11
+ export type MelodyExpression = {
12
+ kind: "MelodyExpression";
13
+ notes: MelodyStep[];
14
+ rate: number;
15
+ };
16
+ export type MelodyStep = MelodyNote | MelodyGroup;
17
+ export type MelodyNote = {
18
+ kind: "MelodyNote";
19
+ value: string;
20
+ };
21
+ export type MelodyGroup = {
22
+ kind: "MelodyGroup";
23
+ notes: MelodyStep[];
24
+ };
25
+ export type BeatStep = BeatSound | BeatGroup | BeatParallel;
26
+ export type BeatSound = {
27
+ kind: "BeatSound";
28
+ value: string;
29
+ };
30
+ export type BeatGroup = {
31
+ kind: "BeatGroup";
32
+ steps: BeatStep[];
33
+ };
34
+ export type BeatParallel = {
35
+ kind: "BeatParallel";
36
+ sounds: BeatSound[];
37
+ };
@@ -0,0 +1,7 @@
1
+ export type EventType = 'drum' | 'note';
2
+ export type Event = {
3
+ time: number;
4
+ dur: number;
5
+ type: EventType;
6
+ value: string;
7
+ };
@@ -0,0 +1,3 @@
1
+ export * from './ast';
2
+ export * from './event';
3
+ export * from './pattern';
@@ -0,0 +1,5 @@
1
+ import { Event } from './event';
2
+ export type Pattern = {
3
+ length: number;
4
+ events: Event[];
5
+ };
@@ -0,0 +1,2 @@
1
+ import { BeatStep } from '../model/ast';
2
+ export declare const parseBeatPattern: (source: string) => BeatStep[];
@@ -0,0 +1,2 @@
1
+ export * from './parser';
2
+ export * from './tokenizer';
@@ -0,0 +1,2 @@
1
+ import { MelodyStep } from '../model/ast';
2
+ export declare function parseMelodyPattern(source: string): MelodyStep[];
@@ -0,0 +1 @@
1
+ export declare const isSupportedNote: (value: string) => boolean;
@@ -0,0 +1,2 @@
1
+ import { AstNode } from '../model/ast';
2
+ export declare const parse: (source: string) => AstNode;
@@ -0,0 +1,14 @@
1
+ export type Token = {
2
+ type: "identifier";
3
+ value: string;
4
+ } | {
5
+ type: "string";
6
+ value: string;
7
+ } | {
8
+ type: "number";
9
+ value: number;
10
+ } | {
11
+ type: "symbol";
12
+ value: '(' | ')' | '.' | ',';
13
+ };
14
+ export declare const tokenize: (source: string) => Token[];
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@vibuca/synth8-core",
3
+ "version": "0.1.1",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md",
17
+ "LICENSE"
18
+ ],
19
+ "scripts": {
20
+ "build": "vite build",
21
+ "test": "vitest run"
22
+ }
23
+ }