|
<!DOCTYPE html> |
|
<html lang="ja"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>音声合成プレイヤー</title> |
|
<style> |
|
body { |
|
font-family: 'Arial', sans-serif; |
|
background-color: #0a192f; |
|
color: #e6f1ff; |
|
margin: 0; |
|
padding: 20px; |
|
display: flex; |
|
flex-direction: column; |
|
align-items: center; |
|
} |
|
|
|
h1 { |
|
color: #64ffda; |
|
text-align: center; |
|
margin-bottom: 30px; |
|
font-size: 2.5em; |
|
text-shadow: 0 0 10px rgba(100, 255, 218, 0.3); |
|
} |
|
|
|
.container { |
|
display: flex; |
|
width: 100%; |
|
max-width: 1200px; |
|
gap: 20px; |
|
} |
|
|
|
.left-panel, .right-panel { |
|
flex: 1; |
|
background-color: #112240; |
|
border-radius: 10px; |
|
padding: 20px; |
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); |
|
} |
|
|
|
.drop-box { |
|
border: 2px dashed #64ffda; |
|
border-radius: 8px; |
|
padding: 20px; |
|
min-height: 150px; |
|
margin-bottom: 20px; |
|
transition: all 0.3s; |
|
background-color: rgba(100, 255, 218, 0.05); |
|
} |
|
|
|
.drop-box.highlight { |
|
background-color: rgba(100, 255, 218, 0.1); |
|
border-color: #64ffda; |
|
box-shadow: 0 0 15px rgba(100, 255, 218, 0.2); |
|
} |
|
|
|
.sound-box { |
|
display: inline-block; |
|
background-color: #233554; |
|
color: #e6f1ff; |
|
padding: 8px 15px; |
|
margin: 5px; |
|
border-radius: 20px; |
|
border: 1px solid #64ffda; |
|
cursor: move; |
|
user-select: none; |
|
transition: all 0.2s; |
|
} |
|
|
|
.sound-box:hover { |
|
background-color: #1e2a47; |
|
transform: translateY(-2px); |
|
} |
|
|
|
.sound-box.selected { |
|
background-color: #64ffda; |
|
color: #0a192f; |
|
font-weight: bold; |
|
} |
|
|
|
.video-container { |
|
position: relative; |
|
width: 100%; |
|
margin-bottom: 20px; |
|
} |
|
|
|
video { |
|
width: 100%; |
|
border-radius: 8px; |
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); |
|
} |
|
|
|
.controls { |
|
display: flex; |
|
justify-content: center; |
|
gap: 15px; |
|
margin-bottom: 20px; |
|
} |
|
|
|
button { |
|
background-color: #233554; |
|
color: #e6f1ff; |
|
border: 1px solid #64ffda; |
|
border-radius: 5px; |
|
padding: 10px 20px; |
|
cursor: pointer; |
|
transition: all 0.3s; |
|
font-size: 1em; |
|
} |
|
|
|
button:hover { |
|
background-color: #64ffda; |
|
color: #0a192f; |
|
transform: translateY(-2px); |
|
box-shadow: 0 5px 15px rgba(100, 255, 218, 0.3); |
|
} |
|
|
|
.settings { |
|
background-color: #112240; |
|
border-radius: 10px; |
|
padding: 20px; |
|
margin-top: 20px; |
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); |
|
width: 100%; |
|
max-width: 1200px; |
|
} |
|
|
|
.settings h2 { |
|
color: #64ffda; |
|
margin-top: 0; |
|
border-bottom: 1px solid #233554; |
|
padding-bottom: 10px; |
|
} |
|
|
|
.setting-group { |
|
display: flex; |
|
flex-wrap: wrap; |
|
gap: 20px; |
|
margin-bottom: 15px; |
|
} |
|
|
|
.setting-item { |
|
flex: 1; |
|
min-width: 200px; |
|
} |
|
|
|
label { |
|
display: block; |
|
margin-bottom: 5px; |
|
color: #ccd6f6; |
|
} |
|
|
|
input[type="range"], input[type="number"] { |
|
width: 100%; |
|
background-color: #233554; |
|
border: 1px solid #1e2a47; |
|
border-radius: 5px; |
|
padding: 8px; |
|
color: #e6f1ff; |
|
} |
|
|
|
input[type="range"] { |
|
-webkit-appearance: none; |
|
height: 5px; |
|
background: #233554; |
|
border-radius: 5px; |
|
} |
|
|
|
input[type="range"]::-webkit-slider-thumb { |
|
-webkit-appearance: none; |
|
width: 15px; |
|
height: 15px; |
|
background: #64ffda; |
|
border-radius: 50%; |
|
cursor: pointer; |
|
} |
|
|
|
.status { |
|
margin-top: 20px; |
|
padding: 10px; |
|
background-color: rgba(100, 255, 218, 0.1); |
|
border-left: 3px solid #64ffda; |
|
border-radius: 0 5px 5px 0; |
|
} |
|
|
|
.tech-decoration { |
|
position: absolute; |
|
width: 100%; |
|
height: 100%; |
|
top: 0; |
|
left: 0; |
|
pointer-events: none; |
|
z-index: -1; |
|
opacity: 0.1; |
|
background: |
|
linear-gradient(90deg, #112240 1px, transparent 1px) 0 0 / 20px 20px, |
|
linear-gradient(#112240 1px, transparent 1px) 0 0 / 20px 20px; |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<div class="tech-decoration"></div> |
|
<h1>音声合成プレイヤー</h1> |
|
|
|
<div class="container"> |
|
<div class="left-panel"> |
|
<h2>音声アセット</h2> |
|
<div id="sound-assets"> |
|
<div class="sound-box" draggable="true" data-sound="p.mp3">p.mp3</div> |
|
<div class="sound-box" draggable="true" data-sound="a.mp3">a.mp3</div> |
|
<div class="sound-box" draggable="true" data-sound="t.mp3">t.mp3</div> |
|
<div class="sound-box" draggable="true" data-sound="s.mp3">s.mp3</div> |
|
</div> |
|
|
|
<h2>ドロップボックス</h2> |
|
<div id="drop-box" class="drop-box"> |
|
<p>音声ファイルをここにドラッグしてください</p> |
|
</div> |
|
</div> |
|
|
|
<div class="right-panel"> |
|
<h2>プレビュー</h2> |
|
<div class="video-container"> |
|
<video id="video" controls> |
|
<source src="v.mp4" type="video/mp4"> |
|
お使いのブラウザはビデオタグをサポートしていません。 |
|
</video> |
|
</div> |
|
|
|
<div class="controls"> |
|
<button id="play-btn">再生</button> |
|
<button id="pause-btn">一時停止</button> |
|
<button id="stop-btn">停止</button> |
|
<button id="reset-btn">リセット</button> |
|
</div> |
|
|
|
<div class="status" id="status"> |
|
準備ができました。音声をドロップボックスに追加してください。 |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="settings"> |
|
<h2>設定</h2> |
|
<div class="setting-group"> |
|
<div class="setting-item"> |
|
<label for="start-time">再生開始秒数 (秒)</label> |
|
<input type="number" id="start-time" min="0" value="0" step="0.1"> |
|
</div> |
|
<div class="setting-item"> |
|
<label for="end-time">再生終了秒数 (秒)</label> |
|
<input type="number" id="end-time" min="0" step="0.1"> |
|
</div> |
|
</div> |
|
|
|
<div class="setting-group"> |
|
<div class="setting-item"> |
|
<label for="volume">音量 (0-3)</label> |
|
<input type="range" id="volume" min="0" max="3" step="0.1" value="1"> |
|
<span id="volume-value">1</span> |
|
</div> |
|
<div class="setting-item"> |
|
<label for="playback-rate">再生速度 (0.5-2)</label> |
|
<input type="range" id="playback-rate" min="0.5" max="2" step="0.1" value="1"> |
|
<span id="playback-rate-value">1</span> |
|
</div> |
|
</div> |
|
|
|
<div class="setting-item"> |
|
<label> |
|
<input type="checkbox" id="loop-checkbox"> |
|
ループ再生 |
|
</label> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
document.addEventListener('DOMContentLoaded', function() { |
|
|
|
const soundAssets = document.getElementById('sound-assets'); |
|
const dropBox = document.getElementById('drop-box'); |
|
const video = document.getElementById('video'); |
|
const playBtn = document.getElementById('play-btn'); |
|
const pauseBtn = document.getElementById('pause-btn'); |
|
const stopBtn = document.getElementById('stop-btn'); |
|
const resetBtn = document.getElementById('reset-btn'); |
|
const statusDiv = document.getElementById('status'); |
|
|
|
|
|
const startTimeInput = document.getElementById('start-time'); |
|
const endTimeInput = document.getElementById('end-time'); |
|
const volumeInput = document.getElementById('volume'); |
|
const volumeValue = document.getElementById('volume-value'); |
|
const playbackRateInput = document.getElementById('playback-rate'); |
|
const playbackRateValue = document.getElementById('playback-rate-value'); |
|
const loopCheckbox = document.getElementById('loop-checkbox'); |
|
|
|
|
|
let audioContext; |
|
let audioBuffers = {}; |
|
let soundSources = []; |
|
let videoDuration = 0; |
|
|
|
|
|
init(); |
|
|
|
async function init() { |
|
try { |
|
|
|
video.addEventListener('loadedmetadata', function() { |
|
videoDuration = video.duration; |
|
endTimeInput.value = videoDuration.toFixed(1); |
|
endTimeInput.max = videoDuration; |
|
}); |
|
|
|
|
|
audioContext = new (window.AudioContext || window.webkitAudioContext)(); |
|
|
|
|
|
const soundElements = soundAssets.querySelectorAll('.sound-box'); |
|
for (const element of soundElements) { |
|
const soundFile = element.getAttribute('data-sound'); |
|
await loadSound(soundFile); |
|
} |
|
|
|
|
|
setupEventListeners(); |
|
|
|
statusDiv.textContent = "準備ができました。音声をドロップボックスに追加してください。"; |
|
} catch (error) { |
|
console.error("初期化エラー:", error); |
|
statusDiv.textContent = "初期化中にエラーが発生しました: " + error.message; |
|
} |
|
} |
|
|
|
function setupEventListeners() { |
|
|
|
dropBox.addEventListener('dragover', function(e) { |
|
e.preventDefault(); |
|
dropBox.classList.add('highlight'); |
|
}); |
|
|
|
dropBox.addEventListener('dragleave', function() { |
|
dropBox.classList.remove('highlight'); |
|
}); |
|
|
|
dropBox.addEventListener('drop', function(e) { |
|
e.preventDefault(); |
|
dropBox.classList.remove('highlight'); |
|
|
|
const soundFile = e.dataTransfer.getData('text/plain'); |
|
if (soundFile && soundFile.endsWith('.mp3')) { |
|
addSoundToDropBox(soundFile); |
|
} |
|
}); |
|
|
|
|
|
soundAssets.querySelectorAll('.sound-box').forEach(box => { |
|
box.addEventListener('dragstart', function(e) { |
|
e.dataTransfer.setData('text/plain', this.getAttribute('data-sound')); |
|
}); |
|
}); |
|
|
|
|
|
playBtn.addEventListener('click', playAll); |
|
pauseBtn.addEventListener('click', pauseAll); |
|
stopBtn.addEventListener('click', stopAll); |
|
resetBtn.addEventListener('click', resetAll); |
|
|
|
|
|
volumeInput.addEventListener('input', function() { |
|
volumeValue.textContent = this.value; |
|
}); |
|
|
|
playbackRateInput.addEventListener('input', function() { |
|
playbackRateValue.textContent = this.value; |
|
video.playbackRate = this.value; |
|
}); |
|
|
|
|
|
video.addEventListener('timeupdate', function() { |
|
const startTime = parseFloat(startTimeInput.value) || 0; |
|
const endTime = parseFloat(endTimeInput.value) || videoDuration; |
|
|
|
if (loopCheckbox.checked && video.currentTime >= endTime) { |
|
video.currentTime = startTime; |
|
restartAudio(startTime); |
|
} |
|
}); |
|
} |
|
|
|
async function loadSound(soundFile) { |
|
if (audioBuffers[soundFile]) return; |
|
|
|
try { |
|
const response = await fetch(soundFile); |
|
const arrayBuffer = await response.arrayBuffer(); |
|
audioBuffers[soundFile] = await audioContext.decodeAudioData(arrayBuffer); |
|
} catch (error) { |
|
console.error(`音声ファイルのロードエラー (${soundFile}):`, error); |
|
throw error; |
|
} |
|
} |
|
|
|
function addSoundToDropBox(soundFile) { |
|
|
|
if (dropBox.querySelector(`.sound-box[data-sound="${soundFile}"]`)) { |
|
statusDiv.textContent = `"${soundFile}" は既に追加されています。`; |
|
return; |
|
} |
|
|
|
|
|
const soundBox = document.createElement('div'); |
|
soundBox.className = 'sound-box'; |
|
soundBox.setAttribute('data-sound', soundFile); |
|
soundBox.textContent = soundFile; |
|
soundBox.draggable = true; |
|
|
|
|
|
soundBox.addEventListener('dragstart', function(e) { |
|
e.dataTransfer.setData('text/plain', this.getAttribute('data-sound')); |
|
e.dataTransfer.effectAllowed = 'move'; |
|
}); |
|
|
|
|
|
soundBox.addEventListener('dragover', function(e) { |
|
e.preventDefault(); |
|
e.dataTransfer.dropEffect = 'move'; |
|
}); |
|
|
|
|
|
soundBox.addEventListener('drop', function(e) { |
|
e.preventDefault(); |
|
const draggedSound = e.dataTransfer.getData('text/plain'); |
|
const draggedElement = dropBox.querySelector(`.sound-box[data-sound="${draggedSound}"]`); |
|
|
|
if (draggedElement && draggedElement !== this) { |
|
const dropY = e.clientY; |
|
const thisY = this.getBoundingClientRect().top; |
|
|
|
if (dropY < thisY) { |
|
dropBox.insertBefore(draggedElement, this); |
|
} else { |
|
dropBox.insertBefore(draggedElement, this.nextSibling); |
|
} |
|
} |
|
}); |
|
|
|
|
|
soundBox.addEventListener('dblclick', function() { |
|
this.remove(); |
|
statusDiv.textContent = `"${soundFile}" を削除しました。`; |
|
}); |
|
|
|
dropBox.appendChild(soundBox); |
|
statusDiv.textContent = `"${soundFile}" を追加しました。`; |
|
} |
|
|
|
function playAll() { |
|
const startTime = parseFloat(startTimeInput.value) || 0; |
|
const endTime = parseFloat(endTimeInput.value) || videoDuration; |
|
const volume = parseFloat(volumeInput.value) || 1; |
|
const playbackRate = parseFloat(playbackRateInput.value) || 1; |
|
|
|
|
|
video.currentTime = startTime; |
|
video.playbackRate = playbackRate; |
|
video.play().catch(e => console.error("動画再生エラー:", e)); |
|
|
|
|
|
stopAudio(); |
|
|
|
const soundBoxes = dropBox.querySelectorAll('.sound-box'); |
|
if (soundBoxes.length === 0) { |
|
statusDiv.textContent = "再生中 (音声なし)"; |
|
return; |
|
} |
|
|
|
let playTime = audioContext.currentTime; |
|
|
|
soundBoxes.forEach(box => { |
|
const soundFile = box.getAttribute('data-sound'); |
|
const audioBuffer = audioBuffers[soundFile]; |
|
|
|
if (audioBuffer) { |
|
const source = audioContext.createBufferSource(); |
|
const gainNode = audioContext.createGain(); |
|
|
|
source.buffer = audioBuffer; |
|
source.playbackRate.value = playbackRate; |
|
gainNode.gain.value = volume; |
|
|
|
source.connect(gainNode); |
|
gainNode.connect(audioContext.destination); |
|
|
|
source.start(playTime, startTime, endTime - startTime); |
|
|
|
soundSources.push({ |
|
source: source, |
|
gain: gainNode |
|
}); |
|
} |
|
}); |
|
|
|
statusDiv.textContent = `再生中 (${startTime.toFixed(1)}秒~${endTime.toFixed(1)}秒)`; |
|
} |
|
|
|
function pauseAll() { |
|
video.pause(); |
|
statusDiv.textContent = "一時停止中"; |
|
} |
|
|
|
function stopAll() { |
|
video.pause(); |
|
video.currentTime = parseFloat(startTimeInput.value) || 0; |
|
stopAudio(); |
|
statusDiv.textContent = "停止中"; |
|
} |
|
|
|
function resetAll() { |
|
video.pause(); |
|
video.currentTime = 0; |
|
stopAudio(); |
|
statusDiv.textContent = "リセットしました"; |
|
} |
|
|
|
function stopAudio() { |
|
soundSources.forEach(source => { |
|
try { |
|
source.source.stop(); |
|
} catch (e) { |
|
console.error("音声停止エラー:", e); |
|
} |
|
}); |
|
soundSources = []; |
|
} |
|
|
|
function restartAudio(startTime) { |
|
if (soundSources.length === 0) return; |
|
|
|
stopAudio(); |
|
|
|
const volume = parseFloat(volumeInput.value) || 1; |
|
const playbackRate = parseFloat(playbackRateInput.value) || 1; |
|
const endTime = parseFloat(endTimeInput.value) || videoDuration; |
|
|
|
let playTime = audioContext.currentTime; |
|
|
|
const soundBoxes = dropBox.querySelectorAll('.sound-box'); |
|
soundBoxes.forEach(box => { |
|
const soundFile = box.getAttribute('data-sound'); |
|
const audioBuffer = audioBuffers[soundFile]; |
|
|
|
if (audioBuffer) { |
|
const source = audioContext.createBufferSource(); |
|
const gainNode = audioContext.createGain(); |
|
|
|
source.buffer = audioBuffer; |
|
source.playbackRate.value = playbackRate; |
|
gainNode.gain.value = volume; |
|
|
|
source.connect(gainNode); |
|
gainNode.connect(audioContext.destination); |
|
|
|
source.start(playTime, startTime, endTime - startTime); |
|
|
|
soundSources.push({ |
|
source: source, |
|
gain: gainNode |
|
}); |
|
} |
|
}); |
|
} |
|
}); |
|
</script> |
|
</body> |
|
</html> |