songsheet 7.2.0 → 7.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "songsheet",
3
- "version": "7.2.0",
3
+ "version": "7.3.0",
4
4
  "description": "A songsheet interpreter",
5
5
  "type": "module",
6
6
  "exports": "./index.js",
package/src/notation.js CHANGED
@@ -173,7 +173,9 @@ function convertSong(song, key, toNNS) {
173
173
  ...measure,
174
174
  chords: measure.chords.map(c => {
175
175
  const converted = convertChord(c, keySemitone, toNNS, preferFlats)
176
- return { ...converted, beatStart: c.beatStart, durationInBeats: c.durationInBeats }
176
+ const playbackChord = { ...converted, beatStart: c.beatStart, durationInBeats: c.durationInBeats }
177
+ if (c.markerIndex !== undefined) playbackChord.markerIndex = c.markerIndex
178
+ return playbackChord
177
179
  }),
178
180
  }))
179
181
  : undefined
package/src/playback.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * Expand a chord into PlaybackChord(s). If the chord has a splitMeasure,
3
3
  * each sub-chord becomes a separate PlaybackChord; otherwise a single one.
4
4
  */
5
- function expandChord(chord) {
5
+ function expandChord(chord, markerIndex) {
6
6
  if (chord.splitMeasure && chord.splitMeasure.length > 0) {
7
7
  return chord.splitMeasure.map(sc => {
8
8
  const pc = { root: sc.root, type: sc.type }
@@ -11,6 +11,7 @@ function expandChord(chord) {
11
11
  if (sc.diamond) pc.diamond = true
12
12
  if (sc.push) pc.push = true
13
13
  if (sc.stop) pc.stop = true
14
+ if (markerIndex !== undefined) pc.markerIndex = markerIndex
14
15
  return pc
15
16
  })
16
17
  }
@@ -20,6 +21,7 @@ function expandChord(chord) {
20
21
  if (chord.diamond) pc.diamond = true
21
22
  if (chord.push) pc.push = true
22
23
  if (chord.stop) pc.stop = true
24
+ if (markerIndex !== undefined) pc.markerIndex = markerIndex
23
25
  return [pc]
24
26
  }
25
27
 
@@ -127,16 +129,17 @@ export function buildPlaybackTimeline(structure, timeSignature) {
127
129
  markers.push({ col: chord.column, type: 'chord', chord })
128
130
  }
129
131
  for (const bar of line.barLines) {
130
- markers.push({ col: bar.column, type: 'bar' })
132
+ markers.push({ col: bar.column, type: 'bar', bar })
131
133
  }
132
134
  markers.sort((a, b) => a.col - b.col)
133
135
 
134
136
  if (shouldGroupByBarlines(markers)) {
135
137
  // Group chords by barline boundaries
136
138
  let currentChords = []
137
- for (const m of markers) {
139
+ for (let mi = 0; mi < markers.length; mi++) {
140
+ const m = markers[mi]
138
141
  if (m.type === 'chord') {
139
- currentChords.push(...expandChord(m.chord))
142
+ currentChords.push(...expandChord(m.chord, mi))
140
143
  } else {
141
144
  // Bar line: flush accumulated chords as a measure
142
145
  if (currentChords.length > 0) {
@@ -156,22 +159,36 @@ export function buildPlaybackTimeline(structure, timeSignature) {
156
159
  // No inter-barlines: each chord is its own measure.
157
160
  // Trailing barlines repeat the preceding chord for one additional measure.
158
161
  let lastExpanded = null
159
- for (const m of markers) {
162
+ for (let mi = 0; mi < markers.length; mi++) {
163
+ const m = markers[mi]
160
164
  if (m.type === 'chord') {
161
- lastExpanded = expandChord(m.chord)
165
+ lastExpanded = expandChord(m.chord, mi)
162
166
  measures.push(buildMeasure(lastExpanded, measureIndex, si, li, ts))
163
167
  measureIndex++
164
- } else if (m.type === 'bar' && lastExpanded) {
165
- measures.push(buildMeasure([...lastExpanded], measureIndex, si, li, ts))
166
- measureIndex++
168
+ } else if (m.type === 'bar') {
169
+ // Use parser-provided bar chord context when available.
170
+ // This enables leading bars on a new line (e.g. "| | G |")
171
+ // to repeat the previous line's carried chord.
172
+ let repeatedAtBar = null
173
+ if (m.bar && m.bar.chord) {
174
+ repeatedAtBar = expandChord(m.bar.chord, mi)
175
+ } else if (lastExpanded) {
176
+ repeatedAtBar = lastExpanded.map(c => ({ ...c, markerIndex: mi }))
177
+ }
178
+ if (repeatedAtBar) {
179
+ measures.push(buildMeasure(repeatedAtBar, measureIndex, si, li, ts))
180
+ measureIndex++
181
+ lastExpanded = repeatedAtBar
182
+ }
167
183
  }
168
184
  }
169
185
  }
170
186
  }
171
187
  } else {
172
188
  // Expression-only entries (FILL, INSTRUMENTAL, etc.) — chords only, no lines
173
- for (const chord of entry.chords) {
174
- const expanded = expandChord(chord)
189
+ for (let ci = 0; ci < entry.chords.length; ci++) {
190
+ const chord = entry.chords[ci]
191
+ const expanded = expandChord(chord, ci)
175
192
  measures.push(buildMeasure(expanded, measureIndex, si, -1, ts))
176
193
  measureIndex++
177
194
  }
package/src/transpose.js CHANGED
@@ -114,7 +114,9 @@ export function transpose(song, semitones, options = {}) {
114
114
  ...measure,
115
115
  chords: measure.chords.map(c => {
116
116
  const transposed = transposeChordObj(c, semitones, preferFlats)
117
- return { ...transposed, beatStart: c.beatStart, durationInBeats: c.durationInBeats }
117
+ const playbackChord = { ...transposed, beatStart: c.beatStart, durationInBeats: c.durationInBeats }
118
+ if (c.markerIndex !== undefined) playbackChord.markerIndex = c.markerIndex
119
+ return playbackChord
118
120
  }),
119
121
  }))
120
122
  : undefined
@@ -75,6 +75,18 @@ describe('buildPlaybackTimeline', () => {
75
75
  expect(playback[7].chords[0].type).toBe('')
76
76
  })
77
77
 
78
+ it('leading bars on next line repeat carried chord context', () => {
79
+ const song = parse(
80
+ 'TITLE\n(3/4 time)\n\nC | F C\nA way out on-line\n | | G |\nA thousand miles from what I know as mine'
81
+ )
82
+ const playback = song.playback
83
+ const roots = playback.map(m => m.chords[0].root + (m.chords[0].bass ? '/' + m.chords[0].bass : ''))
84
+ expect(roots).toEqual(['C', 'C', 'F', 'C', 'C', 'C', 'G', 'G'])
85
+ for (const m of playback) {
86
+ expect(m.chords[0].durationInBeats).toBe(3)
87
+ }
88
+ })
89
+
78
90
  it('D | creates 2 measures: D then barline repeats D', () => {
79
91
  const song = parse('TITLE\n\nD |\n Lyrics')
80
92
  const playback = song.playback
@@ -171,6 +183,20 @@ describe('buildPlaybackTimeline', () => {
171
183
  expect(song.playback[0].measureIndex).toBe(0)
172
184
  expect(song.playback[1].measureIndex).toBe(1)
173
185
  })
186
+
187
+ it('assigns playback markerIndex values for chords and bar repeats', () => {
188
+ const song = parse('TITLE\n\nC D |\n Lyrics')
189
+ expect(song.playback[0].chords[0].markerIndex).toBe(0) // C marker
190
+ expect(song.playback[1].chords[0].markerIndex).toBe(1) // D marker
191
+ expect(song.playback[2].chords[0].markerIndex).toBe(2) // trailing bar marker
192
+ })
193
+
194
+ it('assigns markerIndex for grouped barline measures', () => {
195
+ const song = parse('TITLE\n\n| C D | G |\n Lyrics')
196
+ expect(song.playback[0].chords[0].markerIndex).toBe(1) // C marker
197
+ expect(song.playback[0].chords[1].markerIndex).toBe(2) // D marker
198
+ expect(song.playback[1].chords[0].markerIndex).toBe(4) // G marker
199
+ })
174
200
  })
175
201
 
176
202
  describe('fixture: spent-some-time-in-buffalo.txt', () => {