soiz1's picture
Update index.html
ddace5e
raw
history blame
22 kB
<!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>