Skip to content

Instantly share code, notes, and snippets.

@greg-randall
Created February 19, 2025 00:49
Show Gist options
  • Save greg-randall/09af04cfcedef0bc6f0a21336885a70e to your computer and use it in GitHub Desktop.
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.
<!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;">&nbsp;</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