songsheet 7.1.0 → 7.2.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.1.0",
3
+ "version": "7.2.0",
4
4
  "description": "A songsheet interpreter",
5
5
  "type": "module",
6
6
  "exports": "./index.js",
package/src/playback.js CHANGED
@@ -63,6 +63,43 @@ function detectInterBarlines(markers) {
63
63
  return false
64
64
  }
65
65
 
66
+ /**
67
+ * Detect consecutive barlines (e.g., C || D), which usually indicate
68
+ * repeated held measures rather than explicit bar-delimited grouping.
69
+ */
70
+ function hasConsecutiveBars(markers) {
71
+ let prevWasBar = false
72
+ for (const m of markers) {
73
+ if (m.type === 'bar') {
74
+ if (prevWasBar) return true
75
+ prevWasBar = true
76
+ } else {
77
+ prevWasBar = false
78
+ }
79
+ }
80
+ return false
81
+ }
82
+
83
+ /**
84
+ * Decide whether a line should be grouped by explicit barline boundaries.
85
+ *
86
+ * Heuristic:
87
+ * - require at least one barline between chords
88
+ * - require barline density to be at least chord density
89
+ * - reject consecutive barlines (treated as hold repeats)
90
+ *
91
+ * Sparse barlines like "C C/B Am G F | Fsus4 F" stay in chord-per-measure mode.
92
+ */
93
+ function shouldGroupByBarlines(markers) {
94
+ const chordCount = markers.filter(m => m.type === 'chord').length
95
+ const barCount = markers.length - chordCount
96
+ if (chordCount === 0 || barCount === 0) return false
97
+ if (!detectInterBarlines(markers)) return false
98
+ if (barCount < chordCount) return false
99
+ if (hasConsecutiveBars(markers)) return false
100
+ return true
101
+ }
102
+
66
103
  /**
67
104
  * Build the playback timeline from the song's structure array.
68
105
  *
@@ -94,12 +131,7 @@ export function buildPlaybackTimeline(structure, timeSignature) {
94
131
  }
95
132
  markers.sort((a, b) => a.col - b.col)
96
133
 
97
- // Detect whether barlines actually separate chords on this line.
98
- // Pattern: chord...bar...chord means barlines are measure separators.
99
- // Pattern: chord chord... bar (trailing only) means barlines are phrase markers.
100
- const hasInterBarlines = detectInterBarlines(markers)
101
-
102
- if (hasInterBarlines) {
134
+ if (shouldGroupByBarlines(markers)) {
103
135
  // Group chords by barline boundaries
104
136
  let currentChords = []
105
137
  for (const m of markers) {
@@ -61,6 +61,20 @@ describe('buildPlaybackTimeline', () => {
61
61
  expect(playback[1].chords[0].root).toBe('G')
62
62
  })
63
63
 
64
+ it('sparse inter-barline usage stays chord-per-measure with bar repeat', () => {
65
+ const song = parse('TITLE\n(3/4 time)\n\nC C/B Am G F | Fsus4 F\n Lyrics')
66
+ const playback = song.playback
67
+ expect(playback.length).toBe(8)
68
+ const roots = playback.map(m => m.chords[0].root + (m.chords[0].bass ? '/' + m.chords[0].bass : ''))
69
+ expect(roots).toEqual(['C', 'C/B', 'A', 'G', 'F', 'F', 'F', 'F'])
70
+ for (const m of playback) {
71
+ expect(m.chords.length).toBe(1)
72
+ expect(m.chords[0].durationInBeats).toBe(3)
73
+ }
74
+ expect(playback[6].chords[0].type).toBe('sus4')
75
+ expect(playback[7].chords[0].type).toBe('')
76
+ })
77
+
64
78
  it('D | creates 2 measures: D then barline repeats D', () => {
65
79
  const song = parse('TITLE\n\nD |\n Lyrics')
66
80
  const playback = song.playback