soiz1 commited on
Commit
ae3e52a
·
1 Parent(s): 1e202f4

Update index.html

Browse files
Files changed (1) hide show
  1. 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 combinedAudioBuffer = null;
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 (combinedAudioSource) {
1214
- combinedAudioSource.stop();
1215
- combinedAudioSource = null;
1216
  }
1217
  });
1218
 
1219
  video.addEventListener('playing', function() {
1220
  isBuffering = false;
1221
  bufferingIndicator.style.display = 'none';
1222
- if (!combinedAudioSource && isPlaying) {
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 = audioContext.currentTime - (combinedAudioSource?.startTime || 0);
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 (combinedAudioSource) {
1279
- combinedAudioSource.stop();
 
 
 
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
- if (!isAudioCombined) return;
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
- combinedAudioSource = audioContext.createBufferSource();
1501
- combinedAudioSource.buffer = combinedAudioBuffer;
1502
- combinedAudioSource.connect(audioContext.destination);
1503
 
1504
- // ピッチ維持設定を一度だけ行う
1505
- if ('preservesPitch' in combinedAudioSource) {
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
- combinedAudioSource.playbackRate.value = speed;
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 || !combinedAudioBuffer) return;
1560
 
1561
  if (previewButton.textContent === '▶') {
1562
  // 再生
1563
- if (combinedAudioSource) {
1564
- combinedAudioSource.stop();
1565
- }
1566
-
1567
- combinedAudioSource = audioContext.createBufferSource();
1568
- combinedAudioSource.buffer = combinedAudioBuffer;
1569
- combinedAudioSource.connect(audioContext.destination);
1570
- combinedAudioSource.start(0);
1571
-
1572
- previewButton.textContent = '⏸';
1573
-
1574
- // プレビューの時間表示を更新
1575
- const updatePreviewTime = () => {
1576
- if (!combinedAudioSource || !isAudioCombined) return;
1577
-
1578
- const currentTime = audioContext.currentTime - combinedAudioSource.startTime;
1579
- const duration = combinedAudioBuffer.duration;
1580
-
1581
- if (currentTime >= duration) {
1582
- previewButton.textContent = '▶';
1583
- previewTime.textContent = `00:00 / ${formatTime(duration)}`;
1584
- return;
1585
- }
1586
-
1587
- previewTime.textContent = `${formatTime(currentTime)} / ${formatTime(duration)}`;
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
- if (combinedAudioSource) {
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 (combinedAudioSource) {
1720
- combinedAudioSource.stop();
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 (combinedAudioSource) {
1769
- combinedAudioSource.stop();
1770
- combinedAudioSource = audioContext.createBufferSource();
1771
- combinedAudioSource.buffer = combinedAudioBuffer;
1772
- combinedAudioSource.connect(audioContext.destination);
1773
-
1774
  if (isPlaying) {
1775
- combinedAudioSource.start(0, seekTime);
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 (combinedAudioSource) {
1870
- combinedAudioSource.playbackRate.value = speed;
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