|
27 | 27 | let tremolo |
28 | 28 | let compressor |
29 | 29 | let started = false |
30 | | - let loop = false |
| 30 | + let loop = true |
31 | 31 | let part |
32 | 32 | let isTouch = false |
| 33 | + let currentBeat = -1 |
33 | 34 |
|
34 | 35 | // Clicking a chord selects it (updates diagrams) without triggering audio |
35 | 36 | function selectChord(chord) { |
|
141 | 142 | progression = [...progression.slice(0, i), item, ...progression.slice(i)] |
142 | 143 | } |
143 | 144 |
|
144 | | - async function playProgression() { |
145 | | - await ensureAudio() |
146 | | - Tone.Transport.stop() |
| 145 | + // Reactive statement to update the sequence whenever the progression changes |
| 146 | + $: if (progression && started) { |
| 147 | + if (Tone.Transport.state === 'started') { |
| 148 | + updateSequence(true) // restart playback |
| 149 | + } |
| 150 | + } |
| 151 | +
|
| 152 | + // This function builds or rebuilds the Tone.js Part from the progression |
| 153 | + function updateSequence(restart = false) { |
| 154 | + const wasRunning = Tone.Transport.state === 'started' |
| 155 | + if (wasRunning) Tone.Transport.stop() |
| 156 | +
|
147 | 157 | Tone.Transport.cancel(0) |
148 | | - if (part) { part.dispose(); part = null } |
| 158 | + if (part) { |
| 159 | + part.dispose() |
| 160 | + part = null |
| 161 | + } |
| 162 | + |
| 163 | + if (progression.length === 0) { |
| 164 | + currentBeat = -1 |
| 165 | + return |
| 166 | + } |
149 | 167 |
|
150 | 168 | const quarter = Tone.Time('4n').toSeconds() |
151 | 169 | const events = [] |
152 | 170 | let acc = 0 |
153 | | - for (let i = 0; i < progression.length; i++) { |
154 | | - const item = progression[i] |
| 171 | + progression.forEach((item, i) => { |
155 | 172 | const notes = item?.chord ? chordNotes(item.chord) : [] |
156 | | - const strums = (item?.beats || 4) |
157 | | - for (let s = 0; s < strums; s++) { |
| 173 | + const beats = item?.beats || 4 |
| 174 | + item.startBeat = Math.floor(acc / quarter) |
| 175 | + for (let s = 0; s < beats; s++) { |
158 | 176 | const dir = 'down' |
159 | | - events.push({ time: acc + s * quarter, notes, dir }) |
| 177 | + events.push({ time: acc + s * quarter, notes, dir, beat: item.startBeat + s, chord: item.chord }) |
160 | 178 | } |
161 | | - acc += (item?.beats || 4) * quarter |
162 | | - } |
163 | | -
|
164 | | - if (events.length === 0) return |
| 179 | + acc += beats * quarter |
| 180 | + }) |
165 | 181 |
|
166 | 182 | part = new Tone.Part((time, ev) => { |
| 183 | + currentBeat = ev.beat |
| 184 | + if (currentChord !== ev.chord) { |
| 185 | + currentChord = ev.chord |
| 186 | + } |
167 | 187 | if (ev.notes && ev.notes.length) { |
168 | 188 | strumChord(ev.notes, time, ev.dir) |
169 | 189 | } |
170 | 190 | }, events).start(0) |
171 | 191 | part.loop = loop |
172 | 192 | part.loopEnd = acc |
173 | 193 |
|
174 | | - Tone.Transport.start('+0.05') |
| 194 | + if (restart || wasRunning) { |
| 195 | + Tone.Transport.start('+0.05') |
| 196 | + } |
| 197 | + } |
| 198 | +
|
| 199 | + async function playProgression() { |
| 200 | + await ensureAudio() |
| 201 | + currentBeat = 0 |
| 202 | + updateSequence(true) |
| 203 | + Tone.Transport.on('stop', () => { currentBeat = -1 }) |
175 | 204 | } |
176 | 205 |
|
177 | 206 | function stopProgression() { |
178 | 207 | Tone.Transport.stop() |
179 | 208 | Tone.Transport.cancel(0) |
180 | 209 | if (part) { part.dispose(); part = null } |
| 210 | + currentBeat = -1 |
181 | 211 | } |
182 | 212 | </script> |
183 | 213 |
|
|
205 | 235 | <ChordButton name={c} {isTouch} onPreview={() => preview(c)} onAdd={() => addToProgression(c)} onSelect={() => selectChord(c)} /> |
206 | 236 | {/each} |
207 | 237 | <!-- Rest tile (draggable) --> |
208 | | - <ChordButton name="Rest" {isTouch} dragPayload="REST" onAdd={addRestToProgression} /> |
| 238 | + <ChordButton name="Rest" {isTouch} dragPayload="REST" onAdd={addRestToProgression} onSelect={() => selectChord('Rest')} /> |
209 | 239 | </div> |
210 | 240 | </div> |
211 | 241 |
|
|
220 | 250 | <div class="grid grid-cols-2 gap-3"> |
221 | 251 | <div class="space-y-2"> |
222 | 252 | <div class="text-xs uppercase tracking-wide text-slate-400">Guitar</div> |
223 | | - <ChordDiagram instrument="guitar" shape={guitarShapes[currentChord]} /> |
| 253 | + <ChordDiagram instrument="guitar" shape={{...guitarShapes[currentChord || 'Rest'], chord: currentChord || 'Rest'}} clickable={true} on:click={() => preview(currentChord)} /> |
224 | 254 | </div> |
225 | 255 | <div class="space-y-2"> |
226 | 256 | <div class="text-xs uppercase tracking-wide text-slate-400">Ukulele</div> |
227 | | - <ChordDiagram instrument="uke" shape={ukeShapes[currentChord]} /> |
| 257 | + <ChordDiagram instrument="uke" shape={{...ukeShapes[currentChord || 'Rest'], chord: currentChord || 'Rest'}} clickable={true} on:click={() => preview(currentChord)} /> |
228 | 258 | </div> |
229 | 259 | </div> |
230 | 260 | {/if} |
|
233 | 263 |
|
234 | 264 | <section class="space-y-4"> |
235 | 265 | <h2 class="font-semibold text-slate-200">Progression</h2> |
236 | | - <ProgressionBar {progression} {isTouch} onRemove={removeFromProgression} onUpdateBeat={updateBeats} onReorder={reorderProgression} onInsert={insertAt} /> |
| 266 | + <ProgressionBar {progression} {currentBeat} {isTouch} onRemove={removeFromProgression} onUpdateBeat={updateBeats} onReorder={reorderProgression} onInsert={insertAt} /> |
237 | 267 | <div class="flex flex-wrap items-center gap-3"> |
238 | 268 | <button class="px-4 py-2 rounded-lg bg-cyan-500 hover:bg-cyan-400 text-slate-900 font-semibold" |
239 | 269 | on:click={playProgression} |
|
0 commit comments