soiz1's picture
Update index.html
73384c6
raw
history blame
16.4 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;
}
.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>