mirror of
https://github.com/kennethreitz/interpretations.git
synced 2026-06-05 06:46:15 +00:00
b6009f9a11
Static site with nginx on fly.io. Features: - Album art, track list with descriptions and metadata - Persistent bottom player bar with seek, volume, next/prev - Real-time oscilloscope via Web Audio API - Deep linking via URL hash (#track_name) - Share link per track (copy to clipboard) - Keyboard shortcuts (space, arrows, n/p) - Mobile responsive - SEO meta tags, Open Graph, Twitter cards - Streaming service link placeholders - MP3s converted from WAVs for web delivery Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
612 lines
20 KiB
HTML
612 lines
20 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Interpretations — Infinite State</title>
|
|
<meta name="description" content="A 24-track album written entirely in Python. Indian classical ragas, trap beats, ambient drones, acid bass, mellotron tape, and raw synthesis — every sound generated from code using pytheory.">
|
|
<meta name="keywords" content="Infinite State, Interpretations, pytheory, Python music, algorithmic composition, sitar, mellotron, 808, electronic music, Indian classical, ambient, trap">
|
|
<meta name="author" content="Infinite State">
|
|
<meta property="og:title" content="Interpretations — Infinite State">
|
|
<meta property="og:description" content="24 tracks written in Python. Sitar meets 808. Code becomes music.">
|
|
<meta property="og:image" content="cover.png">
|
|
<meta property="og:type" content="music.album">
|
|
<meta name="twitter:card" content="summary_large_image">
|
|
<meta name="twitter:title" content="Interpretations — Infinite State">
|
|
<meta name="twitter:description" content="24 tracks written in Python. Sitar meets 808. Code becomes music.">
|
|
<meta name="twitter:image" content="cover.png">
|
|
<link rel="icon" type="image/png" href="cover.png">
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
|
|
body {
|
|
background: #0a0a0a;
|
|
color: #e0e0e0;
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
|
min-height: 100vh;
|
|
padding-bottom: 100px;
|
|
}
|
|
|
|
/* Header */
|
|
.header {
|
|
text-align: center;
|
|
padding: 40px 20px 20px;
|
|
}
|
|
|
|
.cover {
|
|
width: 320px;
|
|
height: 320px;
|
|
border-radius: 8px;
|
|
box-shadow: 0 8px 32px rgba(0,0,0,0.6);
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.album-title {
|
|
font-size: 28px;
|
|
font-weight: 700;
|
|
color: #fff;
|
|
letter-spacing: 1px;
|
|
}
|
|
|
|
.artist {
|
|
font-size: 16px;
|
|
color: #888;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.album-info {
|
|
font-size: 13px;
|
|
color: #666;
|
|
margin-top: 8px;
|
|
}
|
|
|
|
/* Track list */
|
|
.tracklist {
|
|
max-width: 700px;
|
|
margin: 30px auto;
|
|
padding: 0 20px;
|
|
}
|
|
|
|
.track {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 12px 16px;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
transition: background 0.15s;
|
|
gap: 14px;
|
|
}
|
|
|
|
.track:hover {
|
|
background: #1a1a1a;
|
|
}
|
|
|
|
.track.active {
|
|
background: #1a1a1a;
|
|
}
|
|
|
|
.track.playing {
|
|
background: #161a12;
|
|
}
|
|
|
|
.track-num {
|
|
font-size: 14px;
|
|
color: #555;
|
|
width: 28px;
|
|
text-align: right;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.track.playing .track-num {
|
|
color: #c8a846;
|
|
}
|
|
|
|
.track-info {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.track-title {
|
|
font-size: 15px;
|
|
font-weight: 500;
|
|
color: #ddd;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.track.playing .track-title {
|
|
color: #c8a846;
|
|
}
|
|
|
|
.track-meta {
|
|
font-size: 12px;
|
|
color: #666;
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.track-duration {
|
|
font-size: 13px;
|
|
color: #555;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
/* Player bar */
|
|
.player {
|
|
position: fixed;
|
|
bottom: 0;
|
|
left: 0;
|
|
right: 0;
|
|
background: #141414;
|
|
border-top: 1px solid #222;
|
|
padding: 12px 20px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
z-index: 100;
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
|
|
.player-cover {
|
|
width: 48px;
|
|
height: 48px;
|
|
border-radius: 4px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.player-info {
|
|
flex-shrink: 0;
|
|
width: 180px;
|
|
}
|
|
|
|
.player-title {
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
color: #fff;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.player-artist {
|
|
font-size: 12px;
|
|
color: #888;
|
|
}
|
|
|
|
.player-controls {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.player-btn {
|
|
background: none;
|
|
border: none;
|
|
color: #ccc;
|
|
font-size: 20px;
|
|
cursor: pointer;
|
|
padding: 4px;
|
|
transition: color 0.15s;
|
|
}
|
|
|
|
.player-btn:hover {
|
|
color: #fff;
|
|
}
|
|
|
|
.player-btn.play-btn {
|
|
font-size: 28px;
|
|
color: #fff;
|
|
}
|
|
|
|
.player-progress {
|
|
flex: 1;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.player-time {
|
|
font-size: 12px;
|
|
color: #888;
|
|
flex-shrink: 0;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.progress-bar {
|
|
flex: 1;
|
|
height: 4px;
|
|
background: #333;
|
|
border-radius: 2px;
|
|
cursor: pointer;
|
|
position: relative;
|
|
}
|
|
|
|
.progress-bar:hover {
|
|
height: 6px;
|
|
}
|
|
|
|
.progress-fill {
|
|
height: 100%;
|
|
background: #c8a846;
|
|
border-radius: 2px;
|
|
width: 0%;
|
|
transition: width 0.1s linear;
|
|
}
|
|
|
|
.player-volume {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.volume-bar {
|
|
width: 80px;
|
|
height: 4px;
|
|
background: #333;
|
|
border-radius: 2px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.volume-fill {
|
|
height: 100%;
|
|
background: #888;
|
|
border-radius: 2px;
|
|
width: 80%;
|
|
}
|
|
|
|
/* Oscilloscope */
|
|
.visualizer-wrap {
|
|
max-width: 700px;
|
|
margin: 0 auto;
|
|
padding: 0 20px;
|
|
}
|
|
|
|
#oscilloscope {
|
|
width: 100%;
|
|
height: 80px;
|
|
display: block;
|
|
}
|
|
|
|
/* Streaming links */
|
|
.streaming-links {
|
|
margin-top: 16px;
|
|
display: flex;
|
|
gap: 10px;
|
|
justify-content: center;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.stream-btn {
|
|
display: inline-block;
|
|
padding: 8px 18px;
|
|
border: 1px solid #333;
|
|
border-radius: 20px;
|
|
color: #aaa;
|
|
text-decoration: none;
|
|
font-size: 13px;
|
|
transition: all 0.15s;
|
|
}
|
|
|
|
.stream-btn:hover {
|
|
border-color: #c8a846;
|
|
color: #c8a846;
|
|
}
|
|
|
|
/* Track description */
|
|
.track-desc {
|
|
font-size: 12px;
|
|
color: #555;
|
|
margin-top: 2px;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.track.playing .track-desc {
|
|
color: #887a3a;
|
|
}
|
|
|
|
/* Share link */
|
|
.track-share {
|
|
font-size: 12px;
|
|
color: #444;
|
|
cursor: pointer;
|
|
flex-shrink: 0;
|
|
padding: 4px;
|
|
transition: color 0.15s;
|
|
}
|
|
|
|
.track-share:hover {
|
|
color: #c8a846;
|
|
}
|
|
|
|
/* Responsive */
|
|
@media (max-width: 640px) {
|
|
.cover { width: 240px; height: 240px; }
|
|
.player-info { width: 120px; }
|
|
.player-volume { display: none; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="header">
|
|
<img src="cover.png" alt="Interpretations" class="cover">
|
|
<div class="album-title">Interpretations</div>
|
|
<div class="artist">Infinite State</div>
|
|
<div class="album-info">24 tracks · Written in Python · Generated from code</div>
|
|
<div class="streaming-links" id="streaming-links">
|
|
<!-- Uncomment as links go live
|
|
<a href="#" class="stream-btn">Spotify</a>
|
|
<a href="#" class="stream-btn">Apple Music</a>
|
|
<a href="#" class="stream-btn">Bandcamp</a>
|
|
<a href="#" class="stream-btn">YouTube Music</a>
|
|
-->
|
|
<a href="https://github.com/kennethreitz/interpretations" class="stream-btn">GitHub</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="visualizer-wrap">
|
|
<canvas id="oscilloscope" width="700" height="80"></canvas>
|
|
</div>
|
|
|
|
<div class="tracklist" id="tracklist"></div>
|
|
|
|
<div class="player" id="player" style="display:none">
|
|
<img src="cover.png" alt="" class="player-cover">
|
|
<div class="player-info">
|
|
<div class="player-title" id="player-title">—</div>
|
|
<div class="player-artist">Infinite State</div>
|
|
</div>
|
|
<div class="player-controls">
|
|
<button class="player-btn" onclick="prevTrack()">⏮</button>
|
|
<button class="player-btn play-btn" id="play-btn" onclick="togglePlay()">▶</button>
|
|
<button class="player-btn" onclick="nextTrack()">⏭</button>
|
|
</div>
|
|
<div class="player-progress">
|
|
<span class="player-time" id="time-current">0:00</span>
|
|
<div class="progress-bar" id="progress-bar" onclick="seek(event)">
|
|
<div class="progress-fill" id="progress-fill"></div>
|
|
</div>
|
|
<span class="player-time" id="time-total">0:00</span>
|
|
</div>
|
|
<div class="player-volume">
|
|
<span style="color:#888;font-size:14px">🔊</span>
|
|
<div class="volume-bar" onclick="setVolume(event)">
|
|
<div class="volume-fill" id="volume-fill"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const TRACKS = [
|
|
{ num: 1, file: "01_raga_midnight", title: "Raga Midnight", key: "D phr", bpm: 90, tuning: "shruti · 432Hz", desc: "Sitar raga in four movements with 808 drop. Tabla, tambura, dhol." },
|
|
{ num: 2, file: "02_shruti_lofi", title: "Shruti Lofi", key: "Dm", bpm: 75, tuning: "shruti", desc: "Microtonal lo-fi hip hop. Kalimba, Rhodes, sitar hook, mellotron flute." },
|
|
{ num: 3, file: "03_ghost_protocol", title: "Ghost Protocol", key: "Fm", bpm: 128, tuning: "", desc: "Trip-hop into trance build. Drift arp, kick at bar 49, NES melody at the peak." },
|
|
{ num: 4, file: "04_silk_road", title: "Silk Road", key: "Dm", bpm: 95, tuning: "", desc: "A caravan picking up musicians. Koto → sitar → mandolin → guitar → all together." },
|
|
{ num: 5, file: "05_the_observatory", title: "The Observatory", key: "Gm", bpm: 112, tuning: "", desc: "Chapel through shortwave static. Organ, choir, saw arp, theremin signal." },
|
|
{ num: 6, file: "06_acid_reign", title: "Acid Reign", key: "Am", bpm: 140, tuning: "", desc: "Dual 303 acid bass with resonant filter sweeps. Cajon, Rhodes, 808 sub." },
|
|
{ num: 7, file: "07_beast_mode", title: "Beast Mode", key: "Gm", bpm: 135, tuning: "", desc: "Trap drums, 808 slides, sitar hook + shred solo, mellotron flute drop." },
|
|
{ num: 8, file: "08_apex", title: "Apex", key: "Ebm", bpm: 140, tuning: "", desc: "The fastest, the hardest. Koto hook, wavefold bass, timpani, 32nd shreds." },
|
|
{ num: 9, file: "09_voltage", title: "Voltage", key: "Fm", bpm: 138, tuning: "", desc: "Raw oscillators. Sine, saw, square, hard_sync. Rhythm is pitch." },
|
|
{ num: 10, file: "10_an_exception_occurred", title: "An Exception Occurred", key: "Eb", bpm: 80, tuning: "", desc: "Stability → seeking → psychosis → despair → hymn → recovery. Every note by hand." },
|
|
{ num: 11, file: "11_voices", title: "Voices", key: "F#m", bpm: 65, tuning: "", desc: "Five vocal parts multiplying. Piano enters as reality. One last whisper." },
|
|
{ num: 12, file: "12_intrusive", title: "Intrusive", key: "Bbm", bpm: 92, tuning: "", desc: "One wavefold phrase repeating. Rhodes fights it. Drums fight it. It passes." },
|
|
{ num: 13, file: "13_gravity", title: "Gravity", key: "Cm", bpm: 88, tuning: "", desc: "Sparse piano, 808, boom bap. Sitar bend in the breakdown. Eastern touches." },
|
|
{ num: 14, file: "14_the_interruption", title: "The Interruption", key: "Dm", bpm: 85, tuning: "", desc: "String quartet ambushed by DnB. Flute, harp, drift reese. The strings win." },
|
|
{ num: 15, file: "15_sleight_of_hand", title: "Sleight of Hand", key: "Dm", bpm: 100, tuning: "", desc: "Nine genre shifts. Music box → didge → jazz → 808 → theremin → choir → acid 303." },
|
|
{ num: 16, file: "16_waveforms", title: "Waveforms", key: "Fm", bpm: 118, tuning: "", desc: "Synth showcase. Percussive blips stacking. FM solo, duet in thirds, canon." },
|
|
{ num: 17, file: "17_emergence", title: "Emergence", key: "Em", bpm: 100, tuning: "", desc: "Singing bowls + sitar arps birth synths. Both worlds collide at the peak." },
|
|
{ num: 18, file: "18_chakra", title: "Chakra", key: "(multi)", bpm: "60→135", tuning: "shruti · 432Hz", desc: "Root to crown journey. Seven chakras, metric modulation, drift at the crown." },
|
|
{ num: 19, file: "19_the_temple", title: "The Temple", key: "A phr", bpm: 65, tuning: "shruti · 432Hz", desc: "Devotional layers in a vast stone chamber. The reverb is the instrument." },
|
|
{ num: 20, file: "20_the_dialogue", title: "The Dialogue", key: "E phr", bpm: 75, tuning: "shruti · 432Hz", desc: "Sitar (human) and theremin (machine) find each other. House beat arrives." },
|
|
{ num: 21, file: "21_cathedral", title: "Cathedral", key: "Dm", bpm: 60, tuning: "", desc: "Tubular bells, bagpipe drone, mellotron choir, timpani thunder, pipe organ." },
|
|
{ num: 22, file: "22_tape_memory", title: "Tape Memory", key: "Dbm", bpm: 90, tuning: "", desc: "Mellotron flute dreams. FM, drift, crotales, hard_sync, PWM, wavefold, ring_mod." },
|
|
{ num: 23, file: "23_music_box_factory", title: "Music Box Factory", key: "G", bpm: 108, tuning: "", desc: "Eight tuned percussion instruments. Kalimba, vibraphone, celesta, marimba." },
|
|
{ num: 24, file: "24_deep_time", title: "Deep Time", key: "Bm", bpm: 40, tuning: "just", desc: "7.5 min ambient drone. Tingsha, singing bowls, didgeridoo, theremin, cello." },
|
|
];
|
|
|
|
const audio = new Audio();
|
|
audio.crossOrigin = 'anonymous';
|
|
let currentTrack = -1;
|
|
let isPlaying = false;
|
|
|
|
// Web Audio API — oscilloscope
|
|
let audioCtx, analyser, source, dataArray;
|
|
const canvas = document.getElementById('oscilloscope');
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
function initAudio() {
|
|
if (audioCtx) return;
|
|
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
|
analyser = audioCtx.createAnalyser();
|
|
analyser.fftSize = 2048;
|
|
source = audioCtx.createMediaElementSource(audio);
|
|
source.connect(analyser);
|
|
analyser.connect(audioCtx.destination);
|
|
dataArray = new Uint8Array(analyser.frequencyBinCount);
|
|
drawOscilloscope();
|
|
}
|
|
|
|
function drawOscilloscope() {
|
|
requestAnimationFrame(drawOscilloscope);
|
|
if (!analyser) return;
|
|
|
|
const w = canvas.width = canvas.offsetWidth * 2;
|
|
const h = canvas.height = 160;
|
|
analyser.getByteTimeDomainData(dataArray);
|
|
|
|
ctx.fillStyle = '#0a0a0a';
|
|
ctx.fillRect(0, 0, w, h);
|
|
|
|
ctx.lineWidth = 2;
|
|
ctx.strokeStyle = '#c8a846';
|
|
ctx.beginPath();
|
|
|
|
const sliceWidth = w / dataArray.length;
|
|
let x = 0;
|
|
|
|
for (let i = 0; i < dataArray.length; i++) {
|
|
const v = dataArray[i] / 128.0;
|
|
const y = v * h / 2;
|
|
if (i === 0) ctx.moveTo(x, y);
|
|
else ctx.lineTo(x, y);
|
|
x += sliceWidth;
|
|
}
|
|
|
|
ctx.lineTo(w, h / 2);
|
|
ctx.stroke();
|
|
}
|
|
|
|
// Build track list
|
|
const tracklist = document.getElementById('tracklist');
|
|
const BASE_URL = 'https://interpretations.kennethreitz.org';
|
|
|
|
TRACKS.forEach((t, i) => {
|
|
const meta = [t.key, t.bpm + ' BPM', t.tuning].filter(Boolean).join(' · ');
|
|
const el = document.createElement('div');
|
|
el.className = 'track';
|
|
el.id = `track-${i}`;
|
|
el.setAttribute('data-slug', t.file.replace(/^\d+_/, ''));
|
|
el.innerHTML = `
|
|
<span class="track-num">${t.num}</span>
|
|
<div class="track-info">
|
|
<div class="track-title">${t.title}</div>
|
|
<div class="track-meta">${meta}</div>
|
|
<div class="track-desc">${t.desc}</div>
|
|
</div>
|
|
<span class="track-share" title="Copy link" onclick="event.stopPropagation(); copyTrackLink(${i})">🔗</span>
|
|
<span class="track-duration" id="dur-${i}">—</span>
|
|
`;
|
|
el.onclick = () => playTrack(i);
|
|
tracklist.appendChild(el);
|
|
});
|
|
|
|
function copyTrackLink(i) {
|
|
const slug = TRACKS[i].file.replace(/^\d+_/, '');
|
|
const url = `${BASE_URL}/#${slug}`;
|
|
navigator.clipboard.writeText(url).then(() => {
|
|
const el = document.querySelectorAll('.track-share')[i];
|
|
el.textContent = '✓';
|
|
setTimeout(() => el.textContent = '🔗', 1500);
|
|
});
|
|
}
|
|
|
|
function formatTime(s) {
|
|
if (isNaN(s)) return '0:00';
|
|
const m = Math.floor(s / 60);
|
|
const sec = Math.floor(s % 60);
|
|
return `${m}:${sec.toString().padStart(2, '0')}`;
|
|
}
|
|
|
|
function playTrack(index) {
|
|
if (currentTrack >= 0) {
|
|
document.getElementById(`track-${currentTrack}`).classList.remove('playing');
|
|
}
|
|
currentTrack = index;
|
|
const t = TRACKS[index];
|
|
audio.src = `mp3s/${t.file}.mp3`;
|
|
initAudio();
|
|
audio.play();
|
|
isPlaying = true;
|
|
|
|
document.getElementById(`track-${index}`).classList.add('playing');
|
|
document.getElementById('player').style.display = 'flex';
|
|
document.getElementById('player-title').textContent = t.title;
|
|
document.getElementById('play-btn').textContent = '⏸';
|
|
}
|
|
|
|
function togglePlay() {
|
|
if (currentTrack < 0) return;
|
|
if (isPlaying) {
|
|
audio.pause();
|
|
isPlaying = false;
|
|
document.getElementById('play-btn').textContent = '▶';
|
|
} else {
|
|
audio.play();
|
|
isPlaying = true;
|
|
document.getElementById('play-btn').textContent = '⏸';
|
|
}
|
|
}
|
|
|
|
function nextTrack() {
|
|
if (currentTrack < TRACKS.length - 1) {
|
|
playTrack(currentTrack + 1);
|
|
}
|
|
}
|
|
|
|
function prevTrack() {
|
|
if (audio.currentTime > 3) {
|
|
audio.currentTime = 0;
|
|
} else if (currentTrack > 0) {
|
|
playTrack(currentTrack - 1);
|
|
}
|
|
}
|
|
|
|
function seek(e) {
|
|
const bar = document.getElementById('progress-bar');
|
|
const pct = e.offsetX / bar.offsetWidth;
|
|
audio.currentTime = pct * audio.duration;
|
|
}
|
|
|
|
function setVolume(e) {
|
|
const bar = e.currentTarget;
|
|
const pct = e.offsetX / bar.offsetWidth;
|
|
audio.volume = Math.max(0, Math.min(1, pct));
|
|
document.getElementById('volume-fill').style.width = (pct * 100) + '%';
|
|
}
|
|
|
|
// Auto-advance
|
|
audio.addEventListener('ended', () => {
|
|
if (currentTrack < TRACKS.length - 1) {
|
|
playTrack(currentTrack + 1);
|
|
} else {
|
|
isPlaying = false;
|
|
document.getElementById('play-btn').textContent = '▶';
|
|
}
|
|
});
|
|
|
|
// Progress updates
|
|
audio.addEventListener('timeupdate', () => {
|
|
document.getElementById('time-current').textContent = formatTime(audio.currentTime);
|
|
document.getElementById('time-total').textContent = formatTime(audio.duration);
|
|
const pct = (audio.currentTime / audio.duration) * 100;
|
|
document.getElementById('progress-fill').style.width = pct + '%';
|
|
});
|
|
|
|
// Load durations
|
|
audio.addEventListener('loadedmetadata', () => {
|
|
if (currentTrack >= 0) {
|
|
document.getElementById(`dur-${currentTrack}`).textContent = formatTime(audio.duration);
|
|
}
|
|
});
|
|
|
|
// Deep link — play track from URL hash
|
|
function checkHash() {
|
|
const hash = window.location.hash.slice(1);
|
|
if (hash) {
|
|
const idx = TRACKS.findIndex(t => t.file.replace(/^\d+_/, '') === hash);
|
|
if (idx >= 0) {
|
|
playTrack(idx);
|
|
document.getElementById(`track-${idx}`).scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
}
|
|
}
|
|
}
|
|
window.addEventListener('hashchange', checkHash);
|
|
window.addEventListener('load', checkHash);
|
|
|
|
// Keyboard shortcuts
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.code === 'Space') { e.preventDefault(); togglePlay(); }
|
|
if (e.code === 'ArrowRight') { audio.currentTime += 5; }
|
|
if (e.code === 'ArrowLeft') { audio.currentTime -= 5; }
|
|
if (e.key === 'n') { nextTrack(); }
|
|
if (e.key === 'p') { prevTrack(); }
|
|
});
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|