@@ -3,7 +3,24 @@ const DRUM_TOKENS = new Set(["kick", "snare", "hat", "ride", "clap", "tom", "cra
33const LANE_NAMES = [ "chords" , "bass" , "pad" , "lead" , "drums" ] ;
44const NOTE_PATTERN = / ^ [ A - G ] (?: # | b ) ? [ 0 - 8 ] $ / ;
55const REST_TOKENS = new Set ( [ "" , "-" , "." , "rest" ] ) ;
6- const SUPPORTED_SUBDIVISIONS = new Set ( [ 1 , 2 , 4 ] ) ;
6+ const SUPPORTED_SUBDIVISIONS = new Set ( [ 1 , 2 , 4 , 8 , 16 ] ) ;
7+
8+ const CHORD_TONES = {
9+ A : [ "A" , "C#" , "E" ] ,
10+ Am : [ "A" , "C" , "E" ] ,
11+ B : [ "B" , "D#" , "F#" ] ,
12+ Bm : [ "B" , "D" , "F#" ] ,
13+ C : [ "C" , "E" , "G" ] ,
14+ Cm : [ "C" , "Eb" , "G" ] ,
15+ D : [ "D" , "F#" , "A" ] ,
16+ Dm : [ "D" , "F" , "A" ] ,
17+ E : [ "E" , "G#" , "B" ] ,
18+ Em : [ "E" , "G" , "B" ] ,
19+ F : [ "F" , "A" , "C" ] ,
20+ Fm : [ "F" , "Ab" , "C" ] ,
21+ G : [ "G" , "B" , "D" ] ,
22+ Gm : [ "G" , "Bb" , "D" ]
23+ } ;
724
825export class InstrumentGridParser {
926 parse ( input = { } ) {
@@ -21,7 +38,7 @@ export class InstrumentGridParser {
2138 }
2239 if ( ! SUPPORTED_SUBDIVISIONS . has ( subdivision . value ) ) {
2340 return {
24- message : `Unsupported timing subdivision: ${ subdivision . value } . Use 1, 2, or 4 .` ,
41+ message : `Unsupported timing subdivision: ${ subdivision . value } . Use 1, 2, 4, 8, or 16 .` ,
2542 ok : false
2643 } ;
2744 }
@@ -35,6 +52,7 @@ export class InstrumentGridParser {
3552 barCount,
3653 beatsPerBar : beatsPerBar . value ,
3754 lane,
55+ generatedSource : input . generatedLanes ?. [ lane ] ,
3856 sections : sections . value ,
3957 source : input . lanes ?. [ lane ] ,
4058 stepsPerBar,
@@ -67,13 +85,49 @@ export class InstrumentGridParser {
6785 sectionSummary : normalizedSections . map ( ( section ) => `${ section . label } : ${ section . bars } bar${ section . bars === 1 ? "" : "s" } ` ) . join ( "; " ) ,
6886 sections : normalizedSections ,
6987 subdivision : subdivision . value ,
88+ subdivisionLabel : `1/${ subdivision . value } ` ,
7089 timeline,
7190 totalSteps : barCount * stepsPerBar ,
7291 warningSummary : warnings . length ? warnings . join ( "; " ) : "none" ,
7392 warnings
7493 } ;
7594 }
7695
96+ generateLane ( input = { } , lane ) {
97+ if ( ! [ "bass" , "pad" , "lead" , "drums" ] . includes ( lane ) ) {
98+ return { message : `Unsupported generated lane: ${ lane } .` , ok : false } ;
99+ }
100+ const sections = this . parseSections ( input . sections ) ;
101+ if ( ! sections . ok ) {
102+ return sections ;
103+ }
104+ const beatsPerBar = this . parsePositiveInteger ( input . beatsPerBar , "beats per bar" ) ;
105+ if ( ! beatsPerBar . ok ) {
106+ return beatsPerBar ;
107+ }
108+ const subdivision = this . parsePositiveInteger ( input . subdivision , "timing subdivision" ) ;
109+ if ( ! subdivision . ok ) {
110+ return subdivision ;
111+ }
112+ if ( ! SUPPORTED_SUBDIVISIONS . has ( subdivision . value ) ) {
113+ return {
114+ message : `Unsupported timing subdivision: ${ subdivision . value } . Use 1, 2, 4, 8, or 16.` ,
115+ ok : false
116+ } ;
117+ }
118+ const barCount = sections . value . reduce ( ( total , section ) => total + section . bars , 0 ) ;
119+ const stepsPerBar = beatsPerBar . value * subdivision . value ;
120+ if ( lane === "drums" ) {
121+ return this . generateDrums ( { barCount, beatsPerBar : beatsPerBar . value , stepsPerBar, subdivision : subdivision . value } ) ;
122+ }
123+ return this . generateFromChords ( {
124+ barCount,
125+ lane,
126+ source : input . lanes ?. chords ,
127+ stepsPerBar
128+ } ) ;
129+ }
130+
77131 parseSections ( source ) {
78132 const text = String ( source || "" ) . trim ( ) ;
79133 if ( ! text ) {
@@ -108,9 +162,10 @@ export class InstrumentGridParser {
108162 return { ok : true , value : number } ;
109163 }
110164
111- parseLane ( { barCount, beatsPerBar, lane, sections, source, stepsPerBar, subdivision } ) {
165+ parseLane ( { barCount, beatsPerBar, generatedSource , lane, sections, source, stepsPerBar, subdivision } ) {
112166 const text = String ( source || "" ) . trim ( ) ;
113167 const bars = text ? text . split ( "|" ) . map ( ( bar ) => bar . trim ( ) ) : [ ] ;
168+ const generatedBars = this . parseGeneratedBars ( { barCount, generatedSource, stepsPerBar } ) ;
114169 if ( bars . length && bars . length !== barCount ) {
115170 return {
116171 message : `Malformed bar counts for ${ lane } . Expected ${ barCount } bar${ barCount === 1 ? "" : "s" } from the section map, received ${ bars . length } .` ,
@@ -130,7 +185,8 @@ export class InstrumentGridParser {
130185 }
131186 for ( let stepIndex = 0 ; stepIndex < stepsPerBar ; stepIndex += 1 ) {
132187 const token = tokens [ stepIndex ] || "" ;
133- const normalized = this . normalizeToken ( { barIndex, beatsPerBar, lane, sections, stepIndex, subdivision, token } ) ;
188+ const generatedToken = generatedBars [ barIndex ] ?. [ stepIndex ] || "" ;
189+ const normalized = this . normalizeToken ( { barIndex, beatsPerBar, generatedToken, lane, sections, stepIndex, subdivision, token } ) ;
134190 if ( ! normalized . ok ) {
135191 return normalized ;
136192 }
@@ -142,14 +198,16 @@ export class InstrumentGridParser {
142198 return { cells, events, ok : true , warnings } ;
143199 }
144200
145- normalizeToken ( { barIndex, beatsPerBar, lane, sections, stepIndex, subdivision, token } ) {
201+ normalizeToken ( { barIndex, beatsPerBar, generatedToken , lane, sections, stepIndex, subdivision, token } ) {
146202 const value = token . trim ( ) ;
147203 const timing = this . timingForCell ( { barIndex, beatsPerBar, sections, stepIndex, subdivision } ) ;
204+ const tokenSource = this . tokenSource ( { generatedToken, value } ) ;
148205 const cell = {
149206 bar : timing . bar ,
150207 beat : timing . beat ,
151208 lane,
152209 section : timing . section ,
210+ source : tokenSource ,
153211 subdivision : timing . subdivision ,
154212 subdivisionStep : timing . subdivisionStep ,
155213 token : value
@@ -202,11 +260,143 @@ export class InstrumentGridParser {
202260 kind,
203261 lane : cell . lane ,
204262 section : cell . section ,
263+ source : cell . source ,
205264 subdivision : cell . subdivision ,
206265 value
207266 } ;
208267 }
209268
269+ parseGeneratedBars ( { barCount, generatedSource, stepsPerBar } ) {
270+ const text = String ( generatedSource || "" ) . trim ( ) ;
271+ const bars = text ? text . split ( "|" ) . map ( ( bar ) => bar . trim ( ) ) : [ ] ;
272+ if ( bars . length !== barCount ) {
273+ return [ ] ;
274+ }
275+ return bars . map ( ( bar ) => {
276+ const tokens = bar ? bar . split ( / \s + / ) . filter ( Boolean ) : [ ] ;
277+ return tokens . length === stepsPerBar ? tokens : [ ] ;
278+ } ) ;
279+ }
280+
281+ tokenSource ( { generatedToken, value } ) {
282+ const generated = String ( generatedToken || "" ) . trim ( ) ;
283+ if ( REST_TOKENS . has ( String ( value || "" ) . toLowerCase ( ) ) ) {
284+ return generated && ! REST_TOKENS . has ( generated . toLowerCase ( ) ) ? "manual" : "empty" ;
285+ }
286+ return generated === value ? "generated" : "manual" ;
287+ }
288+
289+ generateDrums ( { barCount, beatsPerBar, stepsPerBar, subdivision } ) {
290+ const bars = [ ] ;
291+ for ( let barIndex = 0 ; barIndex < barCount ; barIndex += 1 ) {
292+ const tokens = [ ] ;
293+ for ( let stepIndex = 0 ; stepIndex < stepsPerBar ; stepIndex += 1 ) {
294+ if ( stepIndex % subdivision !== 0 ) {
295+ tokens . push ( "-" ) ;
296+ continue ;
297+ }
298+ const beat = Math . floor ( stepIndex / subdivision ) + 1 ;
299+ tokens . push ( beat === 1 ? "kick" : beat === Math . ceil ( beatsPerBar / 2 ) + 1 ? "snare" : "hat" ) ;
300+ }
301+ bars . push ( tokens . join ( " " ) ) ;
302+ }
303+ return {
304+ generatedCount : barCount * beatsPerBar ,
305+ lane : "drums" ,
306+ message : `Generated Basic Drums for ${ barCount } bar${ barCount === 1 ? "" : "s" } .` ,
307+ ok : true ,
308+ skippedEmptyBars : 0 ,
309+ text : bars . join ( " | " ) ,
310+ warningSummary : "none" ,
311+ warnings : [ ]
312+ } ;
313+ }
314+
315+ generateFromChords ( { barCount, lane, source, stepsPerBar } ) {
316+ const text = String ( source || "" ) . trim ( ) ;
317+ const bars = text ? text . split ( "|" ) . map ( ( bar ) => bar . trim ( ) ) : Array . from ( { length : barCount } , ( ) => "" ) ;
318+ if ( bars . length !== barCount ) {
319+ return {
320+ message : `Malformed bar counts for chord generation. Expected ${ barCount } bar${ barCount === 1 ? "" : "s" } , received ${ bars . filter ( Boolean ) . length || bars . length } .` ,
321+ ok : false
322+ } ;
323+ }
324+ const generatedBars = [ ] ;
325+ const warnings = [ ] ;
326+ let generatedCount = 0 ;
327+ let skippedEmptyBars = 0 ;
328+ bars . forEach ( ( bar , barIndex ) => {
329+ const tokens = bar ? bar . split ( / \s + / ) . filter ( Boolean ) : [ ] ;
330+ if ( ! tokens . length || tokens . every ( ( token ) => REST_TOKENS . has ( token . toLowerCase ( ) ) ) ) {
331+ skippedEmptyBars += 1 ;
332+ generatedBars . push ( Array . from ( { length : stepsPerBar } , ( ) => "-" ) . join ( " " ) ) ;
333+ return ;
334+ }
335+ if ( tokens . length !== stepsPerBar ) {
336+ warnings . push ( `Skipped bar ${ barIndex + 1 } : expected ${ stepsPerBar } chord slot${ stepsPerBar === 1 ? "" : "s" } , received ${ tokens . length } .` ) ;
337+ generatedBars . push ( Array . from ( { length : stepsPerBar } , ( ) => "-" ) . join ( " " ) ) ;
338+ return ;
339+ }
340+ const generatedTokens = tokens . map ( ( chord , stepIndex ) => {
341+ const generated = this . generateTokenForChord ( { chord, lane, stepIndex } ) ;
342+ if ( ! generated . ok ) {
343+ warnings . push ( `${ generated . message } Bar ${ barIndex + 1 } , slot ${ stepIndex + 1 } skipped.` ) ;
344+ return "-" ;
345+ }
346+ generatedCount += 1 ;
347+ return generated . value ;
348+ } ) ;
349+ generatedBars . push ( generatedTokens . join ( " " ) ) ;
350+ } ) ;
351+ const skipMessage = skippedEmptyBars ? ` Skipped ${ skippedEmptyBars } empty bar${ skippedEmptyBars === 1 ? "" : "s" } .` : "" ;
352+ return {
353+ generatedCount,
354+ lane,
355+ message : `Generated ${ this . generatedLaneLabel ( lane ) } from chords with ${ generatedCount } cell${ generatedCount === 1 ? "" : "s" } .${ skipMessage } ` ,
356+ ok : true ,
357+ skippedEmptyBars,
358+ text : generatedBars . join ( " | " ) ,
359+ warningSummary : warnings . length ? warnings . join ( "; " ) : "none" ,
360+ warnings
361+ } ;
362+ }
363+
364+ generateTokenForChord ( { chord, lane, stepIndex } ) {
365+ const value = String ( chord || "" ) . trim ( ) ;
366+ if ( REST_TOKENS . has ( value . toLowerCase ( ) ) ) {
367+ return { ok : true , value : "-" } ;
368+ }
369+ if ( ! CHORD_PATTERN . test ( value ) ) {
370+ return { message : `Invalid chord "${ value } " for lane generation.` , ok : false } ;
371+ }
372+ const simple = value . match ( / ^ ( [ A - G ] (?: # | b ) ? ) ( m ? ) $ / ) ;
373+ if ( ! simple ) {
374+ return { message : `Unsupported chord pattern "${ value } " for lane generation.` , ok : false } ;
375+ }
376+ const chordName = `${ simple [ 1 ] } ${ simple [ 2 ] || "" } ` ;
377+ const tones = CHORD_TONES [ chordName ] ;
378+ if ( ! tones ) {
379+ return { message : `Invalid note generation for chord "${ value } ".` , ok : false } ;
380+ }
381+ if ( lane === "bass" ) {
382+ return { ok : true , value : `${ tones [ 0 ] } 2` } ;
383+ }
384+ if ( lane === "pad" ) {
385+ return { ok : true , value } ;
386+ }
387+ return { ok : true , value : `${ tones [ stepIndex % tones . length ] } 4` } ;
388+ }
389+
390+ generatedLaneLabel ( lane ) {
391+ return lane === "bass"
392+ ? "Bass"
393+ : lane === "pad"
394+ ? "Pad"
395+ : lane === "lead"
396+ ? "Arpeggio"
397+ : "Basic Drums" ;
398+ }
399+
210400 timingForCell ( { barIndex, beatsPerBar, sections, stepIndex, subdivision } ) {
211401 const bar = barIndex + 1 ;
212402 const section = sections . find ( ( entry ) => bar >= entry . startBar && bar < entry . startBar + entry . bars ) ?. label || "unknown" ;
0 commit comments