soiz1's picture
Update index.html
26f8785
raw
history blame
74.3 kB
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<script src="https://soiz1-eruda3.hf.space/eruda.js"></script><script>eruda.init();</script>
<link rel="icon" type="image/svg+xml" href='data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgdmlld0JveD0iMCAwIDIwMCAyMDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgbGFuZz0iamEiPjxkZWZzPiA8bGluZWFyR3JhZGllbnQgaWQ9ImdyYWQxIiB4MT0iMCUiIHkxPSIwJSIgeDI9IjAlIiB5Mj0iMTAwJSI+IDxzdG9wIG9mZnNldD0iMCUiIHN0b3AtY29sb3I9IiMzNDc0ZWIiIC8+IDxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzM0YThlYiIgLz4gPC9saW5lYXJHcmFkaWVudD4gPC9kZWZzPiA8IS0tIOWQhOS6uuWbvuWkjeS9nOWumuS7tuWkjeaWsOWIl+OBjOaWsOWIl+OBq+OBhOOBq+OBvuOBq+OCi+OCieWbvuWkjeOBjOOCjOOCieOCk+ODq+ODvOODrOODs+ODg+OCiwotLT4gPHBvbHlnb24gcG9pbnRzPSI1MCw1MCA1MCwxNTAgMTUwLDEwMCIgZmlsbD0idXJsKCNncmFkMSkiIHN0cm9rZT0iYmxhY2siIHN0cm9rZS13aWR0aD0iMiIgLz48L3N2Zz4=' />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>高度な音声動画プレイヤー</title>
<style>
/* テクノロジー風背景スタイル */
.tech-background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -2;
background: linear-gradient(135deg, #0f0c29, #302b63, #24243e);
overflow: hidden;
}
.circuit-line {
position: absolute;
background: rgba(0, 255, 255, 0.1);
box-shadow: 0 0 10px rgba(0, 255, 255, 0.3);
}
.grid-dot {
position: absolute;
width: 2px;
height: 2px;
background: rgba(0, 255, 255, 0.3);
border-radius: 50%;
}
.hexagon {
position: absolute;
width: 40px;
height: 23px;
background: rgba(0, 255, 255, 0.05);
border: 1px solid rgba(0, 255, 255, 0.1);
box-shadow: 0 0 5px rgba(0, 255, 255, 0.2);
}
.hexagon:before, .hexagon:after {
content: "";
position: absolute;
width: 0;
border-left: 20px solid transparent;
border-right: 20px solid transparent;
}
.hexagon:before {
bottom: 100%;
border-bottom: 11.5px solid rgba(0, 255, 255, 0.05);
}
.hexagon:after {
top: 100%;
width: 0;
border-top: 11.5px solid rgba(0, 255, 255, 0.05);
}
.pulse {
position: absolute;
width: 10px;
height: 10px;
background: rgba(0, 255, 255, 0.7);
border-radius: 50%;
box-shadow: 0 0 10px 5px rgba(0, 255, 255, 0.5);
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { transform: scale(0.8); opacity: 0.7; }
50% { transform: scale(1.2); opacity: 1; }
100% { transform: scale(0.8); opacity: 0.7; }
}
@keyframes float {
0% { transform: translateY(0) rotate(0deg); }
50% { transform: translateY(-20px) rotate(5deg); }
100% { transform: translateY(0) rotate(0deg); }
}
/* メインスタイル */
body {
font-family: 'Arial', sans-serif;
background-color: rgba(10, 25, 47, 0.8);
color: #e6f1ff;
margin: 0;
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
}
h1 {
color: #64ffda;
text-align: center;
margin-bottom: 30px;
border-bottom: 1px solid #64ffda;
padding-bottom: 10px;
width: 100%;
}
.container {
display: flex;
flex-direction: column;
width: 100%;
max-width: 800px;
background-color: rgba(17, 34, 64, 0.5);
border-radius: 10px;
padding: 20px;
box-shadow: 0 0 20px rgba(100, 255, 218, 0.2);
backdrop-filter: blur(5px);
border: 1px solid rgba(100, 255, 218, 0.1);
}
.video-container {
position: relative;
width: 100%;
margin-bottom: 20px;
}
video {
width: 100%;
border-radius: 5px;
background-color: #000;
display: block;
cursor: pointer;
}
.video-controls {
background-color: rgba(17, 34, 64, 0.6);
padding: 10px;
border-radius: 0 0 5px 5px;
display: flex;
flex-direction: column;
gap: 10px;
transition: opacity 0.3s;
}
.progress-container {
width: 100%;
height: 10px;
background-color: #1e2a47;
border-radius: 5px;
cursor: pointer;
position: relative;
}
.progress-bar {
height: 100%;
background-color: #64ffda;
border-radius: 5px;
width: 0%;
position: relative;
}
.progress-time {
position: absolute;
top: -25px;
transform: translateX(-50%);
background-color: rgba(30, 42, 71, 0.9);
padding: 3px 6px;
border-radius: 3px;
font-size: 12px;
display: none;
white-space: nowrap;
}
/* プログレスバーのマーカー */
.progress-marker {
position: absolute;
bottom: -5px;
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 10px solid #ff5555;
transform: translateX(-50%);
z-index: 2;
}
.main-controls {
display: flex;
align-items: center;
gap: 15px;
}
.control-button {
background: none;
border: none;
color: #e6f1ff;
font-size: 18px;
cursor: pointer;
padding: 5px;
border-radius: 50%;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.3s;
}
.control-button:hover {
background-color: rgba(100, 255, 218, 0.2);
}
.time-display {
font-size: 14px;
color: #ccd6f6;
white-space: nowrap;
}
.volume-control {
display: flex;
align-items: center;
gap: 5px;
margin-left: auto;
}
.volume-button {
background: none;
border: none;
color: #e6f1ff;
font-size: 18px;
cursor: pointer;
padding: 5px;
}
.volume-slider {
width: 80px;
height: 6px;
-webkit-appearance: none;
background: #1e2a47;
border-radius: 3px;
outline: none;
opacity: 0;
transition: opacity 0.3s, width 0.3s;
background-image: linear-gradient(#64ffda, #64ffda);
background-size: 100% 100%;
background-repeat: no-repeat;
}
.volume-control:hover .volume-slider {
opacity: 1;
width: 100px;
}
.volume-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 12px;
height: 12px;
background: #64ffda;
border-radius: 50%;
cursor: pointer;
}
.speed-control {
display: flex;
align-items: center;
gap: 5px;
}
.speed-slider {
width: 80px;
height: 6px;
-webkit-appearance: none;
background: #1e2a47;
border-radius: 3px;
outline: none;
background-image: linear-gradient(#64ffda, #64ffda);
background-size: 100% 100%;
background-repeat: no-repeat;
}
.speed-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 12px;
height: 12px;
background: #64ffda;
border-radius: 50%;
cursor: pointer;
}
.speed-value {
font-size: 14px;
min-width: 30px;
text-align: center;
}
.fullscreen-button {
margin-left: 10px;
}
.audio-controls {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 15px;
}
.audio-item {
display: flex;
align-items: center;
gap: 10px;
}
.audio-item label {
min-width: 50px;
color: #64ffda;
}
.audio-slider {
flex-grow: 1;
height: 8px;
-webkit-appearance: none;
background: #1e2a47;
border-radius: 5px;
outline: none;
background-image: linear-gradient(#64ffda, #64ffda);
background-size: 100% 100%;
background-repeat: no-repeat;
}
.audio-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 18px;
height: 18px;
background: #64ffda;
border-radius: 50%;
cursor: pointer;
}
.settings {
background-color: rbga(30, 42, 71, 0.3);
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}
.setting-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.setting-item:last-child {
margin-bottom: 0;
}
.setting-item label {
color: #ccd6f6;
}
.global-volume-container, .playback-speed-container {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
}
.global-volume-slider, .playback-speed-slider {
flex-grow: 1;
height: 8px;
-webkit-appearance: none;
background: #1e2a47;
border-radius: 5px;
outline: none;
background-image: linear-gradient(#64ffda, #64ffda);
background-size: 100% 100%;
background-repeat: no-repeat;
}
.global-volume-slider::-webkit-slider-thumb,
.playback-speed-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 16px;
height: 16px;
background: #64ffda;
border-radius: 50%;
cursor: pointer;
}
.slider-value {
min-width: 40px;
text-align: right;
}
input[type="number"], input[type="checkbox"], select {
background-color: #112240;
border: 1px solid #64ffda;
color: #e6f1ff;
padding: 5px;
border-radius: 3px;
}
.tech-decoration {
width: 100%;
height: 2px;
background: linear-gradient(90deg, transparent, #64ffda, transparent);
margin: 20px 0;
}
/* 全画面時のスタイル */
.video-container:-webkit-full-screen {
width: 100%;
height: 100%;
background-color: black;
}
.video-container:-webkit-full-screen video {
width: 100%;
height: 100%;
}
.video-container:-webkit-full-screen .video-controls {
position: fixed;
bottom: 0;
left: 0;
right: 0;
width: 100%;
border-radius: 0;
}
/* ローディングアニメーション */
.loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
transition: opacity 1s ease-out;
}
.spinner-box {
width: 300px;
height: 300px;
display: flex;
justify-content: center;
align-items: center;
background-color: transparent;
}
/* 軌道スタイル */
.leo {
position: absolute;
display: flex;
justify-content: center;
align-items: center;
border-radius: 50%;
}
.blue-orbit {
width: 165px;
height: 165px;
border: 1px solid #91daffa5;
animation: spin3D 3s linear .2s infinite;
}
.green-orbit {
width: 120px;
height: 120px;
border: 1px solid #91ffbfa5;
animation: spin3D 2s linear 0s infinite;
}
.red-orbit {
width: 90px;
height: 90px;
border: 1px solid #ffca91a5;
animation: spin3D 1s linear 0s infinite;
}
.white-orbit {
width: 60px;
height: 60px;
border: 2px solid #ffffff;
animation: spin3D 10s linear 0s infinite;
}
.w1 {
transform: rotate3D(1, 1, 1, 90deg);
}
.w2 {
transform: rotate3D(1, 2, .5, 90deg);
}
.w3 {
transform: rotate3D(.5, 1, 2, 90deg);
}
/* キーフレームアニメーション */
@keyframes spin3D {
from {
transform: rotate3d(.5,.5,.5, 360deg);
}
to {
transform: rotate3d(0,0,0, 0deg);
}
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.time-set-button {
background-color: #112240;
border: 1px solid #64ffda;
color: #e6f1ff;
padding: 5px 10px;
border-radius: 3px;
cursor: pointer;
font-size: 12px;
margin-left: 5px;
transition: background-color 0.3s;
}
.time-set-button:hover {
background-color: rgba(100, 255, 218, 0.2);
}
/* 合成ボタンスタイル */
.combine-button {
background-color: #64ffda;
color: #0a192f;
border: none;
padding: 10px 20px;
border-radius: 5px;
font-size: 16px;
cursor: pointer;
margin-top: 20px;
transition: all 0.3s;
font-weight: bold;
}
.combine-button:hover {
background-color: #52e0c4;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(100, 255, 218, 0.4);
}
.combine-button:disabled {
background-color: #3a5a78;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
/* 合成ステータスメッセージ */
.combine-status {
margin-top: 10px;
color: #64ffda;
font-size: 14px;
height: 20px;
}
/* プレビューセクション */
.preview-section {
margin-top: 20px;
padding: 15px;
background-color: rgba(17, 34, 64, 0.7);
border-radius: 5px;
display: none;
}
.preview-section h3 {
margin-top: 0;
color: #64ffda;
border-bottom: 1px solid #64ffda;
padding-bottom: 5px;
}
/* 無効状態のオーバーレイ */
.disabled-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(10, 25, 47, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 10;
border-radius: 5px;
}
.disabled-message {
background-color: rgba(30, 42, 71, 0.9);
padding: 20px;
border-radius: 5px;
text-align: center;
max-width: 80%;
}
.disabled-message p {
margin-bottom: 15px;
}
/* 新しいローダースタイル */
.loader {
width: 80px;
aspect-ratio: 1;
border: 10px solid #000;
box-sizing: border-box;
background:
radial-gradient(farthest-side,#fff 98%,#0000) 50%/20px 20px,
radial-gradient(farthest-side,#fff 98%,#0000) 50%/20px 20px,
radial-gradient(farthest-side,#fff 98%,#0000) 50%/20px 20px,
radial-gradient(farthest-side,#fff 98%,#0000) 50%/20px 20px,
radial-gradient(farthest-side,#fff 98%,#0000) 50%/80% 80%,
#000;
background-repeat: no-repeat;
filter: blur(4px) contrast(10);
animation: squarePulse 1s infinite alternate;
}
@keyframes squarePulse {
0% {
background-position:
50% 50%, 50% 50%, 50% 50%, 50% 50%, 50% 50%, 50% 50%;
}
25% {
background-position:
50% 0, 50% 50%, 50% 50%, 50% 50%, 50% 50%, 50% 50%;
}
50% {
background-position:
50% 0, 50% 100%, 50% 50%, 50% 50%, 50% 50%, 50% 50%;
}
75% {
background-position:
50% 0, 50% 100%, 0 50%, 50% 50%, 50% 50%, 50% 50%;
}
100% {
background-position:
50% 0, 50% 100%, 0 50%, 100% 50%, 50% 50%, 50% 50%;
}
}
#buffering-indicator {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 10;
display: none;
}
.sync-status {
position: absolute;
bottom: 100px;
left: 10px;
width: 150px; /* 固定幅 */
height: 30px; /* 固定高さ */
background-color: rgba(0, 0, 0, 0.7);
color: #64ffda;
padding: 5px 10px;
border-radius: 3px;
font-size: 12px;
z-index: 5;
display: flex;
align-items: center;
gap: 5px;
white-space: nowrap; /* 文字の折り返しを防止 */
overflow: hidden; /* 内容がはみ出す場合に隠す */
text-overflow: ellipsis; /* はみ出したテキストに「…」を表示(任意) */
user-select: none; /* テキストの選択を不可にする */
/* はみ出してもスクロールバーを出さない */
contain: strict; /* レイアウトの影響を最小限にする */
}
.sync-status button {
background: none;
border: none;
color: #fff;
cursor: pointer;
font-size: 12px;
}
.lock-controls-btn {
position: fixed;
bottom: 20px;
right: 20px;
background-color: rgba(0, 0, 0, 0.7);
border: none;
color: #fff;
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
z-index: 100;
display: none;
}
.lock-controls-btn.locked {
color: #64ffda;
}
</style>
</head>
<body>
<script>
// 動画パスを再設定
document.addEventListener('DOMContentLoaded', function() {
const urlParams = new URLSearchParams(window.location.search);
const isTMode = urlParams.has('mode') && urlParams.get('mode') === 't';
const videoSource = document.querySelector('#video source');
if (isTMode && videoSource) {
videoSource.src = '/t/v.mp4';
const video = document.getElementById('video');
video.load();
}
});
</script>
<!-- テクノロジー風背景 -->
<div class="tech-background" id="techBg"></div>
<!-- ローディングオーバーレイ -->
<div class="loading-overlay" id="loadingOverlay">
<div class="spinner-box">
<div class="blue-orbit leo"></div>
<div class="green-orbit leo"></div>
<div class="red-orbit leo"></div>
<div class="white-orbit w1 leo"></div>
<div class="white-orbit w2 leo"></div>
<div class="white-orbit w3 leo"></div>
</div>
</div>
<h1>高度な音声動画プレイヤー</h1>
<div class="settings">
<h2>サービスワーカー設定</h2>
<div class="setting-item">
<label><input type="checkbox" id="sw-video" checked> 動画ファイル (/v.mp4)</label>
</div>
<div class="setting-item">
<label><input type="checkbox" id="sw-t-video" checked> 動画ファイル (/t/v.mp4)</label>
</div><br>
<div class="setting-item">
<label><input type="checkbox" id="sw-piano" checked> ピアノ音声 (/p.mp3)</label>
</div>
<div class="setting-item">
<label><input type="checkbox" id="sw-soprano" checked> ソプラノ音声 (/s.mp3)</label>
</div>
<div class="setting-item">
<label><input type="checkbox" id="sw-alto" checked> アルト音声 (/a.mp3)</label>
</div>
<div class="setting-item">
<label><input type="checkbox" id="sw-tenor" checked> テノール音声 (/t.mp3)</label>
</div>
<div class="setting-item">
<label><input type="checkbox" id="sw-combined" checked> 全体音声 (/k.mp3)</label>
</div><br>
<div class="setting-item">
<label><input type="checkbox" id="sw-t-piano" checked> ピアノ音声 (/t/p.mp3)</label>
</div>
<div class="setting-item">
<label><input type="checkbox" id="sw-t-soprano" checked> ソプラノ音声 (/t/s.mp3)</label>
</div>
<div class="setting-item">
<label><input type="checkbox" id="sw-t-alto" checked> アルト音声 (/t/a.mp3)</label>
</div>
<div class="setting-item">
<label><input type="checkbox" id="sw-t-tenor" checked> テノール音声 (/t/t.mp3)</label>
</div>
<div class="setting-item">
<label><input type="checkbox" id="sw-t-combined" checked> 全体音声 (/t/k.mp3)</label>
</div><br>
<div class="setting-item">
<label><input type="checkbox" id="sw-index" checked> インデックスファイル (/index.html)</label>
</div>
<div class="setting-item">
<label><input type="checkbox" id="sw-root" checked> ルートファイル (/)</label>
</div>
<button class="combine-button" id="sw-register-btn">登録を開始</button>
<div class="combine-status" id="sw-status"></div>
<script>
document.getElementById('sw-register-btn').addEventListener('click', async () => {
const statusElem = document.getElementById('sw-status');
function updateStatus(message) {
console.log(message);
statusElem.textContent = message;
}
function updateError(message, error) {
const errorMsg = `${message}\nエラー詳細: ${error?.message || error}`;
console.error(errorMsg, error);
statusElem.textContent = errorMsg;
}
if (!('serviceWorker' in navigator)) {
updateError('このブラウザはService Workerに対応していません。');
return;
}
try {
updateStatus('既存のService Workerをアンレジスター中...');
const registrations = await navigator.serviceWorker.getRegistrations();
for (const registration of registrations) {
try {
await registration.unregister();
updateStatus('Service Workerをアンレジスターしました。');
} catch (unregErr) {
updateError('Service Workerのアンレジスター中に例外が発生しました。', unregErr);
return;
}
}
updateStatus('アンレジスター完了。新しいService Workerを登録中...');
setTimeout(async () => {
try {
const newRegistration = await navigator.serviceWorker.register('/sw.js');
updateStatus('Service Workerが正常に登録されました。\n登録範囲: ' + newRegistration.scope);
// チェックボックスの状態からファイルリストを作成
const fileMap = {
'sw-video': '/v.mp4',
'sw-t-video': '/t/v.mp4',
'sw-piano': '/p.mp3',
'sw-soprano': '/s.mp3',
'sw-alto': '/a.mp3',
'sw-tenor': '/t.mp3',
'sw-combined': '/k.mp3',
'sw-t-piano': '/t/p.mp3',
'sw-t-soprano': '/t/s.mp3',
'sw-t-alto': '/t/a.mp3',
'sw-t-tenor': '/t/t.mp3',
'sw-t-combined': '/t/k.mp3',
'sw-index': '/index.html',
'sw-root': '/',
};
// チェックされているものだけ抽出
const filesToCache = [];
const checkboxStates = {};
for (const [id, url] of Object.entries(fileMap)) {
const checkbox = document.getElementById(id);
if (!checkbox) continue;
checkboxStates[id] = checkbox.checked;
if (checkbox.checked) {
filesToCache.push(url);
}
}
// SWがコントロール中ならメッセージ送信
if (navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage({
type: 'CACHE_FILES',
files: filesToCache,
checkboxStates: checkboxStates,
});
updateStatus('キャッシュ更新のメッセージをサービスワーカーに送信しました。');
} else {
updateStatus('サービスワーカーがコントロール中ではありません。キャッシュ更新メッセージは送信できません。');
}
} catch (regErr) {
updateError('Service Workerの登録に失敗しました。', regErr);
}
}, 500);
} catch (err) {
updateError('Service Workerの処理中に致命的なエラーが発生しました。', err);
}
});
</script>
</div>
<div class="container">
<div class="video-container" id="video-container">
<!-- バッファリングインジケーター -->
<div id="buffering-indicator"><div class="loader"></div></div>
<!-- 同期ステータス -->
<div class="sync-status" id="sync-status">
<span id="sync-status-text"></span>
<button id="sync-status-close">×</button>
</div>
<script src="https://unpkg.com/draggabilly@2/dist/draggabilly.pkgd.min.js"></script>
<script>new Draggabilly('#sync-status');</script>
<!-- 無効状態のオーバーレイ -->
<div class="disabled-overlay" id="disabledOverlay">
<div class="disabled-message">
<p>音声の合成が完了していません</p>
<p>下の音声コントロールで各パートの音量を調整し、「合成」ボタンを押してください</p>
</div>
</div>
<video id="video" muted>
<source src="v.mp4" type="video/mp4" allowPictureInPicture>
</video>
<div class="video-controls">
<div class="progress-container" id="progress-container">
<div class="progress-bar" id="progress-bar"></div>
<div class="progress-time" id="progress-time">00:00</div>
<div class="progress-marker" id="start-marker" style="left: 0%; display: none;"></div>
<div class="progress-marker" id="end-marker" style="left: 100%; display: none;"></div>
</div>
<div class="main-controls">
<button class="control-button" id="play-pause-btn" disabled></button>
<div class="time-display" id="time-display">00:00.00 / 00:00.00</div>
<div class="volume-control">
<button class="volume-button" id="volume-btn" disabled>🔊</button>
<input type="range" class="volume-slider" id="volume-slider" min="0" max="1" step="0.01" value="1" disabled>
</div>
<div class="speed-control">
<span class="speed-value" id="speed-value">1.00x</span>
<input type="range" class="speed-slider" id="speed-slider" min="0.01" max="5" step="0.01" value="1" disabled>
</div>
<button class="control-button fullscreen-button" id="fullscreen-btn" disabled></button>
</div>
</div>
</div>
<div class="tech-decoration"></div>
<div class="settings">
<h2>設定</h2>
<div class="setting-item">
<label for="start-time">再生開始秒数:</label>
<div>
<input type="number" id="start-time" min="0" value="0" step="0.01" disabled>
<button class="time-set-button" id="set-start-time" disabled>現在の秒数に設定</button>
</div>
</div>
<div class="setting-item">
<label for="end-time">再生終了秒数:</label>
<div>
<input type="number" id="end-time" min="0" value="0" step="0.01" disabled>
<button class="time-set-button" id="set-end-time" disabled>現在の秒数に設定</button>
<button class="time-set-button" id="reset-end-time" disabled>動画の長さに戻す</button>
</div>
</div>
<div class="setting-item">
<label for="loop">ループ再生:</label>
<input type="checkbox" id="loop" disabled>
</div>
<div class="setting-item">
<div class="global-volume-container">
<label>全体音量係数:</label>
<input type="range" class="global-volume-slider" id="global-volume" min="0" max="10" step="0.01" value="5" disabled>
<span class="slider-value" id="global-volume-value">0.5</span>
</div>
</div>
<div class="setting-item">
<label for="tempo">テンポ (BPM):</label>
<input type="number" id="tempo" min="40" max="200" value="92" step="0.1">
<span id="tempo-speed-value">1.00x</span>
</div>
<div class="setting-item">
<div class="playback-speed-container">
<label>再生速度:</label>
<input type="range" class="playback-speed-slider" id="playback-speed" min="0.01" max="5" step="0.001" value="1" disabled>
<span class="slider-value" id="playback-speed-value">1.00x</span>
</div>
</div>
</div>
<div class="tech-decoration"></div>
<div class="audio-controls">
<h2>音声コントロール</h2>
<div class="audio-item">
<label>ピアノ</label>
<input type="range" class="audio-slider" data-audio="p" min="0" max="1" step="0.01" value="0">
<span class="slider-value volume-value">0.00</span>
</div>
<div class="audio-item">
<label>ソプラノ</label>
<input type="range" class="audio-slider" data-audio="s" min="0" max="1" step="0.01" value="1">
<span class="slider-value volume-value">1.00</span>
</div>
<div class="audio-item">
<label>アルト</label>
<input type="range" class="audio-slider" data-audio="a" min="0" max="1" step="0.01" value="1">
<span class="slider-value volume-value">1.00</span>
</div>
<div class="audio-item">
<label>テノール</label>
<input type="range" class="audio-slider" data-audio="t" min="0" max="1" step="0.01" value="1">
<span class="slider-value volume-value">1.00</span>
</div>
<div class="audio-item">
<label>全体(非推奨)</label>
<input type="range" class="audio-slider" data-audio="k" min="0" max="1" step="0.01" value="0">
<span class="slider-value volume-value">0.00</span>
</div>
<!-- 合成ボタンとステータス -->
<button class="combine-button" id="combine-button">音声を合成</button>
<div class="combine-status" id="combine-status"></div>
</div>
<!-- プレビューセクション -->
<div class="preview-section" id="preview-section">
<h3 hidden>プレビュー</h3>
<p hidden>合成された音声をプレビューできます。再生ボタンをクリックして確認してください。</p>
<button class="control-button" id="preview-button" hidden></button>
<span id="preview-time" hidden>00:00 / 00:00</span>
</div>
</div>
<!-- 全画面時のロックボタン -->
<button class="lock-controls-btn" id="lock-controls-btn" title="コントロールバーを固定">🔒</button>
<script>
document.addEventListener('DOMContentLoaded', function() {
// 同期管理用の変数
let lastSyncTime = 0;
let isBuffering = false;
let syncDriftLog = [];
let syncCheckInterval;
let audioContext;
let controlsHideTimeout;
let isControlsLocked = false;
let controlsVisible = true;
try {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
} catch (e) {
console.error('Web Audio APIがサポートされていません:', e);
}
// テクノロジー風背景を生成
function createTechBackground() {
const bg = document.getElementById('techBg');
for (let i = 0; i < 200; i++) {
const line = document.createElement('div');
line.className = 'circuit-line';
const isHorizontal = Math.random() > 0.5;
if (isHorizontal) {
line.style.width = `${Math.random() * 300 + 100}px`;
line.style.height = '1px';
} else {
line.style.width = '1px';
line.style.height = `${Math.random() * 300 + 100}px`;
}
line.style.left = `${Math.random() * 100}%`;
line.style.top = `${Math.random() * 100}%`;
line.style.opacity = Math.random() * 0.5 + 0.1;
bg.appendChild(line);
}
for (let i = 0; i < 200; i++) {
const dot = document.createElement('div');
dot.className = 'grid-dot';
dot.style.left = `${Math.random() * 100}%`;
dot.style.top = `${Math.random() * 100}%`;
bg.appendChild(dot);
}
for (let i = 0; i < 15; i++) {
const hex = document.createElement('div');
hex.className = 'hexagon';
hex.style.left = `${Math.random() * 100}%`;
hex.style.top = `${Math.random() * 100}%`;
hex.style.transform = `rotate(${Math.random() * 360}deg)`;
hex.style.animation = `float ${Math.random() * 10 + 5}s infinite ease-in-out`;
bg.appendChild(hex);
}
for (let i = 0; i < 8; i++) {
const pulse = document.createElement('div');
pulse.className = 'pulse';
pulse.style.left = `${Math.random() * 100}%`;
pulse.style.top = `${Math.random() * 100}%`;
pulse.style.animationDelay = `${Math.random() * 2}s`;
bg.appendChild(pulse);
}
}
createTechBackground();
const urlParams = new URLSearchParams(window.location.search);
const isTMode = urlParams.has('mode') && urlParams.get('mode') === 't';
const basePath = isTMode ? '/t/' : '/';
// ローディング状態を管理
let loadingCount = 0;
let totalToLoad = 6; // 動画 + 5つの音声ファイル
let lastUpdateTime = 0;
const updateInterval = 1;
function checkLoadingComplete() {
loadingCount++;
if (loadingCount >= totalToLoad) {
setTimeout(function() {
const loadingOverlay = document.getElementById('loadingOverlay');
loadingOverlay.style.opacity = '0';
setTimeout(function() {
loadingOverlay.style.display = 'none';
}, 1000);
}, 500);
}
}
function handleError(error, message) {
console.error(message, error);
window.alert(`${message}\n\nエラー詳細: ${error.message}`);
}
// 要素を取得
const video = document.getElementById('video');
video.preservesPitch = true;
video.mozPreservesPitch = true; // Firefox用
video.webkitPreservesPitch = true; // 古いWebKit用
const videoContainer = document.getElementById('video-container');
const playPauseBtn = document.getElementById('play-pause-btn');
const timeDisplay = document.getElementById('time-display');
const progressContainer = document.getElementById('progress-container');
const progressBar = document.getElementById('progress-bar');
const progressTime = document.getElementById('progress-time');
const volumeBtn = document.getElementById('volume-btn');
const volumeSlider = document.getElementById('volume-slider');
const speedSlider = document.getElementById('speed-slider');
const speedValue = document.getElementById('speed-value');
const playbackSpeedSlider = document.getElementById('playback-speed');
const playbackSpeedValue = document.getElementById('playback-speed-value');
const fullscreenBtn = document.getElementById('fullscreen-btn');
const startTimeInput = document.getElementById('start-time');
const endTimeInput = document.getElementById('end-time');
const loopCheckbox = document.getElementById('loop');
const globalVolumeSlider = document.getElementById('global-volume');
const globalVolumeValue = document.getElementById('global-volume-value');
const audioSliders = document.querySelectorAll('.audio-slider');
const volumeValues = document.querySelectorAll('.volume-value');
const setStartTimeBtn = document.getElementById('set-start-time');
const setEndTimeBtn = document.getElementById('set-end-time');
const resetEndTimeBtn = document.getElementById('reset-end-time');
const disabledOverlay = document.getElementById('disabledOverlay');
const combineButton = document.getElementById('combine-button');
const combineStatus = document.getElementById('combine-status');
const previewSection = document.getElementById('preview-section');
const previewButton = document.getElementById('preview-button');
const previewTime = document.getElementById('preview-time');
const bufferingIndicator = document.getElementById('buffering-indicator');
const syncStatus = document.getElementById('sync-status');
const syncStatusText = document.getElementById('sync-status-text');
const syncStatusClose = document.getElementById('sync-status-close');
const lockControlsBtn = document.getElementById('lock-controls-btn');
const startMarker = document.getElementById('start-marker');
const endMarker = document.getElementById('end-marker');
const tempoInput = document.getElementById('tempo');
const tempoSpeedValue = document.getElementById('tempo-speed-value');
const videoControls = document.querySelector('.video-controls');
// 音声オブジェクトを作成
const audioElements = {};
const audioBuffers = {};
const audioFiles = ['p', 'a', 't', 's', 'k'];
let combinedAudioElement = null;
let isAudioCombined = false;
let currentVolumes = { p: 0, a: 1, t: 1, s: 1, k: 0 };
// 初期化
let videoDuration = 0;
let isPlaying = false;
let lastVolume = 1;
let currentPlaybackRate = 1;
let isFullscreen = false;
async function enterPiP() {
if (!document.pictureInPictureElement && !video.paused) {
try {
await video.requestPictureInPicture();
} catch (err) {
console.warn('PiP開始失敗:', err);
}
}
}
async function exitPiP() {
if (document.pictureInPictureElement) {
try {
await document.exitPictureInPicture();
} catch (err) {
console.warn('PiP終了失敗:', err);
}
}
}
// 動画のバッファリング状態を監視
video.addEventListener('waiting', function() {
isBuffering = true;
bufferingIndicator.style.display = 'block';
if (combinedAudioElement) {
combinedAudioElement.pause();
}
});
video.addEventListener('playing', function() {
isBuffering = false;
bufferingIndicator.style.display = 'none';
if (combinedAudioElement && isPlaying) {
syncAudioWithVideo();
}
});
video.addEventListener('suspend', function() {
console.log('動画の読み込みが一時停止しました');
});
video.addEventListener('stalled', function() {
console.log('動画の読み込みが停滞しました');
if (isPlaying) {
pauseMedia();
}
});
// startSyncCheck関数を修正
function startSyncCheck() {
if (isCheckingSync) return;
isCheckingSync = true;
if (syncCheckInterval) clearInterval(syncCheckInterval);
syncCheckInterval = setInterval(checkSync, 1000);
}
// stopSyncCheck関数を修正
function stopSyncCheck() {
isCheckingSync = false;
if (syncCheckInterval) clearInterval(syncCheckInterval);
}
// checkSync関数を修正(バックグラウンド時はチェックしない)
function checkSync() {
if (!isAudioCombined || !isPlaying || isBuffering || isInBackgroundTab) return;
const videoTime = video.currentTime;
const audioTime = combinedAudioElement.currentTime;
const drift = videoTime - audioTime;
syncDriftLog.push(drift);
if (syncDriftLog.length > 5) syncDriftLog.shift();
const avgDrift = syncDriftLog.reduce((a, b) => a + b, 0) / syncDriftLog.length;
syncStatusText.textContent = `同期ズレ: ${avgDrift.toFixed(3)}秒`;
if (Math.abs(avgDrift) > 0.1) {
console.log(`同期ズレを修正: ${avgDrift.toFixed(3)}秒`);
syncAudioWithVideo();
}
}
// 音声を動画に同期させる関数
function syncAudioWithVideo() {
if (!isAudioCombined || !isPlaying) return;
const currentTime = video.currentTime;
if (combinedAudioElement) {
combinedAudioElement.currentTime = currentTime;
if (isPlaying) {
combinedAudioElement.play().catch(e => console.error('音声再生エラー:', e));
}
}
// ズレ記録をリセット
syncDriftLog = [];
lastSyncTime = performance.now();
}
// 音声ファイルをロード
function loadAudioFiles() {
audioFiles.forEach(file => {
try {
const audio = new Audio(`${basePath}${file}.mp3`);
audio.preload = 'auto';
audio.loop = false;
audioElements[file] = audio;
audio.addEventListener('loadedmetadata', function() {
console.log(`${basePath}${file}.mp3 loaded`);
checkLoadingComplete();
});
audio.addEventListener('error', function() {
console.error(`音声ファイル読み込みエラー (${basePath}${file}.mp3):`, audio.error);
checkLoadingComplete();
});
} catch (error) {
console.error(`音声ファイル初期化エラー (${basePath}${file}.mp3):`, error);
checkLoadingComplete();
}
});
}
// 音声を結合する関数
async function combineAudio() {
combineButton.disabled = true;
combineStatus.textContent = "音声を合成中...";
try {
// 現在の音量設定を保存
audioFiles.forEach(file => {
currentVolumes[file] = parseFloat(document.querySelector(`.audio-slider[data-audio="${file}"]`).value);
});
// 各音声ファイルをデコード
const audioBufferPromises = audioFiles.map(async file => {
const audio = audioElements[file];
if (!audio) return null;
// 音量が0の場合はスキップ
if (currentVolumes[file] === 0) return null;
const response = await fetch(`${basePath}${file}.mp3`);
const arrayBuffer = await response.arrayBuffer();
return await audioContext.decodeAudioData(arrayBuffer);
});
// すべての音声バッファを取得
const buffers = await Promise.all(audioBufferPromises);
audioFiles.forEach((file, index) => {
audioBuffers[file] = buffers[index];
});
// 最長の音声バッファの長さを取得
const maxDuration = Math.max(...buffers.filter(b => b).map(b => b.duration));
// 新しい音声バッファを作成
const combinedAudioBuffer = audioContext.createBuffer(
2, // ステレオ
audioContext.sampleRate * maxDuration,
audioContext.sampleRate
);
// 各音声バッファを結合
for (let file of audioFiles) {
if (!audioBuffers[file] || currentVolumes[file] === 0) continue;
const buffer = audioBuffers[file];
const volume = currentVolumes[file];
// 各チャンネルに音声を加算
for (let channel = 0; channel < 2; channel++) {
const inputData = buffer.getChannelData(channel % buffer.numberOfChannels);
const outputData = combinedAudioBuffer.getChannelData(channel);
for (let i = 0; i < inputData.length; i++) {
outputData[i] += inputData[i] * volume;
}
}
}
// 音量を正規化 (クリッピング防止)
for (let channel = 0; channel < 2; channel++) {
const outputData = combinedAudioBuffer.getChannelData(channel);
let max = 0;
for (let i = 0; i < outputData.length; i++) {
if (Math.abs(outputData[i]) > max) {
max = Math.abs(outputData[i]);
}
}
if (max > 1) {
for (let i = 0; i < outputData.length; i++) {
outputData[i] /= max;
}
}
}
// AudioBufferをBlobに変換
const blob = bufferToWave(combinedAudioBuffer);
const url = URL.createObjectURL(blob);
// 新しいaudio要素を作成
combinedAudioElement = new Audio(url);
combinedAudioElement.preservesPitch = true;
combinedAudioElement.mozPreservesPitch = true;
combinedAudioElement.webkitPreservesPitch = true;
combinedAudioElement.playbackRate = currentPlaybackRate;
isAudioCombined = true;
combineStatus.textContent = "音声の合成が完了しました";
enablePlayerControls();
previewSection.style.display = 'block';
let isCheckingSync = false;
let isInBackgroundTab = false;
// visibilitychange イベントリスナーの修正
document.addEventListener('visibilitychange', async () => {
if (document.hidden) {
// 他タブに移動した場合
isInBackgroundTab = true;
stopSyncCheck(); // 同期チェックを停止
try {
// PiP表示を試みる(再生中のときのみ)
if (!video.paused) {
await enterPiP();
// PiPに成功したら同期チェックを再開
if (document.pictureInPictureElement) {
isInBackgroundTab = false;
startSyncCheck();
}
}
} catch (e) {
console.warn('PiP開始失敗:', e);
// PiP失敗時は何もしない(isInBackgroundTabがtrueのまま)
}
} else {
// タブに戻った場合
isInBackgroundTab = false;
// PiPを終了(再生中のときのみ)
if (!video.paused) {
try {
await exitPiP();
} catch (e) {
console.warn('PiP終了失敗:', e);
}
}
// タブに戻った直後に一度だけ同期チェックを実行
if (isPlaying) {
checkSync();
// その後、通常通り同期チェックを再開
startSyncCheck();
}
}
});
// 動画終了時に自動的にPiPを閉じる(次回再開のため)
video.addEventListener('ended', exitPiP);
// 合成後に音量と再生速度を適用
applyVolume();
applyPlaybackRate();
} catch (error) {
console.error('音声合成エラー:', error);
combineStatus.textContent = "音声の合成に失敗しました";
combineButton.disabled = false;
}
}
function bufferToWave(abuffer) {
const numOfChan = abuffer.numberOfChannels,
length = abuffer.length * numOfChan * 2 + 44,
buffer = new ArrayBuffer(length),
view = new DataView(buffer),
channels = [],
sampleRate = abuffer.sampleRate;
// posをletで宣言(constから変更)
let pos = 0;
// write WAV header
setUint32(0x46464952); // "RIFF"
setUint32(length - 8); // file length - 8
setUint32(0x45564157); // "WAVE"
setUint32(0x20746d66); // "fmt " chunk
setUint32(16); // length = 16
setUint16(1); // PCM (uncompressed)
setUint16(numOfChan);
setUint32(sampleRate);
setUint32(sampleRate * 2 * numOfChan);
setUint16(numOfChan * 2);
setUint16(16);
setUint32(0x61746164); // "data" - chunk
setUint32(length - pos - 4);
// write interleaved data
for (let i = 0; i < abuffer.length; i++) {
for (let channel = 0; channel < numOfChan; channel++) {
let sample = abuffer.getChannelData(channel)[i] * 0x7fff;
if (sample < -32768) sample = -32768;
if (sample > 32767) sample = 32767;
view.setInt16(pos, sample, true);
pos += 2;
}
}
function setUint16(data) {
view.setUint16(pos, data, true);
pos += 2;
}
function setUint32(data) {
view.setUint32(pos, data, true);
pos += 4;
}
return new Blob([buffer], { type: 'audio/wav' });
}
function applyVolume() {
if (!isAudioCombined) return;
// ベース音量 (0-1)
const baseVolume = parseFloat(volumeSlider.value);
// グローバル音量係数 (0-10)
const globalVolume = parseFloat(globalVolumeSlider.value) / 10; // 0-1に変換
// 最終音量 (0-1)
const finalVolume = Math.max(0, Math.min(1, baseVolume * globalVolume));
// 動画と音声の音量を設定
video.volume = finalVolume;
if (combinedAudioElement) {
combinedAudioElement.volume = finalVolume;
}
// 音量アイコンを更新
updateVolumeIcon();
}
function applyPlaybackRate() {
if (!isAudioCombined) return;
const speed = parseFloat(playbackSpeedSlider.value);
currentPlaybackRate = speed;
video.playbackRate = speed;
if (combinedAudioElement) {
combinedAudioElement.playbackRate = speed;
}
speedValue.textContent = speed.toFixed(2) + 'x';
playbackSpeedValue.textContent = speed.toFixed(2) + 'x';
speedSlider.value = speed;
}
// プレイヤーコントロールを有効化
function enablePlayerControls() {
disabledOverlay.style.display = 'none';
playPauseBtn.disabled = false;
volumeBtn.disabled = false;
volumeSlider.disabled = false;
speedSlider.disabled = false;
fullscreenBtn.disabled = false;
startTimeInput.disabled = false;
endTimeInput.disabled = false;
resetEndTimeBtn.disabled = false;
loopCheckbox.disabled = false;
globalVolumeSlider.disabled = false;
setStartTimeBtn.disabled = false;
setEndTimeBtn.disabled = false;
playbackSpeedSlider.disabled = false;
}
// プレビュー再生
function togglePreview() {
if (!isAudioCombined || !combinedAudioElement) return;
if (previewButton.textContent === '▶') {
// 再生
combinedAudioElement.currentTime = 0;
combinedAudioElement.play()
.then(() => {
previewButton.textContent = '⏸';
// プレビューの時間表示を更新
const updatePreviewTime = () => {
if (!combinedAudioElement || !isAudioCombined) return;
const currentTime = combinedAudioElement.currentTime;
const duration = combinedAudioElement.duration;
if (currentTime >= duration) {
previewButton.textContent = '▶';
previewTime.textContent = `00:00 / ${formatTime(duration)}`;
return;
}
previewTime.textContent = `${formatTime(currentTime)} / ${formatTime(duration)}`;
requestAnimationFrame(updatePreviewTime);
};
updatePreviewTime();
})
.catch(e => console.error('プレビュー再生エラー:', e));
} else {
// 一時停止
combinedAudioElement.pause();
previewButton.textContent = '▶';
}
}
// 時間をフォーマットするヘルパー関数
function formatTime(seconds) {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
}
// 動画のメタデータが読み込まれたら
video.addEventListener('loadedmetadata', function() {
try {
videoDuration = video.duration;
endTimeInput.value = videoDuration.toFixed(2);
endTimeInput.max = videoDuration;
startTimeInput.max = videoDuration - 0.1;
updateTimeDisplay();
checkLoadingComplete();
} catch (error) {
handleError(error, '動画メタデータ読み込み中にエラーが発生しました');
}
});
// 動画エラー処理
video.addEventListener('error', function() {
handleError(video.error, '動画読み込み中にエラーが発生しました');
});
// 再生ボタンクリック
playPauseBtn.addEventListener('click', function() {
const endTime = parseFloat(endTimeInput.value) || videoDuration;
if (video.currentTime >= endTime) {
const startTime = parseFloat(startTimeInput.value) || 0;
seekMedia(startTime);
}
togglePlayPause();
});
// 時間表示を更新
function updateTimeDisplay() {
const now = performance.now();
if (now - lastUpdateTime < updateInterval && !isFullscreen) return;
lastUpdateTime = now;
try {
const currentTime = video.currentTime;
const duration = video.duration || videoDuration;
const currentMinutes = Math.floor(currentTime / 60);
const currentSeconds = Math.floor(currentTime % 60);
const currentMilliseconds = Math.floor((currentTime % 1) * 100);
const durationMinutes = Math.floor(duration / 60);
const durationSeconds = Math.floor(duration % 60);
const durationMilliseconds = Math.floor((duration % 1) * 100);
timeDisplay.textContent =
`${String(currentMinutes).padStart(2, '0')}:${String(currentSeconds).padStart(2, '0')}.${String(currentMilliseconds).padStart(2, '0')} / ` +
`${String(durationMinutes).padStart(2, '0')}:${String(durationSeconds).padStart(2, '0')}.${String(durationMilliseconds).padStart(2, '0')}`;
const progressPercent = (currentTime / duration) * 100;
progressBar.style.width = `${progressPercent}%`;
} catch (error) {
console.error('時間表示更新エラー:', error);
}
}
// 再生/一時停止をトグル
function togglePlayPause() {
if (isPlaying) {
pauseMedia();
} else {
playMedia();
}
}
// 再生関数 (改良版)
function playMedia() {
try {
const duration = video.duration || videoDuration;
const startTime = parseFloat(startTimeInput.value) || 0;
const endTime = parseFloat(endTimeInput.value) || duration;
if (video.currentTime >= endTime) {
video.currentTime = startTime;
}
const playPromise = video.play();
if (playPromise !== undefined) {
playPromise.then(() => {
isPlaying = true;
playPauseBtn.textContent = '⏸';
syncAudioWithVideo();
startSyncCheck();
video.playbackRate = currentPlaybackRate;
}).catch(error => {
console.error('動画再生エラー:', error);
});
}
} catch (error) {
console.error('メディア再生エラー:', error);
}
}
// 一時停止関数
function pauseMedia() {
try {
video.pause();
isPlaying = false;
playPauseBtn.textContent = '▶';
stopSyncCheck();
if (combinedAudioElement) {
combinedAudioElement.pause();
}
} catch (error) {
console.error('メディア一時停止エラー:', error);
}
}
// 時間更新時の処理
video.addEventListener('timeupdate', function() {
const duration = video.duration || videoDuration;
const endTime = parseFloat(endTimeInput.value) || duration;
if (video.currentTime >= endTime && endTime > 0) {
if (loopCheckbox.checked) {
const startTime = parseFloat(startTimeInput.value) || 0;
video.currentTime = startTime;
syncAudioWithVideo();
} else {
pauseMedia();
video.currentTime = endTime;
}
}
updateTimeDisplay();
});
// プログレスバークリックでシーク
progressContainer.addEventListener('click', function(e) {
if (!video.duration) return;
const rect = this.getBoundingClientRect();
const pos = (e.clientX - rect.left) / rect.width;
const seekTime = pos * video.duration;
seekMedia(seekTime);
});
// 指定した時間にシーク (改良版)
function seekMedia(time) {
try {
const duration = video.duration || videoDuration;
const startTime = parseFloat(startTimeInput.value) || 0;
const endTime = parseFloat(endTimeInput.value) || duration;
const seekTime = Math.max(startTime, Math.min(time, endTime));
video.currentTime = seekTime;
if (combinedAudioElement) {
combinedAudioElement.currentTime = seekTime;
if (isPlaying) {
combinedAudioElement.play().catch(e => console.error('音声再生エラー:', e));
}
}
} catch (error) {
console.error('メディアシークエラー:', error);
}
}
// プログレスバー上でマウス移動時に時間を表示
progressContainer.addEventListener('mousemove', function(e) {
if (!video.duration) return;
const rect = this.getBoundingClientRect();
const pos = (e.clientX - rect.left) / rect.width;
const hoverTime = pos * video.duration;
const minutes = Math.floor(hoverTime / 60);
const seconds = Math.floor(hoverTime % 60);
const milliseconds = Math.floor((hoverTime % 1) * 100);
progressTime.textContent = `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}.${String(milliseconds).padStart(2, '0')}`;
progressTime.style.display = 'block';
progressTime.style.left = `${pos * 100}%`;
});
progressContainer.addEventListener('mouseleave', function() {
progressTime.style.display = 'none';
});
// 動画クリックで再生/一時停止
video.addEventListener('click', function() {
togglePlayPause();
});
volumeSlider.addEventListener('input', function() {
if (!isAudioCombined) return;
lastVolume = parseFloat(this.value);
applyVolume();
});
volumeBtn.addEventListener('click', function() {
if (!isAudioCombined) return;
if (video.volume > 0) {
lastVolume = parseFloat(volumeSlider.value);
volumeSlider.value = 0;
} else {
volumeSlider.value = lastVolume;
}
applyVolume();
});
// 音量アイコンを更新
function updateVolumeIcon() {
if (video.volume === 0) {
volumeBtn.textContent = '🔇';
} else if (video.volume < 3) {
volumeBtn.textContent = '🔈';
} else {
volumeBtn.textContent = '🔊';
}
}
// 再生速度スライダー (動画プレイヤー)
speedSlider.addEventListener('input', function() {
if (!isAudioCombined) return;
const speed = parseFloat(this.value);
speedValue.textContent = speed.toFixed(2) + 'x';
playbackSpeedSlider.value = speed;
playbackSpeedValue.textContent = speed.toFixed(2) + 'x';
updatePlaybackRate(speed);
});
// 再生速度スライダー (設定メニュー)
playbackSpeedSlider.addEventListener('input', function() {
if (!isAudioCombined) return;
const speed = parseFloat(this.value);
playbackSpeedValue.textContent = speed.toFixed(2) + 'x';
speedSlider.value = speed;
speedValue.textContent = speed.toFixed(2) + 'x';
updatePlaybackRate(speed);
});
// テンポ入力による再生速度更新
tempoInput.addEventListener('input', function() {
const tempo = parseFloat(this.value);
const baseTempo = isTMode ? 66 : 92;
const speed = tempo / baseTempo;
const clampedSpeed = Math.max(0.001, Math.min(5.0, speed));
playbackSpeedSlider.value = clampedSpeed;
playbackSpeedValue.textContent = clampedSpeed.toFixed(2) + 'x';
speedSlider.value = clampedSpeed;
speedValue.textContent = clampedSpeed.toFixed(2) + 'x';
tempoSpeedValue.textContent = clampedSpeed.toFixed(2) + 'x';
updatePlaybackRate(clampedSpeed);
});
function updatePlaybackRate(speed) {
if (!isAudioCombined) return;
currentPlaybackRate = speed;
video.playbackRate = speed;
// ピッチ保持を再設定
video.preservesPitch = true;
video.mozPreservesPitch = true;
video.webkitPreservesPitch = true;
if (combinedAudioElement) {
combinedAudioElement.playbackRate = speed;
// 合成音声のピッチ保持を設定
combinedAudioElement.preservesPitch = true;
combinedAudioElement.mozPreservesPitch = true;
combinedAudioElement.webkitPreservesPitch = true;
}
}
// 全画面ボタン
fullscreenBtn.addEventListener('click', function() {
if (!isFullscreen) {
if (videoContainer.requestFullscreen) {
videoContainer.requestFullscreen();
} else if (videoContainer.webkitRequestFullscreen) {
videoContainer.webkitRequestFullscreen();
} else if (videoContainer.msRequestFullscreen) {
videoContainer.msRequestFullscreen();
}
} else {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
}
});
// 全画面変更イベント
document.addEventListener('fullscreenchange', handleFullscreenChange);
document.addEventListener('webkitfullscreenchange', handleFullscreenChange);
document.addEventListener('msfullscreenchange', handleFullscreenChange);
function handleFullscreenChange() {
isFullscreen = !!(document.fullscreenElement || document.webkitFullscreenElement || document.msFullscreenElement);
fullscreenBtn.textContent = isFullscreen ? '⛶' : '⛶';
video.controls = false;
// 全画面時にロックボタンを表示
lockControlsBtn.style.display = isFullscreen ? 'flex' : 'none';
// 全画面時にコントロールバー自動非表示機能を有効化
if (isFullscreen) {
resetControlsHideTimer();
document.addEventListener('mousemove', handleFullscreenMouseMove);
} else {
document.removeEventListener('mousemove', handleFullscreenMouseMove);
clearTimeout(controlsHideTimeout);
showControls();
}
}
// 全画面時のマウス移動処理
function handleFullscreenMouseMove() {
if (!isControlsLocked) {
showControls();
resetControlsHideTimer();
}
}
// コントロールバーを表示
function showControls() {
if (!controlsVisible) {
videoControls.style.opacity = '1';
controlsVisible = true;
}
}
// コントロールバーを非表示
function hideControls() {
if (!isControlsLocked && controlsVisible) {
videoControls.style.opacity = '0';
controlsVisible = false;
}
}
// コントロールバー非表示タイマーをリセット
function resetControlsHideTimer() {
clearTimeout(controlsHideTimeout);
if (!isControlsLocked) {
controlsHideTimeout = setTimeout(hideControls, 1500); // 1.5秒後に非表示
}
}
// ロックボタンのクリック処理
lockControlsBtn.addEventListener('click', function() {
isControlsLocked = !isControlsLocked;
this.classList.toggle('locked', isControlsLocked);
if (isControlsLocked) {
showControls();
clearTimeout(controlsHideTimeout);
} else {
resetControlsHideTimer();
}
});
// キーボードイベント (ESCで全画面終了)
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && isFullscreen) {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
}
});
// ボリュームスライダーのイベント
audioSliders.forEach((slider, index) => {
slider.addEventListener('input', function() {
const value = parseFloat(this.value);
volumeValues[index].textContent = value.toFixed(2);
const percent = value * 100;
this.style.backgroundSize = `${percent}% 100%`;
});
});
globalVolumeSlider.addEventListener('input', function() {
const value = parseFloat(this.value);
globalVolumeValue.textContent = value.toFixed(1);
const percent = (value - this.min) / (this.max - this.min) * 100;
this.style.backgroundSize = `${percent}% 100%`;
applyVolume();
});
// ループ設定変更時
loopCheckbox.addEventListener('change', function() {
// 合成音声ではループは動画に依存する
});
// 現在の秒数を開始時間に設定
setStartTimeBtn.addEventListener('click', function() {
startTimeInput.value = video.currentTime.toFixed(2);
updateProgressMarkers();
});
// 現在の秒数を終了時間に設定
setEndTimeBtn.addEventListener('click', function() {
endTimeInput.value = video.currentTime.toFixed(2);
updateProgressMarkers();
});
// 終了時間を動画の長さにリセット
resetEndTimeBtn.addEventListener('click', function() {
endTimeInput.value = video.duration.toFixed(2);
updateProgressMarkers();
});
// プログレスバーのマーカーを更新
function updateProgressMarkers() {
const duration = video.duration || videoDuration;
const startTime = parseFloat(startTimeInput.value) || 0;
const endTime = parseFloat(endTimeInput.value) || duration;
if (duration > 0) {
startMarker.style.left = `${(startTime / duration) * 100}%`;
endMarker.style.left = `${(endTime / duration) * 100}%`;
startMarker.style.display = 'block';
endMarker.style.display = 'block';
}
}
// 開始/終了時間変更時にマーカーを更新
startTimeInput.addEventListener('input', updateProgressMarkers);
endTimeInput.addEventListener('input', updateProgressMarkers);
// 合成ボタンクリック
combineButton.addEventListener('click', combineAudio);
// プレビューボタンクリック
previewButton.addEventListener('click', togglePreview);
// 同期ステータスを閉じる
syncStatusClose.addEventListener('click', function() {
syncStatus.style.display = 'none';
});
// 初期化
loadAudioFiles();
updateVolumeIcon();
volumeSlider.value = video.volume;
video.controls = false;
// スライダーの背景を初期化
function initSliderBackgrounds() {
const sliders = [
volumeSlider,
speedSlider,
globalVolumeSlider,
playbackSpeedSlider,
...audioSliders
];
sliders.forEach(slider => {
if (slider) {
slider.style.backgroundImage = 'linear-gradient(#64ffda, #64ffda)';
slider.style.backgroundRepeat = 'no-repeat';
if (slider === globalVolumeSlider) {
const percent = (slider.value - slider.min) / (slider.max - slider.min) * 100;
slider.style.backgroundSize = `${percent}% 100%`;
globalVolumeValue.textContent = slider.value;
} else {
slider.style.backgroundSize = `${slider.value * 100}% 100%`;
}
}
});
}
initSliderBackgrounds();
startSyncCheck(); // 同期チェックを開始
// 初期テンポ設定
tempoInput.value = isTMode ? 66 : 92;
tempoInput.dispatchEvent(new Event('input'));
});
</script>
</body>
</html>