Update index.html
Browse files- index.html +120 -402
index.html
CHANGED
@@ -931,232 +931,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
931 |
window.alert(`${message}\n\nエラー詳細: ${error.message}`);
|
932 |
}
|
933 |
|
934 |
-
async function registerServiceWorker() {
|
935 |
-
const statusElement = document.getElementById('sw-status');
|
936 |
-
const registerBtn = document.getElementById('sw-register-btn');
|
937 |
-
|
938 |
-
if (!('serviceWorker' in navigator)) {
|
939 |
-
statusElement.textContent = "このブラウザはService Workerをサポートしていません";
|
940 |
-
return;
|
941 |
-
}
|
942 |
-
|
943 |
-
try {
|
944 |
-
// チェックボックスの状態を収集
|
945 |
-
const checkboxStates = {
|
946 |
-
'sw-video': document.getElementById('sw-video').checked,
|
947 |
-
'sw-t-video': document.getElementById('sw-t-video').checked,
|
948 |
-
'sw-piano': document.getElementById('sw-piano').checked,
|
949 |
-
'sw-soprano': document.getElementById('sw-soprano').checked,
|
950 |
-
'sw-alto': document.getElementById('sw-alto').checked,
|
951 |
-
'sw-tenor': document.getElementById('sw-tenor').checked,
|
952 |
-
'sw-combined': document.getElementById('sw-combined').checked,
|
953 |
-
'sw-t-piano': document.getElementById('sw-t-piano').checked,
|
954 |
-
'sw-t-soprano': document.getElementById('sw-t-soprano').checked,
|
955 |
-
'sw-t-alto': document.getElementById('sw-t-alto').checked,
|
956 |
-
'sw-t-tenor': document.getElementById('sw-t-tenor').checked,
|
957 |
-
'sw-t-combined': document.getElementById('sw-t-combined').checked,
|
958 |
-
'sw-index': document.getElementById('sw-index').checked,
|
959 |
-
'sw-root': document.getElementById('sw-root').checked
|
960 |
-
};
|
961 |
-
|
962 |
-
// チェックされたファイルを収集
|
963 |
-
const filesToCache = [];
|
964 |
-
|
965 |
-
// 通常のメディアファイル
|
966 |
-
if (checkboxStates['sw-video']) filesToCache.push('/v.mp4');
|
967 |
-
if (checkboxStates['sw-piano']) filesToCache.push('/p.mp3');
|
968 |
-
if (checkboxStates['sw-soprano']) filesToCache.push('/s.mp3');
|
969 |
-
if (checkboxStates['sw-alto']) filesToCache.push('/a.mp3');
|
970 |
-
if (checkboxStates['sw-tenor']) filesToCache.push('/t.mp3');
|
971 |
-
if (checkboxStates['sw-combined']) filesToCache.push('/k.mp3');
|
972 |
-
|
973 |
-
// t/ ディレクトリのメディアファイル
|
974 |
-
if (checkboxStates['sw-t-video']) filesToCache.push('/t/v.mp4');
|
975 |
-
if (checkboxStates['sw-t-piano']) filesToCache.push('/t/p.mp3');
|
976 |
-
if (checkboxStates['sw-t-soprano']) filesToCache.push('/t/s.mp3');
|
977 |
-
if (checkboxStates['sw-t-alto']) filesToCache.push('/t/a.mp3');
|
978 |
-
if (checkboxStates['sw-t-tenor']) filesToCache.push('/t/t.mp3');
|
979 |
-
if (checkboxStates['sw-t-combined']) filesToCache.push('/t/k.mp3');
|
980 |
-
|
981 |
-
registerBtn.disabled = true;
|
982 |
-
statusElement.textContent = "サービスワーカーを登録中...";
|
983 |
-
|
984 |
-
// Service Workerを登録
|
985 |
-
const registration = await navigator.serviceWorker.register('/service-worker.js');
|
986 |
-
|
987 |
-
// Service Workerがアクティブになるのを待つ
|
988 |
-
await new Promise((resolve, reject) => {
|
989 |
-
if (registration.active) {
|
990 |
-
resolve();
|
991 |
-
} else {
|
992 |
-
const worker = registration.installing || registration.waiting;
|
993 |
-
if (worker) {
|
994 |
-
worker.addEventListener('statechange', () => {
|
995 |
-
if (worker.state === 'activated') {
|
996 |
-
resolve();
|
997 |
-
} else if (worker.state === 'rejected') {
|
998 |
-
reject(new Error('Service Workerの登録が拒否されました'));
|
999 |
-
}
|
1000 |
-
});
|
1001 |
-
} else {
|
1002 |
-
registration.addEventListener('updatefound', () => {
|
1003 |
-
const newWorker = registration.installing;
|
1004 |
-
newWorker.addEventListener('statechange', () => {
|
1005 |
-
if (newWorker.state === 'activated') {
|
1006 |
-
resolve();
|
1007 |
-
} else if (newWorker.state === 'rejected') {
|
1008 |
-
reject(new Error('Service Workerの登録が拒否されました'));
|
1009 |
-
}
|
1010 |
-
});
|
1011 |
-
});
|
1012 |
-
}
|
1013 |
-
}
|
1014 |
-
});
|
1015 |
-
|
1016 |
-
// キャッシュするファイルをService Workerに送信
|
1017 |
-
registration.active.postMessage({
|
1018 |
-
type: 'CACHE_FILES',
|
1019 |
-
files: filesToCache,
|
1020 |
-
checkboxStates: checkboxStates
|
1021 |
-
});
|
1022 |
-
|
1023 |
-
// キャッシュが完了するのを待つ
|
1024 |
-
statusElement.textContent = "ファイルをキャッシュ中...";
|
1025 |
-
|
1026 |
-
// キャッシュが完了したか確認する
|
1027 |
-
await verifyCache(filesToCache);
|
1028 |
-
|
1029 |
-
statusElement.textContent = "サービスワーカーが正常に登録され、ファイルがキャッシュされました";
|
1030 |
-
registerBtn.disabled = false;
|
1031 |
-
} catch (error) {
|
1032 |
-
console.error('Service Worker登録エラー:', error);
|
1033 |
-
statusElement.textContent = `サービスワーカー登録に失敗しました: ${error.message}`;
|
1034 |
-
registerBtn.disabled = false;
|
1035 |
-
}
|
1036 |
-
}
|
1037 |
-
async function verifyCache(filesToCache) {
|
1038 |
-
const maxAttempts = 10;
|
1039 |
-
const delay = 500; // 500msごとに確認
|
1040 |
-
|
1041 |
-
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
1042 |
-
const cache = await caches.open('media-player-cache-v1');
|
1043 |
-
const cachedRequests = await cache.keys();
|
1044 |
-
const cachedUrls = cachedRequests.map(request => request.url.replace(location.origin, ''));
|
1045 |
-
|
1046 |
-
// ログはここに移動すると、キャッシュ状況が見えやすい
|
1047 |
-
console.log('filesToCache:', filesToCache);
|
1048 |
-
console.log('cachedUrls:', cachedUrls);
|
1049 |
-
|
1050 |
-
// すべてのファイルがキャッシュされているか確認
|
1051 |
-
const allCached = filesToCache.every(file => cachedUrls.includes(file));
|
1052 |
-
|
1053 |
-
if (allCached) {
|
1054 |
-
return true;
|
1055 |
-
}
|
1056 |
-
|
1057 |
-
// 最後の試行でなければ待機
|
1058 |
-
if (attempt < maxAttempts) {
|
1059 |
-
await new Promise(resolve => setTimeout(resolve, delay));
|
1060 |
-
}
|
1061 |
-
}
|
1062 |
-
|
1063 |
-
throw new Error('キャッシュが完了しませんでした');
|
1064 |
-
}
|
1065 |
-
|
1066 |
-
async function restoreCheckboxStates() {
|
1067 |
-
try {
|
1068 |
-
const cache = await caches.open('settings-cache');
|
1069 |
-
const response = await cache.match('checkbox-states');
|
1070 |
-
|
1071 |
-
if (response) {
|
1072 |
-
const checkboxStates = await response.json();
|
1073 |
-
|
1074 |
-
for (const [id, checked] of Object.entries(checkboxStates)) {
|
1075 |
-
const checkbox = document.getElementById(id);
|
1076 |
-
if (checkbox) {
|
1077 |
-
checkbox.checked = checked;
|
1078 |
-
}
|
1079 |
-
}
|
1080 |
-
}
|
1081 |
-
} catch (error) {
|
1082 |
-
console.error('チェックボックス状態の復元に失敗:', error);
|
1083 |
-
}
|
1084 |
-
}
|
1085 |
-
|
1086 |
-
// オフライン状態をチェック
|
1087 |
-
function checkOnlineStatus() {
|
1088 |
-
const settingsSection = document.querySelector('.settings');
|
1089 |
-
if (!navigator.onLine) {
|
1090 |
-
settingsSection.style.display = 'none';
|
1091 |
-
} else {
|
1092 |
-
settingsSection.style.display = 'block';
|
1093 |
-
}
|
1094 |
-
}
|
1095 |
-
|
1096 |
-
// 全画面時のUI表示制御
|
1097 |
-
function setupFullscreenUI() {
|
1098 |
-
const videoContainer = document.getElementById('video-container');
|
1099 |
-
const controls = document.querySelector('.video-controls');
|
1100 |
-
let hideTimeout;
|
1101 |
-
let isMouseOverControls = false;
|
1102 |
-
|
1103 |
-
// マウスがコントロール上にあるかどうかを追跡
|
1104 |
-
controls.addEventListener('mouseenter', () => {
|
1105 |
-
isMouseOverControls = true;
|
1106 |
-
clearTimeout(hideTimeout);
|
1107 |
-
controls.style.opacity = '1';
|
1108 |
-
});
|
1109 |
-
|
1110 |
-
controls.addEventListener('mouseleave', () => {
|
1111 |
-
isMouseOverControls = false;
|
1112 |
-
startHideTimeout();
|
1113 |
-
});
|
1114 |
-
|
1115 |
-
function startHideTimeout() {
|
1116 |
-
clearTimeout(hideTimeout);
|
1117 |
-
if (isFullscreen && !isMouseOverControls) {
|
1118 |
-
hideTimeout = setTimeout(() => {
|
1119 |
-
controls.style.opacity = '0';
|
1120 |
-
}, 1500);
|
1121 |
-
}
|
1122 |
-
}
|
1123 |
-
|
1124 |
-
// 動画コンテナでマウス移動を検出
|
1125 |
-
videoContainer.addEventListener('mousemove', () => {
|
1126 |
-
if (isFullscreen) {
|
1127 |
-
controls.style.opacity = '1';
|
1128 |
-
startHideTimeout();
|
1129 |
-
}
|
1130 |
-
});
|
1131 |
-
}
|
1132 |
-
// DOMContentLoaded時の初期化
|
1133 |
-
document.addEventListener('DOMContentLoaded', function() {
|
1134 |
-
// チェックボックスをデフォルトでオフに
|
1135 |
-
document.querySelectorAll('.settings input[type="checkbox"]').forEach(checkbox => {
|
1136 |
-
checkbox.checked = false;
|
1137 |
-
});
|
1138 |
-
|
1139 |
-
// index.htmlのチェックボックスは必須なのでオンに
|
1140 |
-
document.getElementById('sw-index').checked = true;
|
1141 |
-
document.getElementById('sw-index').disabled = true;
|
1142 |
-
|
1143 |
-
// ルートのチェックボックスは非表示に
|
1144 |
-
document.getElementById('sw-root').parentElement.style.display = 'none';
|
1145 |
-
|
1146 |
-
// チェックボックスの状態を復元
|
1147 |
-
restoreCheckboxStates();
|
1148 |
-
|
1149 |
-
// オンライン状態をチェック
|
1150 |
-
checkOnlineStatus();
|
1151 |
-
window.addEventListener('online', checkOnlineStatus);
|
1152 |
-
window.addEventListener('offline', checkOnlineStatus);
|
1153 |
-
|
1154 |
-
// 全画面UI制御を設定
|
1155 |
-
setupFullscreenUI();
|
1156 |
-
});
|
1157 |
-
|
1158 |
-
// 登録ボタンにイベントリスナーを追加
|
1159 |
-
document.getElementById('sw-register-btn').addEventListener('click', registerServiceWorker);
|
1160 |
// 要素を取得
|
1161 |
const video = document.getElementById('video');
|
1162 |
const videoContainer = document.getElementById('video-container');
|
@@ -1194,8 +968,7 @@ document.getElementById('sw-register-btn').addEventListener('click', registerSer
|
|
1194 |
const audioElements = {};
|
1195 |
const audioBuffers = {};
|
1196 |
const audioFiles = ['p', 'a', 't', 's', 'k'];
|
1197 |
-
let
|
1198 |
-
let combinedAudioSource = null;
|
1199 |
let isAudioCombined = false;
|
1200 |
let currentVolumes = { p: 0, a: 1, t: 1, s: 1, k: 0 };
|
1201 |
|
@@ -1210,16 +983,15 @@ document.getElementById('sw-register-btn').addEventListener('click', registerSer
|
|
1210 |
video.addEventListener('waiting', function() {
|
1211 |
isBuffering = true;
|
1212 |
bufferingIndicator.style.display = 'block';
|
1213 |
-
if (
|
1214 |
-
|
1215 |
-
combinedAudioSource = null;
|
1216 |
}
|
1217 |
});
|
1218 |
|
1219 |
video.addEventListener('playing', function() {
|
1220 |
isBuffering = false;
|
1221 |
bufferingIndicator.style.display = 'none';
|
1222 |
-
if (
|
1223 |
syncAudioWithVideo();
|
1224 |
}
|
1225 |
});
|
@@ -1249,7 +1021,7 @@ document.getElementById('sw-register-btn').addEventListener('click', registerSer
|
|
1249 |
if (!isAudioCombined || !isPlaying || isBuffering) return;
|
1250 |
|
1251 |
const videoTime = video.currentTime;
|
1252 |
-
const audioTime =
|
1253 |
const drift = videoTime - audioTime;
|
1254 |
|
1255 |
// ズレを記録(直近5回分)
|
@@ -1275,76 +1047,18 @@ document.getElementById('sw-register-btn').addEventListener('click', registerSer
|
|
1275 |
|
1276 |
const currentTime = video.currentTime;
|
1277 |
|
1278 |
-
if (
|
1279 |
-
|
|
|
|
|
|
|
1280 |
}
|
1281 |
|
1282 |
-
// 新しい audio 要素を作成
|
1283 |
-
const combinedAudioElement = document.createElement('audio');
|
1284 |
-
combinedAudioElement.src = URL.createObjectURL(bufferToWave(combinedAudioBuffer));
|
1285 |
-
combinedAudioElement.preservesPitch = true; // ピッチを維持
|
1286 |
-
combinedAudioElement.playbackRate = currentPlaybackRate;
|
1287 |
-
combinedAudioElement.currentTime = currentTime;
|
1288 |
-
combinedAudioElement.play();
|
1289 |
-
|
1290 |
-
|
1291 |
// ズレ記録をリセット
|
1292 |
syncDriftLog = [];
|
1293 |
lastSyncTime = performance.now();
|
1294 |
}
|
1295 |
|
1296 |
-
function bufferToWave(abuffer) {
|
1297 |
-
const numOfChan = abuffer.numberOfChannels,
|
1298 |
-
length = abuffer.length * numOfChan * 2 + 44,
|
1299 |
-
buffer = new ArrayBuffer(length),
|
1300 |
-
view = new DataView(buffer),
|
1301 |
-
channels = [],
|
1302 |
-
sampleRate = abuffer.sampleRate,
|
1303 |
-
offset = 0,
|
1304 |
-
pos = 0;
|
1305 |
-
|
1306 |
-
// write WAV header
|
1307 |
-
setUint32(0x46464952); // "RIFF"
|
1308 |
-
setUint32(length - 8); // file length - 8
|
1309 |
-
setUint32(0x45564157); // "WAVE"
|
1310 |
-
|
1311 |
-
setUint32(0x20746d66); // "fmt " chunk
|
1312 |
-
setUint32(16); // length = 16
|
1313 |
-
setUint16(1); // PCM (uncompressed)
|
1314 |
-
setUint16(numOfChan);
|
1315 |
-
setUint32(sampleRate);
|
1316 |
-
setUint32(sampleRate * 2 * numOfChan);
|
1317 |
-
setUint16(numOfChan * 2);
|
1318 |
-
setUint16(16);
|
1319 |
-
|
1320 |
-
setUint32(0x61746164); // "data" - chunk
|
1321 |
-
setUint32(length - pos - 4);
|
1322 |
-
|
1323 |
-
// write interleaved data
|
1324 |
-
for (let i = 0; i < abuffer.length; i++) {
|
1325 |
-
for (let channel = 0; channel < numOfChan; channel++) {
|
1326 |
-
let sample = abuffer.getChannelData(channel)[i] * 0x7fff;
|
1327 |
-
if (sample < -32768) sample = -32768;
|
1328 |
-
if (sample > 32767) sample = 32767;
|
1329 |
-
view.setInt16(pos, sample, true);
|
1330 |
-
pos += 2;
|
1331 |
-
}
|
1332 |
-
}
|
1333 |
-
|
1334 |
-
function setUint16(data) {
|
1335 |
-
view.setUint16(pos, data, true);
|
1336 |
-
pos += 2;
|
1337 |
-
}
|
1338 |
-
|
1339 |
-
function setUint32(data) {
|
1340 |
-
view.setUint32(pos, data, true);
|
1341 |
-
pos += 4;
|
1342 |
-
}
|
1343 |
-
|
1344 |
-
return new Blob([buffer], { type: 'audio/wav' });
|
1345 |
-
}
|
1346 |
-
|
1347 |
-
|
1348 |
// 音声ファイルをロード
|
1349 |
function loadAudioFiles() {
|
1350 |
audioFiles.forEach(file => {
|
@@ -1372,11 +1086,6 @@ combinedAudioElement.play();
|
|
1372 |
|
1373 |
// 音声を結合する関数
|
1374 |
async function combineAudio() {
|
1375 |
-
if (!audioContext) {
|
1376 |
-
combineStatus.textContent = "Web Audio APIが利用できません";
|
1377 |
-
return;
|
1378 |
-
}
|
1379 |
-
|
1380 |
combineButton.disabled = true;
|
1381 |
combineStatus.textContent = "音声を合成中...";
|
1382 |
|
@@ -1406,7 +1115,7 @@ combinedAudioElement.play();
|
|
1406 |
const maxDuration = Math.max(...buffers.filter(b => b).map(b => b.duration));
|
1407 |
|
1408 |
// 新しい音声バッファを作成
|
1409 |
-
combinedAudioBuffer = audioContext.createBuffer(
|
1410 |
2, // ステレオ
|
1411 |
audioContext.sampleRate * maxDuration,
|
1412 |
audioContext.sampleRate
|
@@ -1451,6 +1160,15 @@ combinedAudioElement.play();
|
|
1451 |
}
|
1452 |
}
|
1453 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1454 |
isAudioCombined = true;
|
1455 |
combineStatus.textContent = "音声の合成が完了しました";
|
1456 |
enablePlayerControls();
|
@@ -1467,6 +1185,57 @@ combinedAudioElement.play();
|
|
1467 |
}
|
1468 |
}
|
1469 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1470 |
function applyVolume() {
|
1471 |
if (!isAudioCombined) return;
|
1472 |
|
@@ -1478,65 +1247,32 @@ combinedAudioElement.play();
|
|
1478 |
// 最終音量 (0-1)
|
1479 |
const finalVolume = Math.max(0, Math.min(1, baseVolume * globalVolume));
|
1480 |
|
1481 |
-
//
|
1482 |
video.volume = finalVolume;
|
|
|
|
|
|
|
1483 |
|
1484 |
// 音量アイコンを更新
|
1485 |
updateVolumeIcon();
|
1486 |
}
|
1487 |
|
1488 |
-
function applyPlaybackRate() {
|
1489 |
-
|
1490 |
-
|
1491 |
-
const speed = parseFloat(playbackSpeedSlider.value);
|
1492 |
-
currentPlaybackRate = speed;
|
1493 |
-
video.playbackRate = speed;
|
1494 |
-
|
1495 |
-
if (combinedAudioSource) {
|
1496 |
-
// 既存のオーディオソースを停止
|
1497 |
-
combinedAudioSource.stop();
|
1498 |
|
1499 |
-
|
1500 |
-
|
1501 |
-
|
1502 |
-
combinedAudioSource.connect(audioContext.destination);
|
1503 |
|
1504 |
-
|
1505 |
-
|
1506 |
-
combinedAudioSource.preservesPitch = true;
|
1507 |
-
} else if ('webkitPreservesPitch' in combinedAudioSource) {
|
1508 |
-
combinedAudioSource.webkitPreservesPitch = true;
|
1509 |
-
} else if ('mozPreservesPitch' in combinedAudioSource) {
|
1510 |
-
combinedAudioSource.mozPreservesPitch = true;
|
1511 |
}
|
1512 |
|
1513 |
-
|
1514 |
-
|
1515 |
-
|
1516 |
-
// 再生中なら新しいソースで再生を続ける
|
1517 |
-
if (isPlaying) {
|
1518 |
-
combinedAudioSource.start(0, video.currentTime);
|
1519 |
-
}
|
1520 |
-
}
|
1521 |
-
|
1522 |
-
speedValue.textContent = speed.toFixed(2) + 'x';
|
1523 |
-
playbackSpeedValue.textContent = speed.toFixed(2) + 'x';
|
1524 |
-
speedSlider.value = speed;
|
1525 |
-
}
|
1526 |
-
document.addEventListener('visibilitychange', function() {
|
1527 |
-
if (document.hidden) {
|
1528 |
-
// タブが非アクティブになったら同期チェックを停止
|
1529 |
-
stopSyncCheck();
|
1530 |
-
if (isPlaying) {
|
1531 |
-
pauseMedia();
|
1532 |
-
}
|
1533 |
-
} else {
|
1534 |
-
// タブがアクティブになったら同期チェックを再開
|
1535 |
-
if (isPlaying) {
|
1536 |
-
startSyncCheck();
|
1537 |
-
}
|
1538 |
}
|
1539 |
-
|
1540 |
// プレイヤーコントロールを有効化
|
1541 |
function enablePlayerControls() {
|
1542 |
disabledOverlay.style.display = 'none';
|
@@ -1556,50 +1292,38 @@ document.addEventListener('visibilitychange', function() {
|
|
1556 |
|
1557 |
// ��レビュー再生
|
1558 |
function togglePreview() {
|
1559 |
-
if (!isAudioCombined || !
|
1560 |
|
1561 |
if (previewButton.textContent === '▶') {
|
1562 |
// 再生
|
1563 |
-
|
1564 |
-
|
1565 |
-
|
1566 |
-
|
1567 |
-
|
1568 |
-
|
1569 |
-
|
1570 |
-
|
1571 |
-
|
1572 |
-
|
1573 |
-
|
1574 |
-
|
1575 |
-
|
1576 |
-
|
1577 |
-
|
1578 |
-
|
1579 |
-
|
1580 |
-
|
1581 |
-
|
1582 |
-
|
1583 |
-
|
1584 |
-
|
1585 |
-
|
1586 |
-
|
1587 |
-
|
1588 |
-
requestAnimationFrame(updatePreviewTime);
|
1589 |
-
};
|
1590 |
-
|
1591 |
-
updatePreviewTime();
|
1592 |
-
|
1593 |
-
combinedAudioSource.onended = () => {
|
1594 |
-
previewButton.textContent = '▶';
|
1595 |
-
previewTime.textContent = `00:00 / ${formatTime(combinedAudioBuffer.duration)}`;
|
1596 |
-
};
|
1597 |
} else {
|
1598 |
// 一時停止
|
1599 |
-
|
1600 |
-
combinedAudioSource.stop();
|
1601 |
-
combinedAudioSource = null;
|
1602 |
-
}
|
1603 |
previewButton.textContent = '▶';
|
1604 |
}
|
1605 |
}
|
@@ -1716,9 +1440,8 @@ document.addEventListener('visibilitychange', function() {
|
|
1716 |
playPauseBtn.textContent = '▶';
|
1717 |
stopSyncCheck();
|
1718 |
|
1719 |
-
if (
|
1720 |
-
|
1721 |
-
combinedAudioSource = null;
|
1722 |
}
|
1723 |
} catch (error) {
|
1724 |
console.error('メディア一時停止エラー:', error);
|
@@ -1765,15 +1488,10 @@ document.addEventListener('visibilitychange', function() {
|
|
1765 |
const seekTime = Math.max(startTime, Math.min(time, endTime));
|
1766 |
video.currentTime = seekTime;
|
1767 |
|
1768 |
-
if (
|
1769 |
-
|
1770 |
-
combinedAudioSource = audioContext.createBufferSource();
|
1771 |
-
combinedAudioSource.buffer = combinedAudioBuffer;
|
1772 |
-
combinedAudioSource.connect(audioContext.destination);
|
1773 |
-
|
1774 |
if (isPlaying) {
|
1775 |
-
|
1776 |
-
combinedAudioSource.playbackRate.value = currentPlaybackRate;
|
1777 |
}
|
1778 |
}
|
1779 |
} catch (error) {
|
@@ -1866,8 +1584,8 @@ document.addEventListener('visibilitychange', function() {
|
|
1866 |
currentPlaybackRate = speed;
|
1867 |
video.playbackRate = speed;
|
1868 |
|
1869 |
-
if (
|
1870 |
-
|
1871 |
}
|
1872 |
}
|
1873 |
|
|
|
931 |
window.alert(`${message}\n\nエラー詳細: ${error.message}`);
|
932 |
}
|
933 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
934 |
// 要素を取得
|
935 |
const video = document.getElementById('video');
|
936 |
const videoContainer = document.getElementById('video-container');
|
|
|
968 |
const audioElements = {};
|
969 |
const audioBuffers = {};
|
970 |
const audioFiles = ['p', 'a', 't', 's', 'k'];
|
971 |
+
let combinedAudioElement = null;
|
|
|
972 |
let isAudioCombined = false;
|
973 |
let currentVolumes = { p: 0, a: 1, t: 1, s: 1, k: 0 };
|
974 |
|
|
|
983 |
video.addEventListener('waiting', function() {
|
984 |
isBuffering = true;
|
985 |
bufferingIndicator.style.display = 'block';
|
986 |
+
if (combinedAudioElement) {
|
987 |
+
combinedAudioElement.pause();
|
|
|
988 |
}
|
989 |
});
|
990 |
|
991 |
video.addEventListener('playing', function() {
|
992 |
isBuffering = false;
|
993 |
bufferingIndicator.style.display = 'none';
|
994 |
+
if (combinedAudioElement && isPlaying) {
|
995 |
syncAudioWithVideo();
|
996 |
}
|
997 |
});
|
|
|
1021 |
if (!isAudioCombined || !isPlaying || isBuffering) return;
|
1022 |
|
1023 |
const videoTime = video.currentTime;
|
1024 |
+
const audioTime = combinedAudioElement.currentTime;
|
1025 |
const drift = videoTime - audioTime;
|
1026 |
|
1027 |
// ズレを記録(直近5回分)
|
|
|
1047 |
|
1048 |
const currentTime = video.currentTime;
|
1049 |
|
1050 |
+
if (combinedAudioElement) {
|
1051 |
+
combinedAudioElement.currentTime = currentTime;
|
1052 |
+
if (isPlaying) {
|
1053 |
+
combinedAudioElement.play().catch(e => console.error('音声再生エラー:', e));
|
1054 |
+
}
|
1055 |
}
|
1056 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1057 |
// ズレ記録をリセット
|
1058 |
syncDriftLog = [];
|
1059 |
lastSyncTime = performance.now();
|
1060 |
}
|
1061 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1062 |
// 音声ファイルをロード
|
1063 |
function loadAudioFiles() {
|
1064 |
audioFiles.forEach(file => {
|
|
|
1086 |
|
1087 |
// 音声を結合する関数
|
1088 |
async function combineAudio() {
|
|
|
|
|
|
|
|
|
|
|
1089 |
combineButton.disabled = true;
|
1090 |
combineStatus.textContent = "音声を合成中...";
|
1091 |
|
|
|
1115 |
const maxDuration = Math.max(...buffers.filter(b => b).map(b => b.duration));
|
1116 |
|
1117 |
// 新しい音声バッファを作成
|
1118 |
+
const combinedAudioBuffer = audioContext.createBuffer(
|
1119 |
2, // ステレオ
|
1120 |
audioContext.sampleRate * maxDuration,
|
1121 |
audioContext.sampleRate
|
|
|
1160 |
}
|
1161 |
}
|
1162 |
|
1163 |
+
// AudioBufferをBlobに変換
|
1164 |
+
const blob = bufferToWave(combinedAudioBuffer);
|
1165 |
+
const url = URL.createObjectURL(blob);
|
1166 |
+
|
1167 |
+
// 新しいaudio要素を作成
|
1168 |
+
combinedAudioElement = new Audio(url);
|
1169 |
+
combinedAudioElement.preservesPitch = true;
|
1170 |
+
combinedAudioElement.playbackRate = currentPlaybackRate;
|
1171 |
+
|
1172 |
isAudioCombined = true;
|
1173 |
combineStatus.textContent = "音声の合成が完了しました";
|
1174 |
enablePlayerControls();
|
|
|
1185 |
}
|
1186 |
}
|
1187 |
|
1188 |
+
function bufferToWave(abuffer) {
|
1189 |
+
const numOfChan = abuffer.numberOfChannels,
|
1190 |
+
length = abuffer.length * numOfChan * 2 + 44,
|
1191 |
+
buffer = new ArrayBuffer(length),
|
1192 |
+
view = new DataView(buffer),
|
1193 |
+
channels = [],
|
1194 |
+
sampleRate = abuffer.sampleRate,
|
1195 |
+
offset = 0,
|
1196 |
+
pos = 0;
|
1197 |
+
|
1198 |
+
// write WAV header
|
1199 |
+
setUint32(0x46464952); // "RIFF"
|
1200 |
+
setUint32(length - 8); // file length - 8
|
1201 |
+
setUint32(0x45564157); // "WAVE"
|
1202 |
+
|
1203 |
+
setUint32(0x20746d66); // "fmt " chunk
|
1204 |
+
setUint32(16); // length = 16
|
1205 |
+
setUint16(1); // PCM (uncompressed)
|
1206 |
+
setUint16(numOfChan);
|
1207 |
+
setUint32(sampleRate);
|
1208 |
+
setUint32(sampleRate * 2 * numOfChan);
|
1209 |
+
setUint16(numOfChan * 2);
|
1210 |
+
setUint16(16);
|
1211 |
+
|
1212 |
+
setUint32(0x61746164); // "data" - chunk
|
1213 |
+
setUint32(length - pos - 4);
|
1214 |
+
|
1215 |
+
// write interleaved data
|
1216 |
+
for (let i = 0; i < abuffer.length; i++) {
|
1217 |
+
for (let channel = 0; channel < numOfChan; channel++) {
|
1218 |
+
let sample = abuffer.getChannelData(channel)[i] * 0x7fff;
|
1219 |
+
if (sample < -32768) sample = -32768;
|
1220 |
+
if (sample > 32767) sample = 32767;
|
1221 |
+
view.setInt16(pos, sample, true);
|
1222 |
+
pos += 2;
|
1223 |
+
}
|
1224 |
+
}
|
1225 |
+
|
1226 |
+
function setUint16(data) {
|
1227 |
+
view.setUint16(pos, data, true);
|
1228 |
+
pos += 2;
|
1229 |
+
}
|
1230 |
+
|
1231 |
+
function setUint32(data) {
|
1232 |
+
view.setUint32(pos, data, true);
|
1233 |
+
pos += 4;
|
1234 |
+
}
|
1235 |
+
|
1236 |
+
return new Blob([buffer], { type: 'audio/wav' });
|
1237 |
+
}
|
1238 |
+
|
1239 |
function applyVolume() {
|
1240 |
if (!isAudioCombined) return;
|
1241 |
|
|
|
1247 |
// 最終音量 (0-1)
|
1248 |
const finalVolume = Math.max(0, Math.min(1, baseVolume * globalVolume));
|
1249 |
|
1250 |
+
// 動画と音声の音量を設定
|
1251 |
video.volume = finalVolume;
|
1252 |
+
if (combinedAudioElement) {
|
1253 |
+
combinedAudioElement.volume = finalVolume;
|
1254 |
+
}
|
1255 |
|
1256 |
// 音量アイコンを更新
|
1257 |
updateVolumeIcon();
|
1258 |
}
|
1259 |
|
1260 |
+
function applyPlaybackRate() {
|
1261 |
+
if (!isAudioCombined) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1262 |
|
1263 |
+
const speed = parseFloat(playbackSpeedSlider.value);
|
1264 |
+
currentPlaybackRate = speed;
|
1265 |
+
video.playbackRate = speed;
|
|
|
1266 |
|
1267 |
+
if (combinedAudioElement) {
|
1268 |
+
combinedAudioElement.playbackRate = speed;
|
|
|
|
|
|
|
|
|
|
|
1269 |
}
|
1270 |
|
1271 |
+
speedValue.textContent = speed.toFixed(2) + 'x';
|
1272 |
+
playbackSpeedValue.textContent = speed.toFixed(2) + 'x';
|
1273 |
+
speedSlider.value = speed;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1274 |
}
|
1275 |
+
|
1276 |
// プレイヤーコントロールを有効化
|
1277 |
function enablePlayerControls() {
|
1278 |
disabledOverlay.style.display = 'none';
|
|
|
1292 |
|
1293 |
// ��レビュー再生
|
1294 |
function togglePreview() {
|
1295 |
+
if (!isAudioCombined || !combinedAudioElement) return;
|
1296 |
|
1297 |
if (previewButton.textContent === '▶') {
|
1298 |
// 再生
|
1299 |
+
combinedAudioElement.currentTime = 0;
|
1300 |
+
combinedAudioElement.play()
|
1301 |
+
.then(() => {
|
1302 |
+
previewButton.textContent = '⏸';
|
1303 |
+
|
1304 |
+
// プレビューの時間表示を更新
|
1305 |
+
const updatePreviewTime = () => {
|
1306 |
+
if (!combinedAudioElement || !isAudioCombined) return;
|
1307 |
+
|
1308 |
+
const currentTime = combinedAudioElement.currentTime;
|
1309 |
+
const duration = combinedAudioElement.duration;
|
1310 |
+
|
1311 |
+
if (currentTime >= duration) {
|
1312 |
+
previewButton.textContent = '▶';
|
1313 |
+
previewTime.textContent = `00:00 / ${formatTime(duration)}`;
|
1314 |
+
return;
|
1315 |
+
}
|
1316 |
+
|
1317 |
+
previewTime.textContent = `${formatTime(currentTime)} / ${formatTime(duration)}`;
|
1318 |
+
requestAnimationFrame(updatePreviewTime);
|
1319 |
+
};
|
1320 |
+
|
1321 |
+
updatePreviewTime();
|
1322 |
+
})
|
1323 |
+
.catch(e => console.error('プレビュー再生エラー:', e));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1324 |
} else {
|
1325 |
// 一時停止
|
1326 |
+
combinedAudioElement.pause();
|
|
|
|
|
|
|
1327 |
previewButton.textContent = '▶';
|
1328 |
}
|
1329 |
}
|
|
|
1440 |
playPauseBtn.textContent = '▶';
|
1441 |
stopSyncCheck();
|
1442 |
|
1443 |
+
if (combinedAudioElement) {
|
1444 |
+
combinedAudioElement.pause();
|
|
|
1445 |
}
|
1446 |
} catch (error) {
|
1447 |
console.error('メディア一時停止エラー:', error);
|
|
|
1488 |
const seekTime = Math.max(startTime, Math.min(time, endTime));
|
1489 |
video.currentTime = seekTime;
|
1490 |
|
1491 |
+
if (combinedAudioElement) {
|
1492 |
+
combinedAudioElement.currentTime = seekTime;
|
|
|
|
|
|
|
|
|
1493 |
if (isPlaying) {
|
1494 |
+
combinedAudioElement.play().catch(e => console.error('音声再生エラー:', e));
|
|
|
1495 |
}
|
1496 |
}
|
1497 |
} catch (error) {
|
|
|
1584 |
currentPlaybackRate = speed;
|
1585 |
video.playbackRate = speed;
|
1586 |
|
1587 |
+
if (combinedAudioElement) {
|
1588 |
+
combinedAudioElement.playbackRate = speed;
|
1589 |
}
|
1590 |
}
|
1591 |
|