${titleMain}
${titleSub}
${titleStatus}
`;
};
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 = `