soiz1 commited on
Commit
b16fc19
·
1 Parent(s): 4a5cd2d

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +374 -191
index.html CHANGED
@@ -520,6 +520,84 @@
520
  .time-set-button:hover {
521
  background-color: rgba(100, 255, 218, 0.2);
522
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
523
  </style>
524
  </head>
525
  <body>
@@ -542,6 +620,14 @@
542
 
543
  <div class="container">
544
  <div class="video-container" id="video-container">
 
 
 
 
 
 
 
 
545
  <video id="video" muted>
546
  <source src="v.mp4" type="video/mp4">
547
  </video>
@@ -551,17 +637,17 @@
551
  <div class="progress-time" id="progress-time">00:00</div>
552
  </div>
553
  <div class="main-controls">
554
- <button class="control-button" id="play-pause-btn">▶</button>
555
  <div class="time-display" id="time-display">00:00.00 / 00:00.00</div>
556
  <div class="volume-control">
557
- <button class="volume-button" id="volume-btn">🔊</button>
558
- <input type="range" class="volume-slider" id="volume-slider" min="0" max="1" step="0.01" value="1">
559
  </div>
560
  <div class="speed-control">
561
  <span class="speed-value" id="speed-value">1.00x</span>
562
- <input type="range" class="speed-slider" id="speed-slider" min="0.5" max="2" step="0.01" value="1">
563
  </div>
564
- <button class="control-button fullscreen-button" id="fullscreen-btn">⛶</button>
565
  </div>
566
  </div>
567
  </div>
@@ -573,32 +659,32 @@
573
  <div class="setting-item">
574
  <label for="start-time">再生開始秒数:</label>
575
  <div>
576
- <input type="number" id="start-time" min="0" value="0" step="0.01">
577
- <button class="time-set-button" id="set-start-time">現在の秒数に設定</button>
578
  </div>
579
  </div>
580
  <div class="setting-item">
581
  <label for="end-time">再生終了秒数:</label>
582
  <div>
583
- <input type="number" id="end-time" min="0" value="0" step="0.01">
584
- <button class="time-set-button" id="set-end-time">現在の秒数に設定</button>
585
  </div>
586
  </div>
587
  <div class="setting-item">
588
  <label for="loop">ループ再生:</label>
589
- <input type="checkbox" id="loop">
590
  </div>
591
  <div class="setting-item">
592
  <div class="global-volume-container">
593
  <label>全体音量係数:</label>
594
- <input type="range" class="global-volume-slider" id="global-volume" min="0" max="10" step="0.01" value="0.5">
595
  <span class="slider-value" id="global-volume-value">0.5</span>
596
  </div>
597
  </div>
598
  <div class="setting-item">
599
  <div class="playback-speed-container">
600
  <label>再生速度:</label>
601
- <input type="range" class="playback-speed-slider" id="playback-speed" min="0.5" max="2" step="0.01" value="1">
602
  <span class="slider-value" id="playback-speed-value">1.00x</span>
603
  </div>
604
  </div>
@@ -633,6 +719,18 @@
633
  <input type="range" class="audio-slider" data-audio="k" min="0" max="1" step="0.01" value="0">
634
  <span class="slider-value volume-value">0.00</span>
635
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
636
  </div>
637
  </div>
638
 
@@ -693,7 +791,7 @@
693
 
694
  // ローディング状態を管理
695
  let loadingCount = 0;
696
- let totalToLoad = 6;
697
  let lastUpdateTime = 0;
698
  const updateInterval = 1;
699
 
@@ -747,11 +845,20 @@
747
  const volumeValues = document.querySelectorAll('.volume-value');
748
  const setStartTimeBtn = document.getElementById('set-start-time');
749
  const setEndTimeBtn = document.getElementById('set-end-time');
 
 
 
 
 
 
750
 
751
  // 音声オブジェクトを作成
752
  const audioElements = {};
753
- const audioSources = {};
754
  const audioFiles = ['p', 'a', 't', 's', 'k'];
 
 
 
755
 
756
  // 初期化
757
  let videoDuration = 0;
@@ -762,42 +869,193 @@
762
  let lastSyncTime = 0;
763
  let syncDriftLog = [];
764
 
765
- // 同期チェック関数
766
- function checkSync() {
767
- if (!isPlaying) return;
768
-
769
- const now = performance.now();
770
- if (now - lastSyncTime < 500) return; // 0.5秒ごとにチェック
771
- lastSyncTime = now;
772
-
773
- const videoTime = video.currentTime;
774
- let maxDrift = 0;
775
-
776
  audioFiles.forEach(file => {
777
- if (audioElements[file]) {
778
- const audioTime = audioElements[file].currentTime;
779
- const drift = Math.abs(audioTime - videoTime);
 
 
780
 
781
- if (drift > maxDrift) maxDrift = drift;
 
 
 
782
 
783
- // 50ms以上のずれを修正
784
- if (drift > 0.05) {
785
- audioElements[file].currentTime = videoTime;
786
- console.log(`同期補正: ${file} (差: ${drift.toFixed(3)}秒)`);
787
- }
 
 
788
  }
789
  });
790
-
791
- syncDriftLog.push(maxDrift);
792
- if (syncDriftLog.length > 10) syncDriftLog.shift();
793
  }
794
-
795
- // 平均ずれを計算
796
- function getAverageDrift() {
797
- if (syncDriftLog.length === 0) return 0;
798
- return syncDriftLog.reduce((a, b) => a + b, 0) / syncDriftLog.length;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
799
  }
800
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
801
  // 動画のメタデータが読み込まれたら
802
  video.addEventListener('loadedmetadata', function() {
803
  try {
@@ -850,9 +1108,6 @@
850
 
851
  const progressPercent = (currentTime / duration) * 100;
852
  progressBar.style.width = `${progressPercent}%`;
853
-
854
- // 同期チェックを実行
855
- checkSync();
856
  } catch (error) {
857
  console.error('時間表示更新エラー:', error);
858
  }
@@ -886,25 +1141,29 @@
886
  isPlaying = true;
887
  playPauseBtn.textContent = '⏸';
888
 
889
- // 音声を再生 (Web Audio APIを使用)
890
- const currentTime = video.currentTime;
891
- audioFiles.forEach(file => {
892
- if (audioElements[file]) {
893
- audioElements[file].currentTime = currentTime;
894
- const playPromise = audioElements[file].play();
895
-
896
- if (playPromise !== undefined) {
897
- playPromise.catch(e => {
898
- if (e.name !== 'AbortError') {
899
- console.error(`音声再生エラー (${file}.mp3):`, e);
900
- }
901
- });
902
- }
903
- }
904
- });
905
 
906
- // 初回同期を即時実行
907
- setTimeout(checkSync, 100);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
908
  }).catch(error => {
909
  console.error('動画再生エラー:', error);
910
  });
@@ -921,11 +1180,10 @@
921
  isPlaying = false;
922
  playPauseBtn.textContent = '▶';
923
 
924
- audioFiles.forEach(file => {
925
- if (audioElements[file]) {
926
- audioElements[file].pause();
927
- }
928
- });
929
  } catch (error) {
930
  console.error('メディア一時停止エラー:', error);
931
  }
@@ -941,18 +1199,14 @@
941
  const startTime = parseFloat(startTimeInput.value) || 0;
942
  video.currentTime = startTime;
943
 
944
- audioFiles.forEach(file => {
945
- if (audioElements[file]) {
946
- audioElements[file].currentTime = startTime;
947
- if (isPlaying) {
948
- audioElements[file].play().catch(e => {
949
- if (e.name !== 'AbortError') {
950
- console.error(`音声再生エラー (${file}.mp3):`, e);
951
- }
952
- });
953
- }
954
- }
955
- });
956
  } else {
957
  pauseMedia();
958
  video.currentTime = endTime;
@@ -983,21 +1237,16 @@
983
  const seekTime = Math.max(startTime, Math.min(time, endTime));
984
  video.currentTime = seekTime;
985
 
986
- audioFiles.forEach(file => {
987
- if (audioElements[file]) {
988
- audioElements[file].currentTime = seekTime;
 
 
 
 
 
 
989
  }
990
- });
991
-
992
- // シーク後の同期を強化
993
- if (isPlaying) {
994
- setTimeout(() => {
995
- audioFiles.forEach(file => {
996
- if (audioElements[file]) {
997
- audioElements[file].currentTime = video.currentTime;
998
- }
999
- });
1000
- }, 50);
1001
  }
1002
  } catch (error) {
1003
  console.error('メディアシークエラー:', error);
@@ -1083,11 +1332,9 @@
1083
  currentPlaybackRate = speed;
1084
  video.playbackRate = speed;
1085
 
1086
- audioFiles.forEach(file => {
1087
- if (audioElements[file]) {
1088
- audioElements[file].playbackRate = speed;
1089
- }
1090
- });
1091
  }
1092
 
1093
  // 全画面ボタン
@@ -1135,41 +1382,15 @@
1135
  }
1136
  });
1137
 
1138
- // 音声ファイルをロード (改良版)
1139
- function loadAudioFiles() {
1140
- audioFiles.forEach(file => {
1141
- try {
1142
- const audio = new Audio(`${file}.mp3`);
1143
- audio.preload = 'auto';
1144
- audio.loop = false;
1145
- audioElements[file] = audio;
1146
-
1147
- audio.addEventListener('loadedmetadata', function() {
1148
- console.log(`${file}.mp3 loaded`);
1149
- checkLoadingComplete();
1150
- });
1151
-
1152
- audio.addEventListener('error', function() {
1153
- console.error(`音声ファイル読み込みエラー (${file}.mp3):`, audio.error);
1154
- checkLoadingComplete();
1155
- });
1156
- } catch (error) {
1157
- console.error(`音声ファイル初期化エラー (${file}.mp3):`, error);
1158
- checkLoadingComplete();
1159
- }
1160
- });
1161
- }
1162
-
1163
  // ボリュームスライダーのイベント
1164
  audioSliders.forEach((slider, index) => {
1165
  slider.addEventListener('input', function() {
1166
  const value = parseFloat(this.value);
1167
  volumeValues[index].textContent = value.toFixed(2);
1168
 
1169
- if (audioElements[this.dataset.audio]) {
1170
- const globalVolume = parseFloat(globalVolumeSlider.value) /10;
1171
- audioElements[this.dataset.audio].volume = value * globalVolume;
1172
- }
1173
  });
1174
  });
1175
 
@@ -1178,22 +1399,14 @@
1178
  const value = parseFloat(this.value);
1179
  globalVolumeValue.textContent = value.toFixed(1);
1180
 
1181
- audioFiles.forEach(file => {
1182
- if (audioElements[file]) {
1183
- const volumeSlider = document.querySelector(`.audio-slider[data-audio="${file}"]`);
1184
- const volume = parseFloat(volumeSlider.value) * (value /10);
1185
- audioElements[file].volume = volume;
1186
- }
1187
- });
1188
  });
1189
 
1190
  // ループ設定変更時
1191
  loopCheckbox.addEventListener('change', function() {
1192
- audioFiles.forEach(file => {
1193
- if (audioElements[file]) {
1194
- audioElements[file].loop = this.checked;
1195
- }
1196
- });
1197
  });
1198
 
1199
  // 現在の秒数を開始時間に設定
@@ -1206,27 +1419,19 @@
1206
  endTimeInput.value = video.currentTime.toFixed(2);
1207
  });
1208
 
1209
- // スライダーの背景を更新
1210
- function updateSliderBackgrounds() {
1211
- const volumePercent = volumeSlider.value * 100;
1212
- volumeSlider.style.backgroundSize = `${volumePercent}% 100%`;
1213
-
1214
- const speedPercent = (speedSlider.value - speedSlider.min) / (speedSlider.max - speedSlider.min) * 100;
1215
- speedSlider.style.backgroundSize = `${speedPercent}% 100%`;
1216
-
1217
- const globalVolumePercent = (globalVolumeSlider.value - globalVolumeSlider.min) / (globalVolumeSlider.max - globalVolumeSlider.min) * 100;
1218
- globalVolumeSlider.style.backgroundSize = `${globalVolumePercent}% 100%`;
1219
-
1220
- const playbackSpeedPercent = (playbackSpeedSlider.value - playbackSpeedSlider.min) / (playbackSpeedSlider.max - playbackSpeedSlider.min) * 100;
1221
- playbackSpeedSlider.style.backgroundSize = `${playbackSpeedPercent}% 100%`;
1222
-
1223
- audioSliders.forEach(slider => {
1224
- const percent = slider.value * 100;
1225
- slider.style.backgroundSize = `${percent}% 100%`;
1226
- });
1227
- }
1228
 
1229
- // スライダーの初期背景を設定
 
 
 
 
 
 
 
 
 
1230
  function initSliderBackgrounds() {
1231
  const sliders = [
1232
  volumeSlider,
@@ -1238,42 +1443,20 @@
1238
 
1239
  sliders.forEach(slider => {
1240
  if (slider) {
1241
- slider.style.backgroundImage = 'linear-gradient(#2d3d57, #2d3d57)';
1242
  slider.style.backgroundRepeat = 'no-repeat';
 
 
 
 
 
 
 
1243
  }
1244
  });
1245
-
1246
- updateSliderBackgrounds();
1247
  }
1248
 
1249
- // スライダー変更時に背景を更新
1250
- const allSliders = [
1251
- volumeSlider,
1252
- speedSlider,
1253
- globalVolumeSlider,
1254
- playbackSpeedSlider,
1255
- ...audioSliders
1256
- ];
1257
-
1258
- allSliders.forEach(slider => {
1259
- if (slider) {
1260
- slider.addEventListener('input', updateSliderBackgrounds);
1261
- }
1262
- });
1263
-
1264
- // 初期化
1265
- loadAudioFiles();
1266
- updateVolumeIcon();
1267
- volumeSlider.value = video.volume;
1268
- video.controls = false;
1269
  initSliderBackgrounds();
1270
-
1271
- // 定期的に同期状態をログ出力
1272
- setInterval(() => {
1273
- if (isPlaying) {
1274
- console.log(`平均同期ずれ: ${getAverageDrift().toFixed(4)}秒`);
1275
- }
1276
- }, 5000);
1277
  });
1278
  </script>
1279
  </body>
 
520
  .time-set-button:hover {
521
  background-color: rgba(100, 255, 218, 0.2);
522
  }
523
+
524
+ /* 合成ボタンスタイル */
525
+ .combine-button {
526
+ background-color: #64ffda;
527
+ color: #0a192f;
528
+ border: none;
529
+ padding: 10px 20px;
530
+ border-radius: 5px;
531
+ font-size: 16px;
532
+ cursor: pointer;
533
+ margin-top: 20px;
534
+ transition: all 0.3s;
535
+ font-weight: bold;
536
+ }
537
+
538
+ .combine-button:hover {
539
+ background-color: #52e0c4;
540
+ transform: translateY(-2px);
541
+ box-shadow: 0 5px 15px rgba(100, 255, 218, 0.4);
542
+ }
543
+
544
+ .combine-button:disabled {
545
+ background-color: #3a5a78;
546
+ cursor: not-allowed;
547
+ transform: none;
548
+ box-shadow: none;
549
+ }
550
+
551
+ /* 合成ステータスメッセージ */
552
+ .combine-status {
553
+ margin-top: 10px;
554
+ color: #64ffda;
555
+ font-size: 14px;
556
+ height: 20px;
557
+ }
558
+
559
+ /* プレビューセクション */
560
+ .preview-section {
561
+ margin-top: 20px;
562
+ padding: 15px;
563
+ background-color: rgba(17, 34, 64, 0.7);
564
+ border-radius: 5px;
565
+ display: none;
566
+ }
567
+
568
+ .preview-section h3 {
569
+ margin-top: 0;
570
+ color: #64ffda;
571
+ border-bottom: 1px solid #64ffda;
572
+ padding-bottom: 5px;
573
+ }
574
+
575
+ /* 無効状態のオーバーレイ */
576
+ .disabled-overlay {
577
+ position: absolute;
578
+ top: 0;
579
+ left: 0;
580
+ width: 100%;
581
+ height: 100%;
582
+ background-color: rgba(10, 25, 47, 0.7);
583
+ display: flex;
584
+ justify-content: center;
585
+ align-items: center;
586
+ z-index: 10;
587
+ border-radius: 5px;
588
+ }
589
+
590
+ .disabled-message {
591
+ background-color: rgba(30, 42, 71, 0.9);
592
+ padding: 20px;
593
+ border-radius: 5px;
594
+ text-align: center;
595
+ max-width: 80%;
596
+ }
597
+
598
+ .disabled-message p {
599
+ margin-bottom: 15px;
600
+ }
601
  </style>
602
  </head>
603
  <body>
 
620
 
621
  <div class="container">
622
  <div class="video-container" id="video-container">
623
+ <!-- 無効状態のオーバーレイ -->
624
+ <div class="disabled-overlay" id="disabledOverlay">
625
+ <div class="disabled-message">
626
+ <p>音声の合成が完了していません</p>
627
+ <p>下の音声コントロールで各パートの音量を調整し、「合成」ボタンを押してください</p>
628
+ </div>
629
+ </div>
630
+
631
  <video id="video" muted>
632
  <source src="v.mp4" type="video/mp4">
633
  </video>
 
637
  <div class="progress-time" id="progress-time">00:00</div>
638
  </div>
639
  <div class="main-controls">
640
+ <button class="control-button" id="play-pause-btn" disabled>▶</button>
641
  <div class="time-display" id="time-display">00:00.00 / 00:00.00</div>
642
  <div class="volume-control">
643
+ <button class="volume-button" id="volume-btn" disabled>🔊</button>
644
+ <input type="range" class="volume-slider" id="volume-slider" min="0" max="1" step="0.01" value="1" disabled>
645
  </div>
646
  <div class="speed-control">
647
  <span class="speed-value" id="speed-value">1.00x</span>
648
+ <input type="range" class="speed-slider" id="speed-slider" min="0.5" max="2" step="0.01" value="1" disabled>
649
  </div>
650
+ <button class="control-button fullscreen-button" id="fullscreen-btn" disabled>⛶</button>
651
  </div>
652
  </div>
653
  </div>
 
659
  <div class="setting-item">
660
  <label for="start-time">再生開始秒数:</label>
661
  <div>
662
+ <input type="number" id="start-time" min="0" value="0" step="0.01" disabled>
663
+ <button class="time-set-button" id="set-start-time" disabled>現在の秒数に設定</button>
664
  </div>
665
  </div>
666
  <div class="setting-item">
667
  <label for="end-time">再生終了秒数:</label>
668
  <div>
669
+ <input type="number" id="end-time" min="0" value="0" step="0.01" disabled>
670
+ <button class="time-set-button" id="set-end-time" disabled>現在の秒数に設定</button>
671
  </div>
672
  </div>
673
  <div class="setting-item">
674
  <label for="loop">ループ再生:</label>
675
+ <input type="checkbox" id="loop" disabled>
676
  </div>
677
  <div class="setting-item">
678
  <div class="global-volume-container">
679
  <label>全体音量係数:</label>
680
+ <input type="range" class="global-volume-slider" id="global-volume" min="0" max="10" step="0.01" value="0.5" disabled>
681
  <span class="slider-value" id="global-volume-value">0.5</span>
682
  </div>
683
  </div>
684
  <div class="setting-item">
685
  <div class="playback-speed-container">
686
  <label>再生速度:</label>
687
+ <input type="range" class="playback-speed-slider" id="playback-speed" min="0.5" max="2" step="0.01" value="1" disabled>
688
  <span class="slider-value" id="playback-speed-value">1.00x</span>
689
  </div>
690
  </div>
 
719
  <input type="range" class="audio-slider" data-audio="k" min="0" max="1" step="0.01" value="0">
720
  <span class="slider-value volume-value">0.00</span>
721
  </div>
722
+
723
+ <!-- 合成ボタンとステータス -->
724
+ <button class="combine-button" id="combine-button">音声を合成</button>
725
+ <div class="combine-status" id="combine-status"></div>
726
+ </div>
727
+
728
+ <!-- プレビューセクション -->
729
+ <div class="preview-section" id="preview-section">
730
+ <h3>プレビュー</h3>
731
+ <p>合成された音声をプレビューできます。再生ボタンをクリックして確認してください。</p>
732
+ <button class="control-button" id="preview-button">▶</button>
733
+ <span id="preview-time">00:00 / 00:00</span>
734
  </div>
735
  </div>
736
 
 
791
 
792
  // ローディング状態を管理
793
  let loadingCount = 0;
794
+ let totalToLoad = 6; // 動画 + 5つの音声ファイル
795
  let lastUpdateTime = 0;
796
  const updateInterval = 1;
797
 
 
845
  const volumeValues = document.querySelectorAll('.volume-value');
846
  const setStartTimeBtn = document.getElementById('set-start-time');
847
  const setEndTimeBtn = document.getElementById('set-end-time');
848
+ const disabledOverlay = document.getElementById('disabledOverlay');
849
+ const combineButton = document.getElementById('combine-button');
850
+ const combineStatus = document.getElementById('combine-status');
851
+ const previewSection = document.getElementById('preview-section');
852
+ const previewButton = document.getElementById('preview-button');
853
+ const previewTime = document.getElementById('preview-time');
854
 
855
  // 音声オブジェクトを作成
856
  const audioElements = {};
857
+ const audioBuffers = {};
858
  const audioFiles = ['p', 'a', 't', 's', 'k'];
859
+ let combinedAudioBuffer = null;
860
+ let combinedAudioSource = null;
861
+ let isAudioCombined = false;
862
 
863
  // 初期化
864
  let videoDuration = 0;
 
869
  let lastSyncTime = 0;
870
  let syncDriftLog = [];
871
 
872
+ // 音声ファイルをロード (改良版)
873
+ function loadAudioFiles() {
 
 
 
 
 
 
 
 
 
874
  audioFiles.forEach(file => {
875
+ try {
876
+ const audio = new Audio(`${file}.mp3`);
877
+ audio.preload = 'auto';
878
+ audio.loop = false;
879
+ audioElements[file] = audio;
880
 
881
+ audio.addEventListener('loadedmetadata', function() {
882
+ console.log(`${file}.mp3 loaded`);
883
+ checkLoadingComplete();
884
+ });
885
 
886
+ audio.addEventListener('error', function() {
887
+ console.error(`音声ファイル読み込みエラー (${file}.mp3):`, audio.error);
888
+ checkLoadingComplete();
889
+ });
890
+ } catch (error) {
891
+ console.error(`音声ファイル初期化エラー (${file}.mp3):`, error);
892
+ checkLoadingComplete();
893
  }
894
  });
 
 
 
895
  }
896
+
897
+ // 音声を結合する関数
898
+ async function combineAudio() {
899
+ if (!audioContext) {
900
+ combineStatus.textContent = "Web Audio APIが利用できません";
901
+ return;
902
+ }
903
+
904
+ combineButton.disabled = true;
905
+ combineStatus.textContent = "音声を合成中...";
906
+
907
+ try {
908
+ // 各音声ファイルをデコード
909
+ const audioBufferPromises = audioFiles.map(async file => {
910
+ const audio = audioElements[file];
911
+ if (!audio) return null;
912
+
913
+ const response = await fetch(`${file}.mp3`);
914
+ const arrayBuffer = await response.arrayBuffer();
915
+ return await audioContext.decodeAudioData(arrayBuffer);
916
+ });
917
+
918
+ // すべての音声バッファを取得
919
+ const buffers = await Promise.all(audioBufferPromises);
920
+ audioFiles.forEach((file, index) => {
921
+ audioBuffers[file] = buffers[index];
922
+ });
923
+
924
+ // 最長の音声バッファの長さを取得
925
+ const maxDuration = Math.max(...buffers.filter(b => b).map(b => b.duration));
926
+
927
+ // 新しい音声バッファを作成
928
+ combinedAudioBuffer = audioContext.createBuffer(
929
+ 2, // ステレオ
930
+ audioContext.sampleRate * maxDuration,
931
+ audioContext.sampleRate
932
+ );
933
+
934
+ // 各音声バッファを結合
935
+ for (let file of audioFiles) {
936
+ if (!audioBuffers[file]) continue;
937
+
938
+ const buffer = audioBuffers[file];
939
+ const volume = parseFloat(document.querySelector(`.audio-slider[data-audio="${file}"]`).value);
940
+
941
+ // 音量が0の場合はスキップ
942
+ if (volume === 0) continue;
943
+
944
+ // 各チャンネルに音声を加算
945
+ for (let channel = 0; channel < 2; channel++) {
946
+ const inputData = buffer.getChannelData(channel % buffer.numberOfChannels);
947
+ const outputData = combinedAudioBuffer.getChannelData(channel);
948
+
949
+ for (let i = 0; i < inputData.length; i++) {
950
+ outputData[i] += inputData[i] * volume;
951
+ }
952
+ }
953
+ }
954
+
955
+ // 音量を正規化 (クリッピング防止)
956
+ for (let channel = 0; channel < 2; channel++) {
957
+ const outputData = combinedAudioBuffer.getChannelData(channel);
958
+ let max = 0;
959
+
960
+ for (let i = 0; i < outputData.length; i++) {
961
+ if (Math.abs(outputData[i]) > max) {
962
+ max = Math.abs(outputData[i]);
963
+ }
964
+ }
965
+
966
+ if (max > 1) {
967
+ for (let i = 0; i < outputData.length; i++) {
968
+ outputData[i] /= max;
969
+ }
970
+ }
971
+ }
972
+
973
+ isAudioCombined = true;
974
+ combineStatus.textContent = "音声の合成が完了しました";
975
+ enablePlayerControls();
976
+ previewSection.style.display = 'block';
977
+
978
+ } catch (error) {
979
+ console.error('音声合成エラー:', error);
980
+ combineStatus.textContent = "音声の合成に失敗しました";
981
+ combineButton.disabled = false;
982
+ }
983
  }
984
+
985
+ // プレイヤーコントロールを有効化
986
+ function enablePlayerControls() {
987
+ disabledOverlay.style.display = 'none';
988
+ playPauseBtn.disabled = false;
989
+ volumeBtn.disabled = false;
990
+ volumeSlider.disabled = false;
991
+ speedSlider.disabled = false;
992
+ fullscreenBtn.disabled = false;
993
+ startTimeInput.disabled = false;
994
+ endTimeInput.disabled = false;
995
+ loopCheckbox.disabled = false;
996
+ globalVolumeSlider.disabled = false;
997
+ setStartTimeBtn.disabled = false;
998
+ setEndTimeBtn.disabled = false;
999
+ playbackSpeedSlider.disabled = false;
1000
+ }
1001
+
1002
+ // プレビュー再生
1003
+ function togglePreview() {
1004
+ if (!isAudioCombined || !combinedAudioBuffer) return;
1005
+
1006
+ if (previewButton.textContent === '▶') {
1007
+ // 再生
1008
+ if (combinedAudioSource) {
1009
+ combinedAudioSource.stop();
1010
+ }
1011
+
1012
+ combinedAudioSource = audioContext.createBufferSource();
1013
+ combinedAudioSource.buffer = combinedAudioBuffer;
1014
+ combinedAudioSource.connect(audioContext.destination);
1015
+ combinedAudioSource.start(0);
1016
+
1017
+ previewButton.textContent = '⏸';
1018
+
1019
+ // プレビューの時間表示を更新
1020
+ const updatePreviewTime = () => {
1021
+ if (!combinedAudioSource || !isAudioCombined) return;
1022
+
1023
+ const currentTime = audioContext.currentTime - combinedAudioSource.startTime;
1024
+ const duration = combinedAudioBuffer.duration;
1025
+
1026
+ if (currentTime >= duration) {
1027
+ previewButton.textContent = '▶';
1028
+ previewTime.textContent = `00:00 / ${formatTime(duration)}`;
1029
+ return;
1030
+ }
1031
+
1032
+ previewTime.textContent = `${formatTime(currentTime)} / ${formatTime(duration)}`;
1033
+ requestAnimationFrame(updatePreviewTime);
1034
+ };
1035
+
1036
+ updatePreviewTime();
1037
+
1038
+ combinedAudioSource.onended = () => {
1039
+ previewButton.textContent = '▶';
1040
+ previewTime.textContent = `00:00 / ${formatTime(combinedAudioBuffer.duration)}`;
1041
+ };
1042
+ } else {
1043
+ // 一時停止
1044
+ if (combinedAudioSource) {
1045
+ combinedAudioSource.stop();
1046
+ combinedAudioSource = null;
1047
+ }
1048
+ previewButton.textContent = '▶';
1049
+ }
1050
+ }
1051
+
1052
+ // 時間をフォーマットするヘルパー関数
1053
+ function formatTime(seconds) {
1054
+ const mins = Math.floor(seconds / 60);
1055
+ const secs = Math.floor(seconds % 60);
1056
+ return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
1057
+ }
1058
+
1059
  // 動画のメタデータが読み込まれたら
1060
  video.addEventListener('loadedmetadata', function() {
1061
  try {
 
1108
 
1109
  const progressPercent = (currentTime / duration) * 100;
1110
  progressBar.style.width = `${progressPercent}%`;
 
 
 
1111
  } catch (error) {
1112
  console.error('時間表示更新エラー:', error);
1113
  }
 
1141
  isPlaying = true;
1142
  playPauseBtn.textContent = '⏸';
1143
 
1144
+ // Web Audio APIで合成音声を再生
1145
+ if (combinedAudioSource) {
1146
+ combinedAudioSource.stop();
1147
+ }
 
 
 
 
 
 
 
 
 
 
 
 
1148
 
1149
+ combinedAudioSource = audioContext.createBufferSource();
1150
+ combinedAudioSource.buffer = combinedAudioBuffer;
1151
+ combinedAudioSource.connect(audioContext.destination);
1152
+ combinedAudioSource.start(0, video.currentTime);
1153
+
1154
+ // 再生速度を設定
1155
+ video.playbackRate = currentPlaybackRate;
1156
+ combinedAudioSource.playbackRate.value = currentPlaybackRate;
1157
+
1158
+ // 動画と音声の同期を維持
1159
+ combinedAudioSource.onended = () => {
1160
+ if (loopCheckbox.checked) {
1161
+ video.currentTime = startTime;
1162
+ playMedia();
1163
+ } else {
1164
+ pauseMedia();
1165
+ }
1166
+ };
1167
  }).catch(error => {
1168
  console.error('動画再生エラー:', error);
1169
  });
 
1180
  isPlaying = false;
1181
  playPauseBtn.textContent = '▶';
1182
 
1183
+ if (combinedAudioSource) {
1184
+ combinedAudioSource.stop();
1185
+ combinedAudioSource = null;
1186
+ }
 
1187
  } catch (error) {
1188
  console.error('メディア一時停止エラー:', error);
1189
  }
 
1199
  const startTime = parseFloat(startTimeInput.value) || 0;
1200
  video.currentTime = startTime;
1201
 
1202
+ if (combinedAudioSource) {
1203
+ combinedAudioSource.stop();
1204
+ combinedAudioSource = audioContext.createBufferSource();
1205
+ combinedAudioSource.buffer = combinedAudioBuffer;
1206
+ combinedAudioSource.connect(audioContext.destination);
1207
+ combinedAudioSource.start(0, startTime);
1208
+ combinedAudioSource.playbackRate.value = currentPlaybackRate;
1209
+ }
 
 
 
 
1210
  } else {
1211
  pauseMedia();
1212
  video.currentTime = endTime;
 
1237
  const seekTime = Math.max(startTime, Math.min(time, endTime));
1238
  video.currentTime = seekTime;
1239
 
1240
+ if (combinedAudioSource) {
1241
+ combinedAudioSource.stop();
1242
+ combinedAudioSource = audioContext.createBufferSource();
1243
+ combinedAudioBuffer = combinedAudioBuffer;
1244
+ combinedAudioSource.connect(audioContext.destination);
1245
+
1246
+ if (isPlaying) {
1247
+ combinedAudioSource.start(0, seekTime);
1248
+ combinedAudioSource.playbackRate.value = currentPlaybackRate;
1249
  }
 
 
 
 
 
 
 
 
 
 
 
1250
  }
1251
  } catch (error) {
1252
  console.error('メディアシークエラー:', error);
 
1332
  currentPlaybackRate = speed;
1333
  video.playbackRate = speed;
1334
 
1335
+ if (combinedAudioSource) {
1336
+ combinedAudioSource.playbackRate.value = speed;
1337
+ }
 
 
1338
  }
1339
 
1340
  // 全画面ボタン
 
1382
  }
1383
  });
1384
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1385
  // ボリュームスライダーのイベント
1386
  audioSliders.forEach((slider, index) => {
1387
  slider.addEventListener('input', function() {
1388
  const value = parseFloat(this.value);
1389
  volumeValues[index].textContent = value.toFixed(2);
1390
 
1391
+ // スライダーの背景を更新
1392
+ const percent = value * 100;
1393
+ this.style.backgroundSize = `${percent}% 100%`;
 
1394
  });
1395
  });
1396
 
 
1399
  const value = parseFloat(this.value);
1400
  globalVolumeValue.textContent = value.toFixed(1);
1401
 
1402
+ // スライダーの背景を更新
1403
+ const percent = (value - this.min) / (this.max - this.min) * 100;
1404
+ this.style.backgroundSize = `${percent}% 100%`;
 
 
 
 
1405
  });
1406
 
1407
  // ループ設定変更時
1408
  loopCheckbox.addEventListener('change', function() {
1409
+ // 合成音声ではループは動画に依存する
 
 
 
 
1410
  });
1411
 
1412
  // 現在の秒数を開始時間に設定
 
1419
  endTimeInput.value = video.currentTime.toFixed(2);
1420
  });
1421
 
1422
+ // 合成ボタンクリック
1423
+ combineButton.addEventListener('click', combineAudio);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1424
 
1425
+ // プレビューボタンクリック
1426
+ previewButton.addEventListener('click', togglePreview);
1427
+
1428
+ // 初期化
1429
+ loadAudioFiles();
1430
+ updateVolumeIcon();
1431
+ volumeSlider.value = video.volume;
1432
+ video.controls = false;
1433
+
1434
+ // スライダーの背景を初期化
1435
  function initSliderBackgrounds() {
1436
  const sliders = [
1437
  volumeSlider,
 
1443
 
1444
  sliders.forEach(slider => {
1445
  if (slider) {
1446
+ slider.style.backgroundImage = 'linear-gradient(#64ffda, #64ffda)';
1447
  slider.style.backgroundRepeat = 'no-repeat';
1448
+
1449
+ if (slider === globalVolumeSlider) {
1450
+ const percent = (slider.value - slider.min) / (slider.max - slider.min) * 100;
1451
+ slider.style.backgroundSize = `${percent}% 100%`;
1452
+ } else {
1453
+ slider.style.backgroundSize = `${slider.value * 100}% 100%`;
1454
+ }
1455
  }
1456
  });
 
 
1457
  }
1458
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1459
  initSliderBackgrounds();
 
 
 
 
 
 
 
1460
  });
1461
  </script>
1462
  </body>