diff --git a/index.html b/index.html
index 79eef0b..14bedab 100644
--- a/index.html
+++ b/index.html
@@ -284,6 +284,69 @@
margin: 0;
}
+ .motif-progress-container {
+ margin-top: 20px;
+ padding-top: 20px;
+ border-top: 1px solid rgba(255, 170, 68, 0.15);
+ }
+ .progress-time {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 8px;
+ font-size: 0.85em;
+ color: #8f8f8f;
+ }
+ .progress-bar-wrapper {
+ position: relative;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ }
+ .progress-bar {
+ width: 100%;
+ height: 8px;
+ -webkit-appearance: none;
+ appearance: none;
+ background: rgba(255, 255, 255, 0.1);
+ border-radius: 4px;
+ outline: none;
+ cursor: pointer;
+ position: relative;
+ z-index: 2;
+ }
+ .progress-bar::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ appearance: none;
+ width: 16px;
+ height: 16px;
+ border-radius: 50%;
+ background: #ffaa44;
+ cursor: pointer;
+ box-shadow: 0 0 10px rgba(255, 170, 68, 0.5);
+ }
+ .progress-bar::-moz-range-thumb {
+ width: 16px;
+ height: 16px;
+ border-radius: 50%;
+ background: #ffaa44;
+ cursor: pointer;
+ border: none;
+ box-shadow: 0 0 10px rgba(255, 170, 68, 0.5);
+ }
+ .progress-fill {
+ position: absolute;
+ left: 0;
+ top: 50%;
+ transform: translateY(-50%);
+ height: 8px;
+ background: linear-gradient(90deg, #ffaa44, rgba(255, 170, 68, 0.6));
+ border-radius: 4px;
+ pointer-events: none;
+ z-index: 1;
+ width: 0%;
+ transition: width 0.1s linear;
+ }
+
.engine-selector {
margin-top: 10px;
font-size: 0.9em;
@@ -396,8 +459,8 @@
-
Generate a variation
-
Same MIDI, new energy. This is where “more ambient / more dancey / more ominous” will live.
+
Procedural Synthesis
+
Web Audio oscillators with dynamic filtering and envelopes.
@@ -408,6 +471,17 @@
+
+
diff --git a/src/core/MotifEngine.ts b/src/core/MotifEngine.ts
index f953ff7..8c87e3f 100644
--- a/src/core/MotifEngine.ts
+++ b/src/core/MotifEngine.ts
@@ -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);
diff --git a/src/main.ts b/src/main.ts
index 591914f..88dd780 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -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);
diff --git a/src/synthesis/SynthesisEngine.ts b/src/synthesis/SynthesisEngine.ts
index d2f5c0f..91d7a4e 100644
--- a/src/synthesis/SynthesisEngine.ts
+++ b/src/synthesis/SynthesisEngine.ts
@@ -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();