scribbletune 5.5.0 → 5.5.2
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 +80 -11
- package/dist/cli.cjs +125 -31
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<h1 align="center">Scribbletune</h1>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
Create music with JavaScript. Use simple strings and arrays to craft rhythms, melodies, and chord progressions — then export MIDI files or play them live in the browser with <a href="https://tonejs.github.io/">Tone.js</a
|
|
8
|
+
Create music with JavaScript. Use simple strings and arrays to craft rhythms, melodies, and chord progressions — then export MIDI files or play them live in the browser with <a href="https://tonejs.github.io/">Tone.js</a> or use the CLI to directly emit MIDI file from your terminal.
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
@@ -23,9 +23,9 @@ npm install scribbletune
|
|
|
23
23
|
|
|
24
24
|
## Quick start
|
|
25
25
|
|
|
26
|
-
### CLI
|
|
26
|
+
### Option 1: CLI
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
If you installed Scribbletune globally via `npm i -g scribbletune` then you can directly use `scribbletune` as the command. If you installed it locally via `npm i scribbletune` then please use `npx scribbletune` as the command.
|
|
29
29
|
|
|
30
30
|
Run modes:
|
|
31
31
|
|
|
@@ -39,25 +39,55 @@ npm install scribbletune
|
|
|
39
39
|
npx scribbletune --help
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
+
Quick command examples:
|
|
43
|
+
|
|
42
44
|
#### Command format
|
|
43
45
|
|
|
44
46
|
```bash
|
|
45
|
-
scribbletune --riff <root> <mode> <pattern> [
|
|
47
|
+
scribbletune --riff <root> <mode> <pattern> [subdiv] [options]
|
|
46
48
|
scribbletune --chord <root> <mode> <progression|random> <pattern> [subdiv] [options]
|
|
47
49
|
scribbletune --arp <root> <mode> <progression|random> <pattern> [subdiv] [options]
|
|
48
50
|
```
|
|
49
51
|
|
|
52
|
+
Progression input rules for `--chord` and `--arp`:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
1645 # degree digits
|
|
56
|
+
"I IV vi V" # roman numerals (space separated)
|
|
57
|
+
I,IV,vi,V # roman numerals (comma separated)
|
|
58
|
+
random # generated progression
|
|
59
|
+
CM-FM-Am-GM # explicit chord names (`root` and `mode` are ignored)
|
|
60
|
+
```
|
|
61
|
+
|
|
50
62
|
Common options:
|
|
51
63
|
|
|
52
64
|
```bash
|
|
53
|
-
--outfile <file.mid>
|
|
54
|
-
--bpm <number>
|
|
65
|
+
--outfile <file.mid> # default: music.mid
|
|
55
66
|
--subdiv <4n|8n|1m...>
|
|
56
67
|
--sizzle [sin|cos|rampUp|rampDown] [reps]
|
|
57
68
|
--sizzle-reps <number>
|
|
58
69
|
--amp <0-127>
|
|
59
70
|
--accent <x--x...>
|
|
60
71
|
--accent-low <0-127>
|
|
72
|
+
--style <letters> # riff motif/style, e.g. AABC
|
|
73
|
+
--fit-pattern # explicit enable (already enabled by default)
|
|
74
|
+
--no-fit-pattern # disable automatic pattern fitting
|
|
75
|
+
--bpm <number> # your DAW may or may not support it
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Note: if your pattern uses `[` and `]` (for subdivisions), quote it in shell:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
scribbletune --arp C3 major 1 'x-x[xx]-x-[xx]' 16n
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Pattern helpers:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
x.repeat(4) # -> xxxx
|
|
88
|
+
'x-x[xx]'.repeat(2)
|
|
89
|
+
2(x-x[xx]) # prefix repeat shorthand
|
|
90
|
+
(x-x[xx])2 # suffix repeat shorthand
|
|
61
91
|
```
|
|
62
92
|
|
|
63
93
|
#### `--riff` examples
|
|
@@ -66,10 +96,21 @@ Common options:
|
|
|
66
96
|
# Basic riff from scale
|
|
67
97
|
scribbletune --riff C3 phrygian x-xRx_RR --outfile riff.mid
|
|
68
98
|
|
|
69
|
-
# With
|
|
70
|
-
scribbletune --riff C3 phrygian x-xRx_RR
|
|
99
|
+
# With motif/style and positional subdiv
|
|
100
|
+
scribbletune --riff C3 phrygian x-xRx_RR 8n --style AABC --sizzle sin 2 --outfile riff-aabc.mid
|
|
101
|
+
|
|
102
|
+
# Set riff subdivision via positional arg
|
|
103
|
+
scribbletune --riff C3 phrygian x-xRx_RR 8n --style AABC --outfile riff-8n.mid
|
|
104
|
+
|
|
105
|
+
# Pattern with subdivisions (quote [] in shell)
|
|
106
|
+
scribbletune --riff C3 phrygian 'x-x[xx]-x-[xx]' 8n --style AABC --outfile riff-subdiv.mid
|
|
71
107
|
```
|
|
72
108
|
|
|
109
|
+
Riff + motif note:
|
|
110
|
+
- `--style` creates riff sections by repeating the full pattern per letter.
|
|
111
|
+
- Example: `--style AABC` with pattern `x-x[xx]` creates 4 sections: `A`, `A`, `B`, `C`.
|
|
112
|
+
- Repeated letters reuse the exact same generated section (same rhythm and same notes, including random `R` choices).
|
|
113
|
+
|
|
73
114
|
#### `--chord` examples
|
|
74
115
|
|
|
75
116
|
```bash
|
|
@@ -84,6 +125,9 @@ scribbletune --chord C3 major random xxxx 1m --outfile chords-random.mid
|
|
|
84
125
|
|
|
85
126
|
# Explicit chord names (root/mode currently ignored for this style)
|
|
86
127
|
scribbletune --chord C3 major CM-FM-Am-GM xxxx 1m --outfile chords-explicit.mid
|
|
128
|
+
|
|
129
|
+
# Subdivisions in pattern
|
|
130
|
+
scribbletune --chord C3 major I,IV,vi,V 'x-x[xx]-x-[xx]' 8n --outfile chords-subdiv.mid
|
|
87
131
|
```
|
|
88
132
|
|
|
89
133
|
#### `--arp` examples
|
|
@@ -92,13 +136,32 @@ scribbletune --chord C3 major CM-FM-Am-GM xxxx 1m --outfile chords-explicit.mid
|
|
|
92
136
|
# Arp from degree progression
|
|
93
137
|
scribbletune --arp C3 major 1736 xxxx 1m --sizzle cos 4 --outfile arp-1736.mid
|
|
94
138
|
|
|
139
|
+
# Single degree "1" means tonic chord in the chosen key/mode
|
|
140
|
+
scribbletune --arp C3 major 1 xxxx 4n --outfile arp-degree-1.mid
|
|
141
|
+
|
|
95
142
|
# Arp from explicit chords
|
|
96
|
-
scribbletune --arp C3 major CM-FM-Am-GM xxxx 1m --count 4 --order
|
|
143
|
+
scribbletune --arp C3 major CM-FM-Am-GM xxxx 1m --count 4 --order 1234 --outfile arp-explicit.mid
|
|
144
|
+
|
|
145
|
+
# Custom note order inside each arpeggiated chord (one-based)
|
|
146
|
+
scribbletune --arp C3 major 1 xxxx 4n --order 2143 --outfile arp-order-2143.mid
|
|
147
|
+
|
|
148
|
+
# Same custom order using local dist build
|
|
149
|
+
node dist/cli.cjs --arp C3 major 1 xxxx 4n --order 2143 --outfile arp-order-local.mid
|
|
150
|
+
|
|
151
|
+
# Auto-fit is default (single x expands to full generated arp length)
|
|
152
|
+
scribbletune --arp C3 major 1736 x 4n --outfile arp-fit-default.mid
|
|
153
|
+
|
|
154
|
+
# Disable auto-fit if you want a short clip
|
|
155
|
+
scribbletune --arp C3 major 1736 x 4n --no-fit-pattern --outfile arp-no-fit.mid
|
|
97
156
|
```
|
|
98
157
|
|
|
158
|
+
`--order` behavior:
|
|
159
|
+
- One-based order is supported (`1234`, `2143`) and is recommended.
|
|
160
|
+
- Zero-based order is also accepted for backward compatibility (`0123`, `1032`).
|
|
161
|
+
|
|
99
162
|
Run `scribbletune --help` to see the latest CLI usage text.
|
|
100
163
|
|
|
101
|
-
###
|
|
164
|
+
### Option 2: Node.js
|
|
102
165
|
|
|
103
166
|
```js
|
|
104
167
|
import { scale, clip, midi } from 'scribbletune';
|
|
@@ -111,7 +174,7 @@ midi(c, 'c-major.mid');
|
|
|
111
174
|
|
|
112
175
|
Run it with `node` and open the `.mid` file in Ableton Live, GarageBand, Logic, or any DAW.
|
|
113
176
|
|
|
114
|
-
###
|
|
177
|
+
### Option 3: Browser (with Tone.js)
|
|
115
178
|
|
|
116
179
|
Scribbletune's browser entry point adds `Session`, `Channel`, and live `clip()` support on top of [Tone.js](https://tonejs.github.io/).
|
|
117
180
|
|
|
@@ -286,6 +349,12 @@ npm run lint # check with biome
|
|
|
286
349
|
npm run dev # build in watch mode
|
|
287
350
|
```
|
|
288
351
|
|
|
352
|
+
If developing new features for the CLI, use the following command after running `npm run build` to test before publishing,
|
|
353
|
+
|
|
354
|
+
```bash
|
|
355
|
+
node dist/cli.cjs --help
|
|
356
|
+
```
|
|
357
|
+
|
|
289
358
|
## License
|
|
290
359
|
|
|
291
360
|
MIT
|
package/dist/cli.cjs
CHANGED
|
@@ -546,12 +546,12 @@ var progression = (scaleType, count = 4) => {
|
|
|
546
546
|
|
|
547
547
|
// src/cli.ts
|
|
548
548
|
var HELP_TEXT = `Usage:
|
|
549
|
-
scribbletune --riff <root> <mode> <pattern> [
|
|
549
|
+
scribbletune --riff <root> <mode> <pattern> [subdiv]
|
|
550
550
|
scribbletune --chord <root> <mode> <progression|random> <pattern> [subdiv]
|
|
551
551
|
scribbletune --arp <root> <mode> <progression|random> <pattern> [subdiv]
|
|
552
552
|
|
|
553
553
|
Examples:
|
|
554
|
-
scribbletune --riff C3 phrygian x-xRx_RR
|
|
554
|
+
scribbletune --riff C3 phrygian x-xRx_RR 8n --style AABC --sizzle sin 2 --outfile riff.mid
|
|
555
555
|
scribbletune --chord C3 major 1645 xxxx 1m --sizzle cos 1 --outfile chord.mid
|
|
556
556
|
scribbletune --chord C3 major CM-FM-Am-GM xxxx 1m
|
|
557
557
|
scribbletune --chord C3 major random xxxx 1m
|
|
@@ -568,6 +568,9 @@ Options:
|
|
|
568
568
|
--accent-low <0-127> Accent low level
|
|
569
569
|
--count <2-8> Arp note count (arp command only)
|
|
570
570
|
--order <digits> Arp order string (arp command only)
|
|
571
|
+
--style <letters> Riff motif/style pattern (e.g. AABC)
|
|
572
|
+
--fit-pattern Repeat pattern until it can consume all generated notes (default)
|
|
573
|
+
--no-fit-pattern Disable automatic pattern fitting
|
|
571
574
|
-h, --help Show this help
|
|
572
575
|
`;
|
|
573
576
|
var romanByDigit = (progDigits, mode) => {
|
|
@@ -585,11 +588,6 @@ var romanByDigit = (progDigits, mode) => {
|
|
|
585
588
|
});
|
|
586
589
|
return { chordDegrees: romans.join(" "), raw: progDigits };
|
|
587
590
|
};
|
|
588
|
-
var setOctave = (note, octaveShift = 0) => {
|
|
589
|
-
const base = note.replace(/\d+/g, "");
|
|
590
|
-
const oct = Number(note.match(/\d+/)?.[0] || "4");
|
|
591
|
-
return `${base}${oct + octaveShift}`;
|
|
592
|
-
};
|
|
593
591
|
var parseProgression = (root, mode, progressionInput) => {
|
|
594
592
|
if (progressionInput === "random") {
|
|
595
593
|
const modeType = mode === "minor" || mode === "m" ? "minor" : "major";
|
|
@@ -606,6 +604,53 @@ var parseProgression = (root, mode, progressionInput) => {
|
|
|
606
604
|
}
|
|
607
605
|
return progressionInput.replace(/-/g, " ");
|
|
608
606
|
};
|
|
607
|
+
var normalizeArpOrder = (order) => {
|
|
608
|
+
if (!/^\d+$/.test(order)) {
|
|
609
|
+
throw new TypeError("Invalid value for --order");
|
|
610
|
+
}
|
|
611
|
+
if (order.includes("0")) {
|
|
612
|
+
return order;
|
|
613
|
+
}
|
|
614
|
+
return order.split("").map((ch) => {
|
|
615
|
+
const n = Number(ch);
|
|
616
|
+
if (n < 1) {
|
|
617
|
+
throw new TypeError("Invalid value for --order");
|
|
618
|
+
}
|
|
619
|
+
return String(n - 1);
|
|
620
|
+
}).join("");
|
|
621
|
+
};
|
|
622
|
+
var expandPatternSyntax = (pattern) => {
|
|
623
|
+
const jsRepeatQuoted = pattern.match(/^(['"])(.+)\1\.repeat\((\d+)\)$/);
|
|
624
|
+
if (jsRepeatQuoted) {
|
|
625
|
+
return jsRepeatQuoted[2].repeat(Number(jsRepeatQuoted[3]));
|
|
626
|
+
}
|
|
627
|
+
const jsRepeatUnquoted = pattern.match(/^(.+)\.repeat\((\d+)\)$/);
|
|
628
|
+
if (jsRepeatUnquoted) {
|
|
629
|
+
return jsRepeatUnquoted[1].repeat(Number(jsRepeatUnquoted[2]));
|
|
630
|
+
}
|
|
631
|
+
const prefixRepeat = pattern.match(/^(\d+)\((.+)\)$/);
|
|
632
|
+
if (prefixRepeat) {
|
|
633
|
+
return prefixRepeat[2].repeat(Number(prefixRepeat[1]));
|
|
634
|
+
}
|
|
635
|
+
const suffixRepeat = pattern.match(/^\((.+)\)(\d+)$/);
|
|
636
|
+
if (suffixRepeat) {
|
|
637
|
+
return suffixRepeat[1].repeat(Number(suffixRepeat[2]));
|
|
638
|
+
}
|
|
639
|
+
return pattern;
|
|
640
|
+
};
|
|
641
|
+
var countPatternSteps = (pattern) => (pattern.match(/[xR]/g) || []).length;
|
|
642
|
+
var fitPatternToNoteCount = (pattern, noteCount) => {
|
|
643
|
+
const stepCount = countPatternSteps(pattern);
|
|
644
|
+
if (!stepCount || stepCount >= noteCount) {
|
|
645
|
+
return pattern;
|
|
646
|
+
}
|
|
647
|
+
const reps = Math.ceil(noteCount / stepCount);
|
|
648
|
+
return pattern.repeat(reps);
|
|
649
|
+
};
|
|
650
|
+
var resolvePattern = (rawPattern, noteCount, fitPattern = false) => {
|
|
651
|
+
const expanded = expandPatternSyntax(rawPattern);
|
|
652
|
+
return fitPattern ? fitPatternToNoteCount(expanded, noteCount) : expanded;
|
|
653
|
+
};
|
|
609
654
|
var parseCliArgs = (argv) => {
|
|
610
655
|
if (argv.length === 0 || argv.includes("-h") || argv.includes("--help")) {
|
|
611
656
|
return null;
|
|
@@ -623,7 +668,8 @@ var parseCliArgs = (argv) => {
|
|
|
623
668
|
const options = {
|
|
624
669
|
command: commandArg,
|
|
625
670
|
positionals,
|
|
626
|
-
outfile: "music.mid"
|
|
671
|
+
outfile: "music.mid",
|
|
672
|
+
fitPattern: true
|
|
627
673
|
};
|
|
628
674
|
let i = 1;
|
|
629
675
|
while (i < argv.length) {
|
|
@@ -701,6 +747,21 @@ var parseCliArgs = (argv) => {
|
|
|
701
747
|
i += 2;
|
|
702
748
|
continue;
|
|
703
749
|
}
|
|
750
|
+
if (token === "--style") {
|
|
751
|
+
options.style = argv[i + 1];
|
|
752
|
+
i += 2;
|
|
753
|
+
continue;
|
|
754
|
+
}
|
|
755
|
+
if (token === "--fit-pattern") {
|
|
756
|
+
options.fitPattern = true;
|
|
757
|
+
i += 1;
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
if (token === "--no-fit-pattern") {
|
|
761
|
+
options.fitPattern = false;
|
|
762
|
+
i += 1;
|
|
763
|
+
continue;
|
|
764
|
+
}
|
|
704
765
|
throw new TypeError(`Unknown option "${token}"`);
|
|
705
766
|
}
|
|
706
767
|
return options;
|
|
@@ -711,31 +772,57 @@ var baseClipParams = (parsed) => {
|
|
|
711
772
|
sizzleReps: parsed.sizzleReps,
|
|
712
773
|
amp: parsed.amp,
|
|
713
774
|
accent: parsed.accent,
|
|
714
|
-
accentLow: parsed.accentLow
|
|
715
|
-
subdiv: parsed.subdiv
|
|
775
|
+
accentLow: parsed.accentLow
|
|
716
776
|
};
|
|
717
777
|
};
|
|
718
778
|
var makeRiff = (parsed) => {
|
|
719
|
-
const [root, mode, pattern,
|
|
779
|
+
const [root, mode, pattern, subdiv] = parsed.positionals;
|
|
780
|
+
const style = parsed.style;
|
|
720
781
|
if (!root || !mode || !pattern) {
|
|
721
|
-
throw new TypeError(
|
|
722
|
-
"riff requires: <root> <mode> <pattern> [octaveShift] [motif]"
|
|
723
|
-
);
|
|
782
|
+
throw new TypeError("riff requires: <root> <mode> <pattern> [subdiv]");
|
|
724
783
|
}
|
|
725
|
-
const
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
784
|
+
const riffScale = (0, import_harmonics4.scale)(`${root} ${mode}`);
|
|
785
|
+
let riffNotes = riffScale;
|
|
786
|
+
let resolvedPattern = resolvePattern(
|
|
787
|
+
pattern,
|
|
788
|
+
riffNotes.length,
|
|
789
|
+
parsed.fitPattern
|
|
790
|
+
);
|
|
791
|
+
if (style && style.length) {
|
|
792
|
+
const sectionPattern = expandPatternSyntax(pattern);
|
|
793
|
+
const letters = style.toUpperCase().split("");
|
|
794
|
+
const sectionCache = {};
|
|
795
|
+
const combined = [];
|
|
796
|
+
const clipParams = {
|
|
797
|
+
...baseClipParams(parsed),
|
|
798
|
+
subdiv: parsed.subdiv || subdiv
|
|
799
|
+
};
|
|
800
|
+
for (const letter of letters) {
|
|
801
|
+
if (!sectionCache[letter]) {
|
|
802
|
+
const idx = letter.charCodeAt(0) - 65;
|
|
803
|
+
const note = riffScale[idx >= 0 ? idx % riffScale.length : 0];
|
|
804
|
+
sectionCache[letter] = clip({
|
|
805
|
+
notes: [note],
|
|
806
|
+
randomNotes: riffScale,
|
|
807
|
+
pattern: sectionPattern,
|
|
808
|
+
...clipParams
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
combined.push(
|
|
812
|
+
...sectionCache[letter].map((event) => ({
|
|
813
|
+
...event,
|
|
814
|
+
note: event.note ? [...event.note] : null
|
|
815
|
+
}))
|
|
816
|
+
);
|
|
731
817
|
}
|
|
732
|
-
return
|
|
733
|
-
}
|
|
818
|
+
return combined;
|
|
819
|
+
}
|
|
734
820
|
return clip({
|
|
735
821
|
notes: riffNotes,
|
|
736
822
|
randomNotes: riffScale,
|
|
737
|
-
pattern,
|
|
738
|
-
...baseClipParams(parsed)
|
|
823
|
+
pattern: resolvedPattern,
|
|
824
|
+
...baseClipParams(parsed),
|
|
825
|
+
subdiv: parsed.subdiv || subdiv
|
|
739
826
|
});
|
|
740
827
|
};
|
|
741
828
|
var makeChord = (parsed) => {
|
|
@@ -746,11 +833,13 @@ var makeChord = (parsed) => {
|
|
|
746
833
|
);
|
|
747
834
|
}
|
|
748
835
|
const chords = parseProgression(root, mode, progressionInput);
|
|
836
|
+
const chordCount = chords.trim().split(/\s+/).length;
|
|
837
|
+
const resolvedPattern = resolvePattern(pattern, chordCount, parsed.fitPattern);
|
|
749
838
|
return clip({
|
|
750
839
|
notes: chords,
|
|
751
|
-
pattern,
|
|
752
|
-
|
|
753
|
-
|
|
840
|
+
pattern: resolvedPattern,
|
|
841
|
+
...baseClipParams(parsed),
|
|
842
|
+
subdiv: parsed.subdiv || subdiv
|
|
754
843
|
});
|
|
755
844
|
};
|
|
756
845
|
var makeArp = (parsed) => {
|
|
@@ -764,13 +853,18 @@ var makeArp = (parsed) => {
|
|
|
764
853
|
const arpNotes = arp({
|
|
765
854
|
chords,
|
|
766
855
|
count: parsed.count || 4,
|
|
767
|
-
order: parsed.order
|
|
856
|
+
order: parsed.order ? normalizeArpOrder(parsed.order) : "0123"
|
|
768
857
|
});
|
|
858
|
+
const resolvedPattern = resolvePattern(
|
|
859
|
+
pattern,
|
|
860
|
+
arpNotes.length,
|
|
861
|
+
parsed.fitPattern
|
|
862
|
+
);
|
|
769
863
|
return clip({
|
|
770
864
|
notes: arpNotes,
|
|
771
|
-
pattern,
|
|
772
|
-
|
|
773
|
-
|
|
865
|
+
pattern: resolvedPattern,
|
|
866
|
+
...baseClipParams(parsed),
|
|
867
|
+
subdiv: parsed.subdiv || subdiv
|
|
774
868
|
});
|
|
775
869
|
};
|
|
776
870
|
var runCli = (argv, deps) => {
|