songsheet 7.0.0 → 7.1.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 +1 -1
- package/src/playback.js +36 -7
- package/test/playback.test.js +42 -26
package/package.json
CHANGED
package/src/playback.js
CHANGED
|
@@ -44,6 +44,25 @@ function buildMeasure(chords, measureIndex, structureIndex, lineIndex, ts) {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Check if barlines on this line actually separate chords (e.g., | G | C | D |)
|
|
49
|
+
* vs. just trailing after all chords (e.g., A C D |).
|
|
50
|
+
* Returns true only when a barline appears between two chords.
|
|
51
|
+
*/
|
|
52
|
+
function detectInterBarlines(markers) {
|
|
53
|
+
let seenChord = false
|
|
54
|
+
let seenBarAfterChord = false
|
|
55
|
+
for (const m of markers) {
|
|
56
|
+
if (m.type === 'chord') {
|
|
57
|
+
if (seenBarAfterChord) return true
|
|
58
|
+
seenChord = true
|
|
59
|
+
} else if (m.type === 'bar' && seenChord) {
|
|
60
|
+
seenBarAfterChord = true
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return false
|
|
64
|
+
}
|
|
65
|
+
|
|
47
66
|
/**
|
|
48
67
|
* Build the playback timeline from the song's structure array.
|
|
49
68
|
*
|
|
@@ -75,9 +94,12 @@ export function buildPlaybackTimeline(structure, timeSignature) {
|
|
|
75
94
|
}
|
|
76
95
|
markers.sort((a, b) => a.col - b.col)
|
|
77
96
|
|
|
78
|
-
|
|
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)
|
|
79
101
|
|
|
80
|
-
if (
|
|
102
|
+
if (hasInterBarlines) {
|
|
81
103
|
// Group chords by barline boundaries
|
|
82
104
|
let currentChords = []
|
|
83
105
|
for (const m of markers) {
|
|
@@ -99,11 +121,18 @@ export function buildPlaybackTimeline(structure, timeSignature) {
|
|
|
99
121
|
measureIndex++
|
|
100
122
|
}
|
|
101
123
|
} else {
|
|
102
|
-
// No barlines: each chord is its own measure
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
124
|
+
// No inter-barlines: each chord is its own measure.
|
|
125
|
+
// Trailing barlines repeat the preceding chord for one additional measure.
|
|
126
|
+
let lastExpanded = null
|
|
127
|
+
for (const m of markers) {
|
|
128
|
+
if (m.type === 'chord') {
|
|
129
|
+
lastExpanded = expandChord(m.chord)
|
|
130
|
+
measures.push(buildMeasure(lastExpanded, measureIndex, si, li, ts))
|
|
131
|
+
measureIndex++
|
|
132
|
+
} else if (m.type === 'bar' && lastExpanded) {
|
|
133
|
+
measures.push(buildMeasure([...lastExpanded], measureIndex, si, li, ts))
|
|
134
|
+
measureIndex++
|
|
135
|
+
}
|
|
107
136
|
}
|
|
108
137
|
}
|
|
109
138
|
}
|
package/test/playback.test.js
CHANGED
|
@@ -37,10 +37,20 @@ describe('buildPlaybackTimeline', () => {
|
|
|
37
37
|
}
|
|
38
38
|
})
|
|
39
39
|
|
|
40
|
-
it('
|
|
41
|
-
const song = parse('TITLE\n\
|
|
40
|
+
it('trailing barline repeats last chord: C D | → C, D, D', () => {
|
|
41
|
+
const song = parse('TITLE\n\nC D |\n Lyrics')
|
|
42
42
|
const playback = song.playback
|
|
43
|
-
expect(playback.length).toBe(
|
|
43
|
+
expect(playback.length).toBe(3)
|
|
44
|
+
expect(playback[0].chords[0].root).toBe('C')
|
|
45
|
+
expect(playback[1].chords[0].root).toBe('D')
|
|
46
|
+
expect(playback[2].chords[0].root).toBe('D')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('| C D | G | groups by barlines when inter-barlines present', () => {
|
|
50
|
+
const song = parse('TITLE\n\n| C D | G |\n Lyrics')
|
|
51
|
+
const playback = song.playback
|
|
52
|
+
// Barline between D and G → barline grouping applies
|
|
53
|
+
expect(playback.length).toBe(2)
|
|
44
54
|
expect(playback[0].chords.length).toBe(2)
|
|
45
55
|
expect(playback[0].chords[0].root).toBe('C')
|
|
46
56
|
expect(playback[0].chords[0].beatStart).toBe(0)
|
|
@@ -48,22 +58,24 @@ describe('buildPlaybackTimeline', () => {
|
|
|
48
58
|
expect(playback[0].chords[1].root).toBe('D')
|
|
49
59
|
expect(playback[0].chords[1].beatStart).toBe(2)
|
|
50
60
|
expect(playback[0].chords[1].durationInBeats).toBe(2)
|
|
61
|
+
expect(playback[1].chords[0].root).toBe('G')
|
|
51
62
|
})
|
|
52
63
|
|
|
53
|
-
it('D | creates
|
|
64
|
+
it('D | creates 2 measures: D then barline repeats D', () => {
|
|
54
65
|
const song = parse('TITLE\n\nD |\n Lyrics')
|
|
55
66
|
const playback = song.playback
|
|
56
|
-
expect(playback.length).toBe(
|
|
57
|
-
expect(playback[0].chords.length).toBe(1)
|
|
67
|
+
expect(playback.length).toBe(2)
|
|
58
68
|
expect(playback[0].chords[0].root).toBe('D')
|
|
59
|
-
expect(playback[
|
|
69
|
+
expect(playback[1].chords[0].root).toBe('D')
|
|
60
70
|
})
|
|
61
71
|
|
|
62
|
-
it('
|
|
72
|
+
it('| G | with no inter-barlines: G then barline repeats G', () => {
|
|
63
73
|
const song = parse('TITLE\n\n| G |\n Lyrics')
|
|
64
74
|
const playback = song.playback
|
|
65
|
-
|
|
75
|
+
// Leading | has no preceding chord → ignored. G then trailing | repeats G.
|
|
76
|
+
expect(playback.length).toBe(2)
|
|
66
77
|
expect(playback[0].chords[0].root).toBe('G')
|
|
78
|
+
expect(playback[1].chords[0].root).toBe('G')
|
|
67
79
|
})
|
|
68
80
|
})
|
|
69
81
|
|
|
@@ -111,16 +123,24 @@ describe('buildPlaybackTimeline', () => {
|
|
|
111
123
|
})
|
|
112
124
|
|
|
113
125
|
describe('3/4 time signature', () => {
|
|
114
|
-
it('
|
|
115
|
-
const song = parse('TITLE\n(3/4 time)\n\n
|
|
126
|
+
it('uses 3 beats per measure', () => {
|
|
127
|
+
const song = parse('TITLE\n(3/4 time)\n\nG\n Lyrics')
|
|
116
128
|
const playback = song.playback
|
|
117
129
|
expect(playback.length).toBe(1)
|
|
118
|
-
expect(playback[0].chords.
|
|
119
|
-
expect(playback[0].chords[0].durationInBeats).toBe(1)
|
|
120
|
-
expect(playback[0].chords[1].durationInBeats).toBe(1)
|
|
121
|
-
expect(playback[0].chords[2].durationInBeats).toBe(1)
|
|
130
|
+
expect(playback[0].chords[0].durationInBeats).toBe(3)
|
|
122
131
|
expect(playback[0].timeSignature.beats).toBe(3)
|
|
123
132
|
})
|
|
133
|
+
|
|
134
|
+
it('inter-barline grouping divides beats by 3', () => {
|
|
135
|
+
const song = parse('TITLE\n(3/4 time)\n\n| G | C D |\n Lyrics')
|
|
136
|
+
const playback = song.playback
|
|
137
|
+
expect(playback.length).toBe(2)
|
|
138
|
+
expect(playback[0].chords[0].root).toBe('G')
|
|
139
|
+
expect(playback[0].chords[0].durationInBeats).toBe(3)
|
|
140
|
+
expect(playback[1].chords.length).toBe(2)
|
|
141
|
+
expect(playback[1].chords[0].durationInBeats).toBe(1.5)
|
|
142
|
+
expect(playback[1].chords[1].durationInBeats).toBe(1.5)
|
|
143
|
+
})
|
|
124
144
|
})
|
|
125
145
|
|
|
126
146
|
describe('back-references', () => {
|
|
@@ -153,18 +173,14 @@ describe('buildPlaybackTimeline', () => {
|
|
|
153
173
|
}
|
|
154
174
|
})
|
|
155
175
|
|
|
156
|
-
it('
|
|
157
|
-
// The verse has patterns like "D |" — D followed by barline = 1 measure
|
|
158
|
-
// Check that the first structure entry (verse) doesn't duplicate chords at barlines
|
|
176
|
+
it('trailing barlines repeat last chord: D|, G|, A C D| → D D G G A C D D', () => {
|
|
159
177
|
const verseMeasures = song.playback.filter(m => m.structureIndex === 0)
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
expect(
|
|
166
|
-
expect(verseMeasures[1].chords[0].root).toBe('G')
|
|
167
|
-
expect(verseMeasures[2].chords.length).toBe(3)
|
|
178
|
+
// Line 1: "D |" → D, D (barline repeats)
|
|
179
|
+
// Line 2: "G |" → G, G (barline repeats)
|
|
180
|
+
// Line 3: "A C D |" → A, C, D, D (barline repeats last)
|
|
181
|
+
expect(verseMeasures.length).toBe(8)
|
|
182
|
+
const roots = verseMeasures.map(m => m.chords[0].root)
|
|
183
|
+
expect(roots).toEqual(['D', 'D', 'G', 'G', 'A', 'C', 'D', 'D'])
|
|
168
184
|
})
|
|
169
185
|
|
|
170
186
|
it('measure indices are sequential', () => {
|