Created
February 19, 2025 00:49
-
-
Save greg-randall/09af04cfcedef0bc6f0a21336885a70e to your computer and use it in GitHub Desktop.
Recording Comparison. Reads a list of recordings in from a text file 'voices.csv', the first column is the voice name, and the second is a description. The recordings go in the same folder and the filename is generated by adding ".mp3" to the voice name.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<title>TTS Voice Comparison</title> | |
<style> | |
body { | |
margin: 0; | |
padding: 20px; | |
padding-bottom: 120px; | |
} | |
.voice-container { | |
margin: 10px 0; | |
padding: 10px; | |
border: 1px solid #ccc; | |
border-radius: 4px; | |
} | |
.voice-header { | |
display: flex; | |
align-items: center; | |
gap: 10px; | |
} | |
.voice-details { | |
color: #666; | |
font-size: 0.9em; | |
} | |
#controls { | |
position: fixed; | |
bottom: 0; | |
left: 0; | |
right: 0; | |
background: #f5f5f5; | |
padding: 20px; | |
box-shadow: 0 -2px 10px rgba(0,0,0,0.1); | |
display: grid; | |
grid-template-columns: auto 1fr; | |
gap: 20px; | |
align-items: center; | |
} | |
.controls-left { | |
display: flex; | |
align-items: center; | |
gap: 15px; | |
} | |
#playPauseBtn { | |
width: 60px; | |
height: 60px; | |
border-radius: 50%; | |
border: none; | |
background: #2196F3; | |
color: white; | |
font-size: 24px; | |
cursor: pointer; | |
transition: background-color 0.2s; | |
} | |
#playPauseBtn:hover { | |
background: #1976D2; | |
} | |
#currentTime { | |
font-size: 16px; | |
font-family: monospace; | |
min-width: 80px; | |
} | |
#timeSlider { | |
width: 100%; | |
} | |
input[type="range"] { | |
width: 100%; | |
height: 20px; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="voices"> | |
<?php | |
$handle = fopen("voices.csv", "r"); | |
$voices = array(); | |
// First, collect all the data | |
while (($row = fgetcsv($handle)) !== FALSE) { | |
$voices[$row[0]] = $row[1]; // Use voice name as key, description as value | |
} | |
fclose($handle); | |
// Sort by voice names | |
ksort($voices, SORT_NATURAL); | |
// Now generate HTML with the first item checked | |
$first_item = true; | |
$items = array(); | |
foreach ($voices as $voice_name => $description) { | |
$checked = $first_item ? " checked" : ""; | |
$first_item = false; | |
$items[] = " | |
<div class=\"voice-container\"> | |
<div class=\"voice-header\"> | |
<input type=\"radio\" name=\"voice\" value=\"$voice_name\"$checked> | |
<h2>$voice_name</h2> | |
</div> | |
<div class=\"voice-details\">$description</div> | |
<audio src=\"$voice_name.mp3\" preload=\"auto\"></audio> | |
</div> | |
"; | |
} | |
echo implode($items); | |
?> | |
</div> | |
<p style="padding-bottom:100px;"> </p> | |
<div id="controls"> | |
<div class="controls-left"> | |
<button id="playPauseBtn">▶️</button> | |
<div id="currentTime">0:00</div> | |
</div> | |
<div id="timeSlider"> | |
<input type="range" min="0" max="100" value="0" step="0.1"> | |
</div> | |
<div class="loop-control"> | |
<label for="loopToggle">Loop Recording:</label> | |
<input type="checkbox" id="loopToggle" checked> | |
<br> | |
<label for="repeatMs">Recording Overlap (ms):</label> | |
<input type="number" id="repeatMs" value="750" min="0" max="5000" step="100"> | |
</div> | |
</div> | |
<script> | |
let currentlyPlaying = null; | |
const allAudios = document.querySelectorAll('audio'); | |
const timeDisplay = document.getElementById('currentTime'); | |
const playPauseBtn = document.getElementById('playPauseBtn'); | |
const timeSlider = document.querySelector('#timeSlider input'); | |
const repeatMsInput = document.getElementById('repeatMs'); | |
const loopToggle = document.getElementById('loopToggle'); | |
let isDragging = false; | |
let audioDurations = new Map(); | |
// Initialize all audio elements and store their durations | |
allAudios.forEach(audio => { | |
audio.addEventListener('timeupdate', updateTimeDisplay); | |
audio.addEventListener('loadedmetadata', () => { | |
audioDurations.set(audio, audio.duration); | |
if (!currentlyPlaying) { | |
timeSlider.max = 100; | |
} | |
}); | |
// Handle audio ending | |
audio.addEventListener('ended', () => { | |
if (loopToggle.checked) { | |
const repeatDelay = parseInt(repeatMsInput.value); | |
setTimeout(() => { | |
if (audio === currentlyPlaying) { | |
audio.currentTime = 0; | |
audio.play(); | |
} | |
}, repeatDelay); | |
} else { | |
audio.currentTime = 0; | |
updatePlayPauseButton(); | |
} | |
}); | |
}); | |
// Handle radio button changes | |
document.querySelectorAll('input[type="radio"]').forEach(radio => { | |
radio.addEventListener('change', (e) => { | |
const container = e.target.closest('.voice-container'); | |
const newAudio = container.querySelector('audio'); | |
if (currentlyPlaying) { | |
const currentProgress = currentlyPlaying.currentTime / currentlyPlaying.duration; | |
const wasPlaying = !currentlyPlaying.paused; | |
currentlyPlaying.pause(); | |
// Calculate new position with repeat offset | |
const repeatSeconds = repeatMsInput.value / 1000; | |
const newPosition = (currentProgress * newAudio.duration) - repeatSeconds; | |
// Ensure we don't go below 0 | |
newAudio.currentTime = Math.max(0, newPosition); | |
if (wasPlaying) { | |
newAudio.play(); | |
} | |
} | |
currentlyPlaying = newAudio; | |
updatePlayPauseButton(); | |
}); | |
}); | |
// Update time display and slider using percentage | |
function updateTimeDisplay(e) { | |
if (e.target === currentlyPlaying && !isDragging) { | |
const currentProgress = (e.target.currentTime / e.target.duration) * 100; | |
const minutes = Math.floor(e.target.currentTime / 60); | |
const seconds = Math.floor(e.target.currentTime % 60).toString().padStart(2, '0'); | |
timeDisplay.textContent = `${minutes}:${seconds}`; | |
timeSlider.value = currentProgress; | |
} | |
} | |
// Play/Pause button | |
playPauseBtn.addEventListener('click', togglePlayPause); | |
function togglePlayPause() { | |
if (!currentlyPlaying) { | |
const checkedRadio = document.querySelector('input[type="radio"]:checked'); | |
currentlyPlaying = checkedRadio.closest('.voice-container').querySelector('audio'); | |
} | |
if (currentlyPlaying.paused) { | |
currentlyPlaying.play(); | |
} else { | |
currentlyPlaying.pause(); | |
} | |
updatePlayPauseButton(); | |
} | |
function updatePlayPauseButton() { | |
playPauseBtn.textContent = (!currentlyPlaying || currentlyPlaying.paused) ? '▶️' : '⏸️'; | |
} | |
// Time slider controls using percentage | |
timeSlider.addEventListener('mousedown', () => { | |
isDragging = true; | |
}); | |
timeSlider.addEventListener('mouseup', () => { | |
isDragging = false; | |
}); | |
timeSlider.addEventListener('input', (e) => { | |
if (currentlyPlaying) { | |
const newTime = (e.target.value / 100) * currentlyPlaying.duration; | |
const minutes = Math.floor(newTime / 60); | |
const seconds = Math.floor(newTime % 60).toString().padStart(2, '0'); | |
timeDisplay.textContent = `${minutes}:${seconds}`; | |
} | |
}); | |
timeSlider.addEventListener('change', (e) => { | |
if (currentlyPlaying) { | |
const newTime = (e.target.value / 100) * currentlyPlaying.duration; | |
currentlyPlaying.currentTime = newTime; | |
} | |
}); | |
// Handle spacebar to play/pause | |
document.addEventListener('keydown', (e) => { | |
if (e.code === 'Space' && !e.target.matches('input, button, textarea')) { | |
e.preventDefault(); | |
togglePlayPause(); | |
} | |
}); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment