+
+ ${titleMain}
+ ${titleSub}
+ ${titleStatus}
+
+
+
+
+
--
+
Processing Time↓
+
+
+
+
+
+
+ `;
+ };
+ const supertonicInitial = createInitialResultItem(
+ 'supertonic',
+ 'Supertonic',
+ 'On-Device',
+ 'var(--accent-yellow)',
+ false
+ );
+ demoResults.style.display = 'flex';
+ demoResults.innerHTML = supertonicInitial;
+
+ const totalStep = parseInt(demoTotalSteps.value);
+ const speed = parseFloat(demoSpeed.value);
+ const durationFactor = speedToDurationFactor(speed);
+
+ // Track which one finishes first
+ let latestSupertonicProcessedChars = 0;
+
+ // Helper functions for custom player
+ const formatTime = (seconds, { trimMobile = false } = {}) => {
+ const mins = Math.floor(seconds / 60);
+ const secs = seconds % 60;
+ const secString = secs.toFixed(2).padStart(5, '0');
+ let formatted = `${mins}:${secString}`;
+ if (trimMobile) {
+ formatted = trimDecimalsForMobile(formatted);
+ }
+ return formatted;
+ };
+
+ const updateProgress = () => {
+ if (!isPlaying || !audioContext) return;
+
+ const currentTime = isPaused ? pauseTime : (audioContext.currentTime - startTime);
+ const progress = totalDuration > 0 ? (currentTime / totalDuration) * 100 : 0;
+
+ if (progressFill) {
+ progressFill.style.width = `${Math.min(progress, 100)}%`;
+ }
+ if (currentTimeDisplay) {
+ currentTimeDisplay.textContent = formatTime(Math.min(currentTime, totalDuration), { trimMobile: true });
+ }
+
+ if (currentTime < totalDuration) {
+ animationFrameId = requestAnimationFrame(updateProgress);
+ } else {
+ // Playback finished
+ isPlaying = false;
+ isPaused = false;
+ if (playPauseBtn) {
+ playPauseBtn.innerHTML = PLAY_ICON_SVG;
+ }
+ }
+ };
+
+ const togglePlayPause = () => {
+ if (!audioContext || audioChunks.length === 0) return;
+
+ if (isPaused) {
+ // Resume from paused position
+ pauseAllPlayersExcept(supertonicPlayerRecord);
+
+ const seekTime = pauseTime;
+
+ // Find which chunk we should start from
+ let accumulatedTime = 0;
+ let startChunkIndex = 0;
+ let offsetInChunk = seekTime;
+
+ for (let i = 0; i < audioChunks.length; i++) {
+ const chunkDuration = audioChunks[i].buffer.duration;
+ if (accumulatedTime + chunkDuration > seekTime) {
+ startChunkIndex = i;
+ offsetInChunk = seekTime - accumulatedTime;
+ break;
+ }
+ accumulatedTime += chunkDuration + 0.3;
+ }
+
+ // Stop any existing sources
+ scheduledSources.forEach(source => {
+ try {
+ source.stop();
+ } catch (e) {
+ // Already stopped
+ }
+ });
+ scheduledSources = [];
+
+ // Resume AudioContext if suspended
+ if (audioContext.state === 'suspended') {
+ audioContext.resume();
+ }
+
+ // Reschedule from the pause point
+ startTime = audioContext.currentTime - seekTime;
+ let nextStartTime = audioContext.currentTime;
+
+ for (let i = startChunkIndex; i < audioChunks.length; i++) {
+ const source = audioContext.createBufferSource();
+ source.buffer = audioChunks[i].buffer;
+ source.connect(audioContext.destination);
+
+ if (i === startChunkIndex) {
+ source.start(nextStartTime, offsetInChunk);
+ nextStartTime += (audioChunks[i].buffer.duration - offsetInChunk);
+ } else {
+ source.start(nextStartTime);
+ nextStartTime += audioChunks[i].buffer.duration;
+ }
+
+ if (i < audioChunks.length - 1) {
+ nextStartTime += 0.3;
+ }
+
+ scheduledSources.push(source);
+ }
+
+ nextScheduledTime = nextStartTime;
+
+ isPaused = false;
+ isPlaying = true;
+ playPauseBtn.innerHTML = PAUSE_ICON_SVG;
+ updateProgress();
+ } else if (isPlaying) {
+ // Pause playback
+ pauseTime = audioContext.currentTime - startTime;
+ audioContext.suspend();
+ isPaused = true;
+ playPauseBtn.innerHTML = PLAY_ICON_SVG;
+ if (animationFrameId) {
+ cancelAnimationFrame(animationFrameId);
+ }
+ } else {
+ // Was finished, restart from beginning
+ pauseAllPlayersExcept(supertonicPlayerRecord);
+
+ pauseTime = 0;
+
+ // Resume AudioContext if suspended
+ if (audioContext.state === 'suspended') {
+ audioContext.resume();
+ }
+
+ // Stop any existing sources
+ scheduledSources.forEach(source => {
+ try {
+ source.stop();
+ } catch (e) {
+ // Already stopped
+ }
+ });
+ scheduledSources = [];
+
+ // Restart from beginning
+ startTime = audioContext.currentTime;
+ let nextStartTime = audioContext.currentTime;
+
+ for (let i = 0; i < audioChunks.length; i++) {
+ const source = audioContext.createBufferSource();
+ source.buffer = audioChunks[i].buffer;
+ source.connect(audioContext.destination);
+ source.start(nextStartTime);
+ nextStartTime += audioChunks[i].buffer.duration;
+
+ if (i < audioChunks.length - 1) {
+ nextStartTime += 0.3;
+ }
+
+ scheduledSources.push(source);
+ }
+
+ nextScheduledTime = nextStartTime;
+
+ isPlaying = true;
+ isPaused = false;
+ playPauseBtn.innerHTML = PAUSE_ICON_SVG;
+ updateProgress();
+ }
+ };
+
+ const seekTo = (percentage) => {
+ if (!audioContext || audioChunks.length === 0) return;
+
+ const seekTime = (percentage / 100) * totalDuration;
+
+ // Remember current playing state
+ const wasPlaying = isPlaying;
+ const wasPaused = isPaused;
+
+ // Stop all current sources
+ scheduledSources.forEach(source => {
+ try {
+ source.stop();
+ } catch (e) {
+ // Already stopped
+ }
+ });
+ scheduledSources = [];
+
+ // Cancel animation
+ if (animationFrameId) {
+ cancelAnimationFrame(animationFrameId);
+ }
+
+ // Find which chunk we should start from
+ let accumulatedTime = 0;
+ let startChunkIndex = 0;
+ let offsetInChunk = seekTime;
+
+ for (let i = 0; i < audioChunks.length; i++) {
+ const chunkDuration = audioChunks[i].buffer.duration;
+ if (accumulatedTime + chunkDuration > seekTime) {
+ startChunkIndex = i;
+ offsetInChunk = seekTime - accumulatedTime;
+ break;
+ }
+ accumulatedTime += chunkDuration + 0.3; // Include silence
+ }
+
+ // If paused or finished, just update the pause position
+ if (wasPaused || !wasPlaying) {
+ pauseTime = seekTime;
+
+ // Update UI
+ if (progressFill) {
+ const progress = (seekTime / totalDuration) * 100;
+ progressFill.style.width = `${Math.min(progress, 100)}%`;
+ }
+ if (currentTimeDisplay) {
+ currentTimeDisplay.textContent = formatTime(seekTime, { trimMobile: true });
+ }
+
+ // Set to paused state so play button will resume from seek position
+ isPaused = true;
+ isPlaying = true; // Valid state for playback
+
+ if (playPauseBtn) {
+ playPauseBtn.innerHTML = PLAY_ICON_SVG;
+ }
+
+ return;
+ }
+
+ // Resume AudioContext if it was suspended
+ if (audioContext.state === 'suspended') {
+ audioContext.resume();
+ }
+
+ // Reschedule from the seek point
+ startTime = audioContext.currentTime - seekTime;
+ let nextStartTime = audioContext.currentTime;
+
+ for (let i = startChunkIndex; i < audioChunks.length; i++) {
+ const source = audioContext.createBufferSource();
+ source.buffer = audioChunks[i].buffer;
+ source.connect(audioContext.destination);
+
+ if (i === startChunkIndex) {
+ // Start from offset
+ source.start(nextStartTime, offsetInChunk);
+ nextStartTime += (audioChunks[i].buffer.duration - offsetInChunk);
+ } else {
+ source.start(nextStartTime);
+ nextStartTime += audioChunks[i].buffer.duration;
+ }
+
+ // Add silence between chunks
+ if (i < audioChunks.length - 1) {
+ nextStartTime += 0.3;
+ }
+
+ scheduledSources.push(source);
+ }
+
+ // Update nextScheduledTime for any future chunks
+ nextScheduledTime = nextStartTime;
+
+ // Resume playing state
+ isPlaying = true;
+ isPaused = false;
+ if (playPauseBtn) {
+ playPauseBtn.innerHTML = PAUSE_ICON_SVG;
+ }
+
+ // Restart progress animation
+ updateProgress();
+ };
+
+ // Callback for first chunk ready - create custom player and start playback
+ // Helper function to create AudioBuffer directly from Float32Array
+ const createAudioBufferFromFloat32 = (audioData, sampleRate) => {
+ const audioBuffer = audioContext.createBuffer(1, audioData.length, sampleRate);
+ audioBuffer.getChannelData(0).set(audioData);
+ return audioBuffer;
+ };
+
+ const onFirstChunkReady = async (audioData, sampleRate, duration, text, numChunks, firstChunkTime, processedChars) => {
+ totalChunks = numChunks;
+ firstChunkGenerationTime = firstChunkTime;
+
+ const container = document.getElementById('demoResults');
+
+
+ const textLength = currentGenerationTextLength > 0
+ ? currentGenerationTextLength
+ : (text ? text.length : 0);
+ const isBatch = textLength >= getMaxChunkLength();
+ const processingTimeStr = isBatch && firstChunkTime
+ ? `${formatTimeDetailed(firstChunkTime)} / ${formatTimeDetailed(firstChunkTime)}`
+ : formatTimeDetailed(firstChunkTime);
+ const safeInitialChars = typeof processedChars === 'number' ? processedChars : 0;
+ const displayedInitialChars = textLength > 0 ? Math.min(safeInitialChars, textLength) : safeInitialChars;
+ const charsPerSec = firstChunkTime > 0 && displayedInitialChars > 0
+ ? (displayedInitialChars / firstChunkTime).toFixed(1)
+ : '0.0';
+ const rtf = duration > 0 && firstChunkTime > 0 ? (firstChunkTime / duration).toFixed(3) : '-';
+ const progressValue = textLength > 0 ? Math.min(100, (displayedInitialChars / textLength) * 100) : 0;
+
+ const resultItemEl = document.getElementById('supertonic-result');
+ if (!resultItemEl) {
+ console.warn('Supertonic result container not found.');
+ return;
+ }
+
+ resultItemEl.classList.remove('generating');
+ resultItemEl.style.setProperty('--result-progress', `${progressValue}%`);
+
+ const titleMainEl = resultItemEl.querySelector('.title-main');
+ if (titleMainEl) {
+ titleMainEl.textContent = 'Supertonic';
+ titleMainEl.style.color = 'var(--accent-yellow)';
+ }
+ const titleSubEl = resultItemEl.querySelector('.title-sub');
+ if (titleSubEl) {
+ titleSubEl.textContent = 'On-Device';
+ }
+
+ const infoContainer = resultItemEl.querySelector('.demo-result-info');
+ if (infoContainer) {
+ infoContainer.classList.remove('error');
+ }
+ const timeElInitial = document.getElementById('supertonic-time');
+ if (timeElInitial) {
+ timeElInitial.innerHTML = formatStatValueWithSuffix(processingTimeStr, 's', { firstLabel: true });
+ }
+ const cpsElInitial = document.getElementById('supertonic-cps');
+ if (cpsElInitial) {
+ cpsElInitial.textContent = charsPerSec;
+ }
+ const rtfElInitial = document.getElementById('supertonic-rtf');
+ if (rtfElInitial) {
+ rtfElInitial.innerHTML = formatStatValueWithSuffix(rtf, 'x');
+ }
+
+ const playerContainer = resultItemEl.querySelector('.custom-audio-player');
+ if (playerContainer) {
+ playerContainer.style.display = '';
+ playerContainer.innerHTML = `
+