Spaces:
Runtime error
Runtime error
Download audio-player.js from mocomoco-inc/AudioDecisionModel: direct link, hf CLI and curl.
- Browser
- Download file 7.46 kB
-
https://huggingface.co/spaces/mocomoco-inc/AudioDecisionModel/resolve/main/audio-player.js
- Command line
-
hf download hf://spaces/mocomoco-inc/AudioDecisionModel/audio-player.js
-
curl -L -o audio-player.js https://huggingface.co/spaces/mocomoco-inc/AudioDecisionModel/resolve/main/audio-player.js
7.46 kB
| const playIcon = `<svg aria-hidden="true" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5.3v13.4c0 .8.9 1.3 1.6.8l10.1-6.7a1 1 0 0 0 0-1.6L9.6 4.5A1 1 0 0 0 8 5.3Z"/></svg>`; | |
| const pauseIcon = `<svg aria-hidden="true" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="5" width="4.5" height="14" rx="1"/><rect x="13.5" y="5" width="4.5" height="14" rx="1"/></svg>`; | |
| function formatTime(value) { | |
| const seconds = Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0; | |
| return `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, "0")}`; | |
| } | |
| function userError(error) { | |
| if (error?.name === "NotAllowedError") return "再生がブラウザにより停止されました。再生ボタンをもう一度押してください。"; | |
| if (error?.name === "NotSupportedError") return "この音声形式はブラウザで再生できません。別の音声ファイルをお試しください。"; | |
| return "音声を再生できませんでした。音声ファイルを確認して、もう一度お試しください。"; | |
| } | |
| /** Controls a native audio element and draws decoded mono 16 kHz samples locally. */ | |
| export function createAudioPlayer({ audio, container, canvas, playButton, seek, time, speed, onError } = {}) { | |
| if (!audio || !container || !canvas || !playButton || !seek || !time || !speed) { | |
| throw new Error("音声プレーヤーのUI要素が不足しています。"); | |
| } | |
| const context = canvas.getContext("2d"); | |
| let samples = new Float32Array(); | |
| let animationFrame = 0; | |
| let lastDrawAt = 0; | |
| let disposed = false; | |
| let resizeObserver = null; | |
| canvas.setAttribute("role", "img"); | |
| canvas.setAttribute("aria-label", "音声の波形と再生位置"); | |
| seek.min = "0"; seek.step = "0.01"; seek.value = "0"; | |
| seek.setAttribute("aria-label", "再生位置"); | |
| function duration() { return Number.isFinite(audio.duration) ? audio.duration : 0; } | |
| function setState(value) { container.dataset.playerState = value; } | |
| function setButton(playing) { | |
| playButton.setAttribute("aria-label", playing ? "一時停止" : "再生"); | |
| playButton.setAttribute("aria-pressed", String(playing)); | |
| playButton.innerHTML = playing ? pauseIcon : playIcon; | |
| } | |
| function updateText() { time.textContent = `${formatTime(audio.currentTime)} / ${formatTime(duration())}`; } | |
| function updateSeek() { seek.max = String(duration()); seek.value = String(Math.min(duration(), audio.currentTime || 0)); updateText(); } | |
| function canvasSize() { | |
| const rect = canvas.getBoundingClientRect(); | |
| const dpr = window.devicePixelRatio || 1; | |
| const width = Math.max(1, Math.round(rect.width * dpr)); | |
| const height = Math.max(1, Math.round(rect.height * dpr)); | |
| if (canvas.width !== width || canvas.height !== height) { canvas.width = width; canvas.height = height; } | |
| return { width, height, dpr }; | |
| } | |
| function draw() { | |
| const { width, height, dpr } = canvasSize(); | |
| context.clearRect(0, 0, width, height); | |
| const style = getComputedStyle(container); | |
| const brand = style.getPropertyValue("--brand").trim() || "#7203ff"; | |
| const muted = style.getPropertyValue("--wave-muted").trim() || "#c6c8d0"; | |
| const binCount = Math.max(24, Math.min(140, Math.round((width / dpr) / 4))); | |
| const progress = duration() ? Math.max(0, Math.min(1, audio.currentTime / duration())) : 0; | |
| const gap = Math.max(1, Math.round(dpr)); | |
| const barWidth = Math.max(1, (width - gap * (binCount - 1)) / binCount); | |
| const minHeight = Math.max(2 * dpr, height * 0.07); | |
| for (let bin = 0; bin < binCount; bin += 1) { | |
| const start = Math.floor(bin * samples.length / binCount); | |
| const end = Math.max(start + 1, Math.floor((bin + 1) * samples.length / binCount)); | |
| let peak = 0; | |
| for (let index = start; index < end; index += 1) peak = Math.max(peak, Math.abs(samples[index] || 0)); | |
| const barHeight = Math.max(minHeight, peak * (height * 0.9)); | |
| const x = bin * (barWidth + gap); | |
| context.fillStyle = (bin + 0.5) / binCount <= progress ? brand : muted; | |
| context.beginPath(); | |
| if (typeof context.roundRect === "function") context.roundRect(x, (height - barHeight) / 2, barWidth, barHeight, Math.min(barWidth / 2, 3 * dpr)); | |
| else context.rect(x, (height - barHeight) / 2, barWidth, barHeight); | |
| context.fill(); | |
| } | |
| } | |
| function animate(now) { | |
| if (audio.paused || disposed) { animationFrame = 0; return; } | |
| animationFrame = requestAnimationFrame(animate); | |
| if (now - lastDrawAt >= 50) { lastDrawAt = now; updateSeek(); draw(); } | |
| } | |
| function startAnimation() { | |
| if (!animationFrame) { lastDrawAt = 0; animationFrame = requestAnimationFrame(animate); } | |
| } | |
| function stopAnimation() { cancelAnimationFrame(animationFrame); animationFrame = 0; } | |
| async function togglePlay() { | |
| if (!audio.paused) { audio.pause(); return; } | |
| setState("waiting"); playButton.disabled = true; | |
| try { await audio.play(); } | |
| catch (error) { setState("error"); setButton(false); onError?.(new Error(userError(error))); } | |
| finally { playButton.disabled = false; } | |
| } | |
| function onMetadata() { | |
| container.hidden = false; updateSeek(); setButton(false); setState("ready"); draw(); | |
| } | |
| function onSourceReset() { | |
| stopAnimation(); seek.value = "0"; time.textContent = "0:00 / 0:00"; setButton(false); setState("loading"); draw(); | |
| } | |
| function onPlay() { setState("playing"); setButton(true); startAnimation(); } | |
| function onPause() { setState("paused"); setButton(false); stopAnimation(); updateSeek(); draw(); } | |
| function onEnded() { setState("ended"); setButton(false); stopAnimation(); updateSeek(); draw(); } | |
| function onWaiting() { setState("waiting"); } | |
| function onErrorEvent() { setState("error"); setButton(false); stopAnimation(); onError?.(new Error(userError(audio.error))); } | |
| const listeners = [ | |
| ["loadedmetadata", onMetadata], ["durationchange", updateSeek], ["loadstart", onSourceReset], ["emptied", onSourceReset], | |
| ["timeupdate", () => { updateSeek(); if (audio.paused) draw(); }], ["play", onPlay], ["pause", onPause], ["ended", onEnded], ["waiting", onWaiting], ["error", onErrorEvent] | |
| ]; | |
| listeners.forEach(([event, handler]) => audio.addEventListener(event, handler)); | |
| playButton.addEventListener("click", togglePlay); | |
| seek.addEventListener("input", () => { const position = Number(seek.value); if (Number.isFinite(position)) { audio.currentTime = position; updateText(); draw(); } }); | |
| speed.addEventListener("change", () => { const rate = Number(speed.value); if (Number.isFinite(rate) && rate > 0) audio.playbackRate = rate; }); | |
| if (typeof ResizeObserver !== "undefined") { resizeObserver = new ResizeObserver(draw); resizeObserver.observe(canvas); } | |
| function setSamples(value) { | |
| if (!(value instanceof Float32Array)) throw new TypeError("波形データはFloat32Arrayで指定してください。"); | |
| samples = value.slice(); draw(); | |
| } | |
| function dispose() { | |
| disposed = true; stopAnimation(); resizeObserver?.disconnect(); resizeObserver = null; | |
| listeners.forEach(([event, handler]) => audio.removeEventListener(event, handler)); | |
| playButton.removeEventListener("click", togglePlay); | |
| audio.pause(); | |
| } | |
| const initialRate = Number(speed.value); if (Number.isFinite(initialRate) && initialRate > 0) audio.playbackRate = initialRate; | |
| setButton(false); updateText(); setState("idle"); draw(); | |
| return { setSamples, refreshTheme: draw, dispose }; | |
| } | |