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.
Files changed (3) hide show
  1. package/README.md +80 -11
  2. package/dist/cli.cjs +125 -31
  3. 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
- The package now ships a CLI binary: `scribbletune`.
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> [octaveShift] [motif] [options]
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> # default: music.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 octave shift and motif
70
- scribbletune --riff C3 phrygian x-xRx_RR 0 AABC --sizzle sin 2 --outfile riff-aabc.mid
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 0123 --outfile arp-explicit.mid
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
- ### Generate a MIDI file (Node.js)
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
- ### Play in the browser (with Tone.js)
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> [octaveShift] [motif]
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 0 AABC --sizzle sin 2 --outfile riff.mid
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, octaveShiftArg, motif] = parsed.positionals;
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 octaveShift = Number(octaveShiftArg || "0");
726
- const riffScale = (0, import_harmonics4.scale)(`${setOctave(root, octaveShift)} ${mode}`);
727
- const riffNotes = motif && motif.length ? motif.toUpperCase().split("").map((letter) => {
728
- const idx = letter.charCodeAt(0) - 65;
729
- if (idx < 0) {
730
- return riffScale[0];
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 riffScale[idx % riffScale.length];
733
- }) : riffScale;
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
- subdiv: parsed.subdiv || subdiv,
753
- ...baseClipParams(parsed)
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 || "0123"
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
- subdiv: parsed.subdiv || subdiv,
773
- ...baseClipParams(parsed)
865
+ pattern: resolvedPattern,
866
+ ...baseClipParams(parsed),
867
+ subdiv: parsed.subdiv || subdiv
774
868
  });
775
869
  };
776
870
  var runCli = (argv, deps) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scribbletune",
3
- "version": "5.5.0",
3
+ "version": "5.5.2",
4
4
  "description": "Create music with JavaScript and Node.js!",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",