|
<!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; |
|
} |
|
.container { |
|
max-width: 1000px; |
|
margin: 0 auto; |
|
} |
|
h1 { |
|
color: #64ffda; |
|
text-align: center; |
|
margin-bottom: 30px; |
|
font-size: 2.5em; |
|
} |
|
.player-section { |
|
display: flex; |
|
flex-wrap: wrap; |
|
gap: 20px; |
|
margin-bottom: 30px; |
|
} |
|
.video-container { |
|
flex: 1; |
|
min-width: 300px; |
|
background: #112240; |
|
border-radius: 8px; |
|
padding: 15px; |
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3); |
|
} |
|
video { |
|
width: 100%; |
|
border-radius: 4px; |
|
} |
|
.drop-area { |
|
flex: 1; |
|
min-width: 300px; |
|
background: #112240; |
|
border-radius: 8px; |
|
padding: 15px; |
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3); |
|
} |
|
.drop-box { |
|
border: 2px dashed #64ffda; |
|
border-radius: 8px; |
|
padding: 20px; |
|
text-align: center; |
|
min-height: 100px; |
|
margin-bottom: 15px; |
|
transition: all 0.3s; |
|
} |
|
.drop-box.highlight { |
|
background-color: rgba(100, 255, 218, 0.1); |
|
border-color: #ffffff; |
|
} |
|
.sound-item { |
|
background: #233554; |
|
padding: 10px; |
|
margin-bottom: 8px; |
|
border-radius: 4px; |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
} |
|
.sound-item button { |
|
background: #ff5555; |
|
border: none; |
|
color: white; |
|
border-radius: 4px; |
|
padding: 5px 10px; |
|
cursor: pointer; |
|
} |
|
.controls { |
|
background: #112240; |
|
border-radius: 8px; |
|
padding: 20px; |
|
margin-bottom: 20px; |
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3); |
|
} |
|
.control-group { |
|
margin-bottom: 15px; |
|
} |
|
label { |
|
display: block; |
|
margin-bottom: 5px; |
|
color: #64ffda; |
|
} |
|
input[type="range"], input[type="number"] { |
|
width: 100%; |
|
background: #233554; |
|
border: none; |
|
height: 8px; |
|
border-radius: 4px; |
|
outline: none; |
|
} |
|
input[type="range"]::-webkit-slider-thumb { |
|
-webkit-appearance: none; |
|
width: 16px; |
|
height: 16px; |
|
background: #64ffda; |
|
border-radius: 50%; |
|
cursor: pointer; |
|
} |
|
input[type="number"] { |
|
padding: 8px; |
|
height: auto; |
|
border-radius: 4px; |
|
background: #233554; |
|
border: 1px solid #405676; |
|
color: white; |
|
} |
|
button { |
|
background: #64ffda; |
|
color: #0a192f; |
|
border: none; |
|
padding: 10px 20px; |
|
border-radius: 4px; |
|
cursor: pointer; |
|
font-weight: bold; |
|
transition: all 0.3s; |
|
} |
|
button:hover { |
|
background: #52e3c2; |
|
transform: translateY(-2px); |
|
} |
|
.button-group { |
|
display: flex; |
|
gap: 10px; |
|
margin-top: 15px; |
|
} |
|
.tech-decoration { |
|
height: 2px; |
|
background: linear-gradient(90deg, #64ffda, #0a192f); |
|
margin: 20px 0; |
|
position: relative; |
|
} |
|
.tech-decoration::after { |
|
content: ""; |
|
position: absolute; |
|
top: -3px; |
|
right: 0; |
|
width: 8px; |
|
height: 8px; |
|
background: #64ffda; |
|
border-radius: 50%; |
|
} |
|
.hidden { |
|
display: none; |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<div class="container"> |
|
<h1>音声合成プレイヤー</h1> |
|
|
|
<div class="tech-decoration"></div> |
|
|
|
<div class="player-section"> |
|
<div class="video-container"> |
|
<h2>動画プレビュー</h2> |
|
<video id="video" controls> |
|
<source src="v.mp4" type="video/mp4"> |
|
お使いのブラウザは動画をサポートしていません。 |
|
</video> |
|
</div> |
|
|
|
<div class="drop-area"> |
|
<h2>音声ドロップエリア</h2> |
|
<div class="drop-box" id="dropBox"> |
|
<p>音声ファイルをここにドラッグ&ドロップしてください</p> |
|
<p>(p.mp3, a.mp3, t.mp3, s.mp3)</p> |
|
</div> |
|
<div id="soundList"></div> |
|
</div> |
|
</div> |
|
|
|
<div class="tech-decoration"></div> |
|
|
|
<div class="controls"> |
|
<h2>設定メニュー</h2> |
|
|
|
<div class="control-group"> |
|
<label for="startTime">再生開始秒数 (秒)</label> |
|
<input type="number" id="startTime" min="0" value="0" step="0.1"> |
|
</div> |
|
|
|
<div class="control-group"> |
|
<label for="endTime">再生終了秒数 (秒)</label> |
|
<input type="number" id="endTime" min="0" step="0.1"> |
|
</div> |
|
|
|
<div class="control-group"> |
|
<label for="volume">音量 (0-3)</label> |
|
<input type="range" id="volume" min="0" max="3" step="0.1" value="1"> |
|
<span id="volumeValue">1</span> |
|
</div> |
|
|
|
<div class="control-group"> |
|
<label for="playbackRate">再生速度 (0.5-2)</label> |
|
<input type="range" id="playbackRate" min="0.5" max="2" step="0.1" value="1"> |
|
<span id="playbackRateValue">1</span> |
|
</div> |
|
|
|
<div class="control-group"> |
|
<label> |
|
<input type="checkbox" id="loopCheckbox"> |
|
ループ再生 |
|
</label> |
|
</div> |
|
|
|
<div class="button-group"> |
|
<button id="playButton">再生</button> |
|
<button id="pauseButton">一時停止</button> |
|
<button id="stopButton">停止</button> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
|
|
const dropBox = document.getElementById('dropBox'); |
|
const soundList = document.getElementById('soundList'); |
|
const video = document.getElementById('video'); |
|
const startTimeInput = document.getElementById('startTime'); |
|
const endTimeInput = document.getElementById('endTime'); |
|
const volumeInput = document.getElementById('volume'); |
|
const volumeValue = document.getElementById('volumeValue'); |
|
const playbackRateInput = document.getElementById('playbackRate'); |
|
const playbackRateValue = document.getElementById('playbackRateValue'); |
|
const loopCheckbox = document.getElementById('loopCheckbox'); |
|
const playButton = document.getElementById('playButton'); |
|
const pauseButton = document.getElementById('pauseButton'); |
|
const stopButton = document.getElementById('stopButton'); |
|
|
|
|
|
let audioContext; |
|
let audioBuffers = {}; |
|
let soundSources = []; |
|
let videoDuration = 0; |
|
|
|
|
|
const allowedSounds = ['p.mp3', 'a.mp3', 't.mp3', 's.mp3']; |
|
|
|
|
|
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { |
|
dropBox.addEventListener(eventName, preventDefaults, false); |
|
}); |
|
|
|
function preventDefaults(e) { |
|
e.preventDefault(); |
|
e.stopPropagation(); |
|
} |
|
|
|
['dragenter', 'dragover'].forEach(eventName => { |
|
dropBox.addEventListener(eventName, highlight, false); |
|
}); |
|
|
|
['dragleave', 'drop'].forEach(eventName => { |
|
dropBox.addEventListener(eventName, unhighlight, false); |
|
}); |
|
|
|
function highlight() { |
|
dropBox.classList.add('highlight'); |
|
} |
|
|
|
function unhighlight() { |
|
dropBox.classList.remove('highlight'); |
|
} |
|
|
|
dropBox.addEventListener('drop', handleDrop, false); |
|
|
|
function handleDrop(e) { |
|
const dt = e.dataTransfer; |
|
const files = dt.files; |
|
|
|
handleFiles(files); |
|
} |
|
|
|
function handleFiles(files) { |
|
[...files].forEach(file => { |
|
if (allowedSounds.includes(file.name)) { |
|
addSound(file.name); |
|
} |
|
}); |
|
} |
|
|
|
|
|
function addSound(filename) { |
|
|
|
if (audioBuffers[filename]) { |
|
alert(`${filename} は既に追加されています`); |
|
return; |
|
} |
|
|
|
|
|
const soundItem = document.createElement('div'); |
|
soundItem.className = 'sound-item'; |
|
soundItem.innerHTML = ` |
|
${filename} |
|
<button data-filename="${filename}">削除</button> |
|
`; |
|
soundList.appendChild(soundItem); |
|
|
|
|
|
soundItem.querySelector('button').addEventListener('click', function() { |
|
removeSound(filename); |
|
}); |
|
|
|
|
|
loadSound(filename); |
|
} |
|
|
|
|
|
function removeSound(filename) { |
|
|
|
const items = document.querySelectorAll('.sound-item'); |
|
items.forEach(item => { |
|
if (item.querySelector('button').dataset.filename === filename) { |
|
item.remove(); |
|
} |
|
}); |
|
|
|
|
|
delete audioBuffers[filename]; |
|
} |
|
|
|
|
|
async function loadSound(filename) { |
|
if (!audioContext) { |
|
audioContext = new (window.AudioContext || window.webkitAudioContext)(); |
|
} |
|
|
|
try { |
|
const response = await fetch(filename); |
|
const arrayBuffer = await response.arrayBuffer(); |
|
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer); |
|
audioBuffers[filename] = audioBuffer; |
|
|
|
|
|
if (Object.keys(audioBuffers).length === 1) { |
|
videoDuration = audioBuffer.duration; |
|
endTimeInput.value = videoDuration.toFixed(1); |
|
} |
|
} catch (error) { |
|
console.error('音声の読み込みに失敗しました:', error); |
|
alert(`音声 ${filename} の読み込みに失敗しました`); |
|
removeSound(filename); |
|
} |
|
} |
|
|
|
|
|
video.addEventListener('loadedmetadata', function() { |
|
if (video.duration !== Infinity) { |
|
videoDuration = video.duration; |
|
endTimeInput.value = videoDuration.toFixed(1); |
|
} |
|
video.muted = true; |
|
}); |
|
|
|
|
|
volumeInput.addEventListener('input', function() { |
|
volumeValue.textContent = this.value; |
|
}); |
|
|
|
|
|
playbackRateInput.addEventListener('input', function() { |
|
playbackRateValue.textContent = this.value; |
|
}); |
|
|
|
|
|
playButton.addEventListener('click', function() { |
|
const startTime = parseFloat(startTimeInput.value); |
|
const endTime = parseFloat(endTimeInput.value); |
|
const volume = parseFloat(volumeInput.value); |
|
const playbackRate = parseFloat(playbackRateInput.value); |
|
const loop = loopCheckbox.checked; |
|
|
|
|
|
if (startTime >= endTime) { |
|
alert('終了時間は開始時間より大きくしてください'); |
|
return; |
|
} |
|
|
|
if (Object.keys(audioBuffers).length === 0) { |
|
alert('音声が追加されていません'); |
|
return; |
|
} |
|
|
|
|
|
stopAllSounds(); |
|
|
|
|
|
video.currentTime = startTime; |
|
video.playbackRate = playbackRate; |
|
video.play(); |
|
|
|
|
|
for (const filename in audioBuffers) { |
|
playSound(filename, startTime, endTime, volume, playbackRate, loop); |
|
} |
|
}); |
|
|
|
|
|
function playSound(filename, startTime, endTime, volume, playbackRate, loop) { |
|
if (!audioContext) { |
|
audioContext = new (window.AudioContext || window.webkitAudioContext)(); |
|
} |
|
|
|
const source = audioContext.createBufferSource(); |
|
source.buffer = audioBuffers[filename]; |
|
source.playbackRate.value = playbackRate; |
|
|
|
const gainNode = audioContext.createGain(); |
|
gainNode.gain.value = volume; |
|
|
|
source.connect(gainNode); |
|
gainNode.connect(audioContext.destination); |
|
|
|
source.start(0, startTime % audioBuffers[filename].duration); |
|
|
|
if (loop) { |
|
source.loop = true; |
|
source.loopStart = startTime % audioBuffers[filename].duration; |
|
source.loopEnd = endTime % audioBuffers[filename].duration; |
|
} else { |
|
source.stop(audioContext.currentTime + (endTime - startTime)); |
|
} |
|
|
|
soundSources.push(source); |
|
} |
|
|
|
|
|
pauseButton.addEventListener('click', function() { |
|
video.pause(); |
|
stopAllSounds(); |
|
}); |
|
|
|
|
|
stopButton.addEventListener('click', function() { |
|
video.pause(); |
|
video.currentTime = parseFloat(startTimeInput.value); |
|
stopAllSounds(); |
|
}); |
|
|
|
|
|
function stopAllSounds() { |
|
soundSources.forEach(source => { |
|
try { |
|
source.stop(); |
|
} catch (e) { |
|
console.log('音声ソースは既に停止しています'); |
|
} |
|
}); |
|
soundSources = []; |
|
} |
|
|
|
|
|
video.addEventListener('timeupdate', function() { |
|
const endTime = parseFloat(endTimeInput.value); |
|
const loop = loopCheckbox.checked; |
|
|
|
if (!loop && video.currentTime >= endTime) { |
|
video.pause(); |
|
stopAllSounds(); |
|
} else if (loop && video.currentTime >= endTime) { |
|
video.currentTime = parseFloat(startTimeInput.value); |
|
} |
|
}); |
|
</script> |
|
</body> |
|
</html> |