Add progress bar and seek controls to Motif player

- Add interactive progress bar with visual fill indicator
- Show current time and total duration
- Enable seeking/scrubbing through the synthesis
- Improve Motif player UX with cleaner layout
- Add progress tracking that updates 10x per second
- Format time display as MM:SS
- Progress bar appears when playback starts
- Seeking updates event indices for smooth playback

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
b1rdmania
2025-12-18 19:17:02 +00:00
parent 96477e9c4a
commit 3031614914
4 changed files with 224 additions and 3 deletions
+27
View File
@@ -134,6 +134,33 @@ export class MotifEngine {
}
}
seek(progress: number): void {
if (this.synthesisEngine) {
this.synthesisEngine.seek(progress);
}
}
getProgress(): number {
if (this.synthesisEngine) {
return this.synthesisEngine.getProgress();
}
return 0;
}
getCurrentTime(): number {
if (this.synthesisEngine) {
return this.synthesisEngine.getCurrentTime();
}
return 0;
}
getDuration(): number {
if (this.synthesisEngine) {
return this.synthesisEngine.getDuration();
}
return 0;
}
private generateSyntheticMIDI(songName: string): NoteEvent[] {
// Generate procedural MIDI based on song name hash
const hash = this.simpleHash(songName);
+62 -1
View File
@@ -46,9 +46,15 @@ class MotifApp {
private motifBtn!: HTMLButtonElement;
private motifStopBtn!: HTMLButtonElement;
private motifVolumeSlider!: HTMLInputElement;
private motifProgressContainer!: HTMLElement;
private motifProgressBar!: HTMLInputElement;
private motifProgressFill!: HTMLElement;
private motifCurrentTime!: HTMLElement;
private motifDuration!: HTMLElement;
private motifProgressInterval: number | null = null;
private nextResultBtn!: HTMLButtonElement;
private searchResults: any[] = [];
private selectedResultIndex = 0;
private currentMIDI: { events: NoteEvent[], metadata: any } | null = null;
@@ -99,6 +105,11 @@ class MotifApp {
this.motifBtn = document.getElementById('motifBtn') as HTMLButtonElement;
this.motifStopBtn = document.getElementById('motifStopBtn') as HTMLButtonElement;
this.motifVolumeSlider = document.getElementById('motifVolume') as HTMLInputElement;
this.motifProgressContainer = document.getElementById('motifProgressContainer')!;
this.motifProgressBar = document.getElementById('motifProgressBar') as HTMLInputElement;
this.motifProgressFill = document.getElementById('motifProgressFill')!;
this.motifCurrentTime = document.getElementById('motifCurrentTime')!;
this.motifDuration = document.getElementById('motifDuration')!;
this.nextResultBtn = document.getElementById('nextResultBtn') as HTMLButtonElement;
}
@@ -143,6 +154,10 @@ class MotifApp {
const volume = parseFloat((e.target as HTMLInputElement).value);
this.motifEngine.setVolume(volume);
});
this.motifProgressBar.addEventListener('input', (e) => {
const progress = parseFloat((e.target as HTMLInputElement).value) / 100;
this.handleMotifSeek(progress);
});
this.nextResultBtn.addEventListener('click', () => this.handleNextResult());
}
@@ -361,6 +376,17 @@ class MotifApp {
await this.motifEngine.play();
this.motifStopBtn.disabled = false;
// Show progress bar and set duration
this.motifProgressContainer.style.display = 'block';
const duration = this.motifEngine.getDuration();
this.motifDuration.textContent = this.formatTime(duration);
this.motifProgressBar.value = '0';
this.motifProgressFill.style.width = '0%';
// Start progress updates
this.startMotifProgressUpdates();
this.updateStatus('Playing Motif synthesis...');
console.log('Motif playback started successfully');
} catch (error) {
@@ -374,9 +400,44 @@ class MotifApp {
this.motifEngine.stop();
this.motifBtn.disabled = false;
this.motifStopBtn.disabled = true;
this.stopMotifProgressUpdates();
this.updateStatus('Motif synthesis stopped.');
}
private handleMotifSeek(progress: number): void {
this.motifEngine.seek(progress);
this.updateMotifProgress();
}
private startMotifProgressUpdates(): void {
this.stopMotifProgressUpdates();
this.motifProgressInterval = window.setInterval(() => {
this.updateMotifProgress();
}, 100); // Update 10 times per second
}
private stopMotifProgressUpdates(): void {
if (this.motifProgressInterval !== null) {
clearInterval(this.motifProgressInterval);
this.motifProgressInterval = null;
}
}
private updateMotifProgress(): void {
const progress = this.motifEngine.getProgress();
const currentTime = this.motifEngine.getCurrentTime();
this.motifProgressBar.value = (progress * 100).toString();
this.motifProgressFill.style.width = `${progress * 100}%`;
this.motifCurrentTime.textContent = this.formatTime(currentTime);
}
private formatTime(seconds: number): string {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
}
private handleNextResult(): void {
const nextIndex = (this.selectedResultIndex + 1) % this.searchResults.length;
this.selectResult(nextIndex);
+59
View File
@@ -71,6 +71,65 @@ export class SynthesisEngine {
this.masterGain.gain.value = Math.max(0, Math.min(1, volume));
}
seek(progress: number): void {
if (!this.isPlaying) return;
// Calculate the new start time based on progress
const duration = this.getDuration();
const targetTime = progress * duration;
// Adjust startTime to effectively seek to the target position
this.startTime = this.audioContext.currentTime - targetTime;
// Reset event indices to the appropriate position
for (const [role, assignment] of this.roleAssignments) {
const events = assignment.events;
if (events.length > 0) {
// Find the first event after the target time
let index = 0;
while (index < events.length && events[index].time < targetTime) {
index++;
}
this.nextEventIndex.set(role, index);
}
}
}
getProgress(): number {
if (!this.isPlaying) return 0;
const duration = this.getDuration();
if (duration === 0) return 0;
const currentTime = this.audioContext.currentTime - this.startTime;
return Math.max(0, Math.min(1, currentTime / duration));
}
getCurrentTime(): number {
if (!this.isPlaying) return 0;
return Math.max(0, this.audioContext.currentTime - this.startTime);
}
getDuration(): number {
let maxDuration = 0;
for (const assignment of this.roleAssignments.values()) {
if (assignment.events.length > 0) {
const lastEvent = assignment.events[assignment.events.length - 1];
const eventEnd = lastEvent.time + lastEvent.duration;
maxDuration = Math.max(maxDuration, eventEnd);
}
if (assignment.chords.length > 0) {
const lastChord = assignment.chords[assignment.chords.length - 1];
const chordEnd = lastChord.time + lastChord.duration;
maxDuration = Math.max(maxDuration, chordEnd);
}
}
return maxDuration;
}
private createSynthLayer(role: Role): SynthLayer {
const gainNode = this.audioContext.createGain();
const filterNode = this.audioContext.createBiquadFilter();