soiz1 commited on
Commit
78f6137
·
1 Parent(s): b16fc19

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +691 -635
index.html CHANGED
@@ -735,729 +735,785 @@
735
  </div>
736
 
737
  <script>
738
- document.addEventListener('DOMContentLoaded', function() {
739
- // テクノロジー風背景を生成
740
- function createTechBackground() {
741
- const bg = document.getElementById('techBg');
 
 
 
 
742
 
743
- for (let i = 0; i < 200; i++) {
744
- const line = document.createElement('div');
745
- line.className = 'circuit-line';
746
-
747
- const isHorizontal = Math.random() > 0.5;
748
- if (isHorizontal) {
749
- line.style.width = `${Math.random() * 300 + 100}px`;
750
- line.style.height = '1px';
751
- } else {
752
- line.style.width = '1px';
753
- line.style.height = `${Math.random() * 300 + 100}px`;
754
- }
755
-
756
- line.style.left = `${Math.random() * 100}%`;
757
- line.style.top = `${Math.random() * 100}%`;
758
- line.style.opacity = Math.random() * 0.5 + 0.1;
759
- bg.appendChild(line);
760
- }
761
-
762
- for (let i = 0; i < 200; i++) {
763
- const dot = document.createElement('div');
764
- dot.className = 'grid-dot';
765
- dot.style.left = `${Math.random() * 100}%`;
766
- dot.style.top = `${Math.random() * 100}%`;
767
- bg.appendChild(dot);
768
  }
 
 
 
 
 
 
769
 
770
- for (let i = 0; i < 15; i++) {
771
- const hex = document.createElement('div');
772
- hex.className = 'hexagon';
773
- hex.style.left = `${Math.random() * 100}%`;
774
- hex.style.top = `${Math.random() * 100}%`;
775
- hex.style.transform = `rotate(${Math.random() * 360}deg)`;
776
- hex.style.animation = `float ${Math.random() * 10 + 5}s infinite ease-in-out`;
777
- bg.appendChild(hex);
778
- }
779
 
780
- for (let i = 0; i < 8; i++) {
781
- const pulse = document.createElement('div');
782
- pulse.className = 'pulse';
783
- pulse.style.left = `${Math.random() * 100}%`;
784
- pulse.style.top = `${Math.random() * 100}%`;
785
- pulse.style.animationDelay = `${Math.random() * 2}s`;
786
- bg.appendChild(pulse);
787
- }
788
  }
789
-
790
- createTechBackground();
791
-
792
- // ローディング状態を管理
793
- let loadingCount = 0;
794
- let totalToLoad = 6; // 動画 + 5つの音声ファイル
795
- let lastUpdateTime = 0;
796
- const updateInterval = 1;
797
-
798
- function checkLoadingComplete() {
799
- loadingCount++;
800
- if (loadingCount >= totalToLoad) {
 
 
 
 
 
 
 
 
 
 
 
 
 
801
  setTimeout(function() {
802
- const loadingOverlay = document.getElementById('loadingOverlay');
803
- loadingOverlay.style.opacity = '0';
804
- setTimeout(function() {
805
- loadingOverlay.style.display = 'none';
806
- }, 1000);
807
- }, 500);
808
- }
809
- }
810
-
811
- function handleError(error, message) {
812
- console.error(message, error);
813
- window.alert(`${message}\n\nエラー詳細: ${error.message}`);
814
- }
815
-
816
- // Web Audio Context の初期化
817
- let audioContext;
818
- try {
819
- audioContext = new (window.AudioContext || window.webkitAudioContext)();
820
- } catch (e) {
821
- console.error('Web Audio APIがサポートされていません:', e);
822
- }
823
-
824
- // 要素を取得
825
- const video = document.getElementById('video');
826
- const videoContainer = document.getElementById('video-container');
827
- const playPauseBtn = document.getElementById('play-pause-btn');
828
- const timeDisplay = document.getElementById('time-display');
829
- const progressContainer = document.getElementById('progress-container');
830
- const progressBar = document.getElementById('progress-bar');
831
- const progressTime = document.getElementById('progress-time');
832
- const volumeBtn = document.getElementById('volume-btn');
833
- const volumeSlider = document.getElementById('volume-slider');
834
- const speedSlider = document.getElementById('speed-slider');
835
- const speedValue = document.getElementById('speed-value');
836
- const playbackSpeedSlider = document.getElementById('playback-speed');
837
- const playbackSpeedValue = document.getElementById('playback-speed-value');
838
- const fullscreenBtn = document.getElementById('fullscreen-btn');
839
- const startTimeInput = document.getElementById('start-time');
840
- const endTimeInput = document.getElementById('end-time');
841
- const loopCheckbox = document.getElementById('loop');
842
- const globalVolumeSlider = document.getElementById('global-volume');
843
- const globalVolumeValue = document.getElementById('global-volume-value');
844
- const audioSliders = document.querySelectorAll('.audio-slider');
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;
865
- let isPlaying = false;
866
- let lastVolume = 1;
867
- let currentPlaybackRate = 1;
868
- let isFullscreen = false;
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 {
1062
- videoDuration = video.duration;
1063
- endTimeInput.value = videoDuration.toFixed(2);
1064
- endTimeInput.max = videoDuration;
1065
- startTimeInput.max = videoDuration - 0.1;
1066
- updateTimeDisplay();
1067
- checkLoadingComplete();
1068
- } catch (error) {
1069
- handleError(error, '動画メタデータ読み込み中にエラーが発生しました');
1070
- }
1071
- });
1072
 
1073
- // 動画エラー処理
1074
- video.addEventListener('error', function() {
1075
- handleError(video.error, '動画読み込み中にエラーが発生しました');
1076
- });
 
 
 
 
 
1077
 
1078
- // 再生ボタンクリック
1079
- playPauseBtn.addEventListener('click', function() {
1080
- const endTime = parseFloat(endTimeInput.value) || videoDuration;
1081
- if (video.currentTime >= endTime) {
1082
- const startTime = parseFloat(startTimeInput.value) || 0;
1083
- seekMedia(startTime);
1084
- }
1085
- togglePlayPause();
1086
- });
1087
 
1088
- // 時間表示を更新
1089
- function updateTimeDisplay() {
1090
- const now = performance.now();
1091
- if (now - lastUpdateTime < updateInterval && !isFullscreen) return;
1092
- lastUpdateTime = now;
1093
-
1094
- try {
1095
- const currentTime = video.currentTime;
1096
- const duration = video.duration || videoDuration;
1097
-
1098
- const currentMinutes = Math.floor(currentTime / 60);
1099
- const currentSeconds = Math.floor(currentTime % 60);
1100
- const currentMilliseconds = Math.floor((currentTime % 1) * 100);
1101
- const durationMinutes = Math.floor(duration / 60);
1102
- const durationSeconds = Math.floor(duration % 60);
1103
- const durationMilliseconds = Math.floor((duration % 1) * 100);
1104
-
1105
- timeDisplay.textContent =
1106
- `${String(currentMinutes).padStart(2, '0')}:${String(currentSeconds).padStart(2, '0')}.${String(currentMilliseconds).padStart(2, '0')} / ` +
1107
- `${String(durationMinutes).padStart(2, '0')}:${String(durationSeconds).padStart(2, '0')}.${String(durationMilliseconds).padStart(2, '0')}`;
1108
-
1109
- const progressPercent = (currentTime / duration) * 100;
1110
- progressBar.style.width = `${progressPercent}%`;
1111
- } catch (error) {
1112
- console.error('時間表示更新エラー:', error);
1113
- }
1114
  }
1115
 
1116
- // 再生/一時停止をトグル
1117
- function togglePlayPause() {
1118
- if (isPlaying) {
1119
- pauseMedia();
1120
- } else {
1121
- playMedia();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1122
  }
1123
- }
1124
-
1125
- // 再生関数 (改良版)
1126
- function playMedia() {
1127
- try {
1128
- const duration = video.duration || videoDuration;
1129
- const startTime = parseFloat(startTimeInput.value) || 0;
1130
- const endTime = parseFloat(endTimeInput.value) || duration;
1131
-
1132
- if (video.currentTime >= endTime) {
1133
- video.currentTime = startTime;
1134
- }
1135
 
1136
- // 動画を再生
1137
- const playPromise = video.play();
1138
 
1139
- if (playPromise !== undefined) {
1140
- playPromise.then(() => {
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
- });
1170
  }
1171
- } catch (error) {
1172
- console.error('メディア再生エラー:', error);
1173
- }
1174
- }
1175
-
1176
- // 一時停止関数
1177
- function pauseMedia() {
1178
- try {
1179
- video.pause();
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
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1190
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1191
 
1192
- // 時間更新時の処理
1193
- video.addEventListener('timeupdate', function() {
1194
  const duration = video.duration || videoDuration;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1195
  const endTime = parseFloat(endTimeInput.value) || duration;
1196
 
1197
- if (video.currentTime >= endTime && endTime > 0) {
1198
- if (loopCheckbox.checked) {
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;
1213
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1214
  }
 
 
 
 
 
 
 
 
 
 
 
1215
 
1216
- updateTimeDisplay();
1217
- });
1218
-
1219
- // プログレスバークリックでシーク
1220
- progressContainer.addEventListener('click', function(e) {
1221
- if (!video.duration) return;
1222
-
1223
- const rect = this.getBoundingClientRect();
1224
- const pos = (e.clientX - rect.left) / rect.width;
1225
- const seekTime = pos * video.duration;
1226
-
1227
- seekMedia(seekTime);
1228
- });
1229
 
1230
- // 指定した時間にシーク (改良版)
1231
- function seekMedia(time) {
1232
- try {
1233
- const duration = video.duration || videoDuration;
1234
  const startTime = parseFloat(startTimeInput.value) || 0;
1235
- const endTime = parseFloat(endTimeInput.value) || duration;
1236
-
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);
1253
- }
1254
- }
1255
-
1256
- // プログレスバー上でマウス移動時に時間を表示
1257
- progressContainer.addEventListener('mousemove', function(e) {
1258
- if (!video.duration) return;
1259
-
1260
- const rect = this.getBoundingClientRect();
1261
- const pos = (e.clientX - rect.left) / rect.width;
1262
- const hoverTime = pos * video.duration;
1263
-
1264
- const minutes = Math.floor(hoverTime / 60);
1265
- const seconds = Math.floor(hoverTime % 60);
1266
- const milliseconds = Math.floor((hoverTime % 1) * 100);
1267
-
1268
- progressTime.textContent = `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}.${String(milliseconds).padStart(2, '0')}`;
1269
- progressTime.style.display = 'block';
1270
- progressTime.style.left = `${pos * 100}%`;
1271
- });
1272
-
1273
- progressContainer.addEventListener('mouseleave', function() {
1274
- progressTime.style.display = 'none';
1275
- });
1276
-
1277
- // 動画クリックで再生/一時停止
1278
- video.addEventListener('click', function() {
1279
- togglePlayPause();
1280
- });
1281
-
1282
- // 音量コントロール
1283
- volumeSlider.addEventListener('input', function() {
1284
- video.volume = this.value;
1285
- lastVolume = this.value;
1286
- updateVolumeIcon();
1287
- });
1288
-
1289
- // 音量ボタン
1290
- volumeBtn.addEventListener('click', function() {
1291
- if (video.volume > 0) {
1292
- lastVolume = video.volume;
1293
- video.volume = 0;
1294
- volumeSlider.value = 0;
1295
- } else {
1296
- video.volume = lastVolume;
1297
- volumeSlider.value = lastVolume;
1298
- }
1299
- updateVolumeIcon();
1300
- });
1301
-
1302
- // 音量アイコンを更新
1303
- function updateVolumeIcon() {
1304
- if (video.volume === 0) {
1305
- volumeBtn.textContent = '🔇';
1306
- } else if (video.volume < 0.5) {
1307
- volumeBtn.textContent = '🔈';
1308
  } else {
1309
- volumeBtn.textContent = '🔊';
 
1310
  }
1311
  }
1312
 
1313
- // 再生速度スライダー (動画プレイヤー)
1314
- speedSlider.addEventListener('input', function() {
1315
- const speed = parseFloat(this.value);
1316
- speedValue.textContent = speed.toFixed(2) + 'x';
1317
- playbackSpeedSlider.value = speed;
1318
- playbackSpeedValue.textContent = speed.toFixed(2) + 'x';
1319
- updatePlaybackRate(speed);
1320
- });
1321
 
1322
- // 再生速度スライダー (設定メニュー)
1323
- playbackSpeedSlider.addEventListener('input', function() {
1324
- const speed = parseFloat(this.value);
1325
- playbackSpeedValue.textContent = speed.toFixed(2) + 'x';
1326
- speedSlider.value = speed;
1327
- speedValue.textContent = speed.toFixed(2) + 'x';
1328
- updatePlaybackRate(speed);
1329
- });
1330
 
1331
- function updatePlaybackRate(speed) {
1332
- currentPlaybackRate = speed;
1333
- video.playbackRate = speed;
 
 
 
 
 
 
 
 
 
1334
 
1335
  if (combinedAudioSource) {
1336
- combinedAudioSource.playbackRate.value = speed;
 
 
 
 
 
 
 
 
1337
  }
 
 
1338
  }
 
 
 
 
 
1339
 
1340
- // 全画面ボタン
1341
- fullscreenBtn.addEventListener('click', function() {
1342
- if (!isFullscreen) {
1343
- if (videoContainer.requestFullscreen) {
1344
- videoContainer.requestFullscreen();
1345
- } else if (videoContainer.webkitRequestFullscreen) {
1346
- videoContainer.webkitRequestFullscreen();
1347
- } else if (videoContainer.msRequestFullscreen) {
1348
- videoContainer.msRequestFullscreen();
1349
- }
1350
- } else {
1351
- if (document.exitFullscreen) {
1352
- document.exitFullscreen();
1353
- } else if (document.webkitExitFullscreen) {
1354
- document.webkitExitFullscreen();
1355
- } else if (document.msExitFullscreen) {
1356
- document.msExitFullscreen();
1357
- }
1358
- }
1359
- });
1360
 
1361
- // 全画面変更イベント
1362
- document.addEventListener('fullscreenchange', handleFullscreenChange);
1363
- document.addEventListener('webkitfullscreenchange', handleFullscreenChange);
1364
- document.addEventListener('msfullscreenchange', handleFullscreenChange);
1365
-
1366
- function handleFullscreenChange() {
1367
- isFullscreen = !!(document.fullscreenElement || document.webkitFullscreenElement || document.msFullscreenElement);
1368
- fullscreenBtn.textContent = isFullscreen ? '⛶' : '⛶';
1369
- video.controls = false;
1370
- }
1371
-
1372
- // キーボードイベント (ESCで全画面終了)
1373
- document.addEventListener('keydown', function(e) {
1374
- if (e.key === 'Escape' && isFullscreen) {
1375
- if (document.exitFullscreen) {
1376
- document.exitFullscreen();
1377
- } else if (document.webkitExitFullscreen) {
1378
- document.webkitExitFullscreen();
1379
- } else if (document.msExitFullscreen) {
1380
- document.msExitFullscreen();
1381
- }
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
 
1397
- // 全体音量スライダーのイベント
1398
- globalVolumeSlider.addEventListener('input', function() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- // 現在の秒数を開始時間に設定
1413
- setStartTimeBtn.addEventListener('click', function() {
1414
- startTimeInput.value = video.currentTime.toFixed(2);
1415
- });
1416
-
1417
- // 現在の秒数を終了時間に設定
1418
- setEndTimeBtn.addEventListener('click', function() {
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,
1438
- speedSlider,
1439
- globalVolumeSlider,
1440
- playbackSpeedSlider,
1441
- ...audioSliders
1442
- ];
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>
1463
  </html>
 
735
  </div>
736
 
737
  <script>
738
+ document.addEventListener('DOMContentLoaded', function() {
739
+ // テクノロジー風背景を生成
740
+ function createTechBackground() {
741
+ const bg = document.getElementById('techBg');
742
+
743
+ for (let i = 0; i < 200; i++) {
744
+ const line = document.createElement('div');
745
+ line.className = 'circuit-line';
746
 
747
+ const isHorizontal = Math.random() > 0.5;
748
+ if (isHorizontal) {
749
+ line.style.width = `${Math.random() * 300 + 100}px`;
750
+ line.style.height = '1px';
751
+ } else {
752
+ line.style.width = '1px';
753
+ line.style.height = `${Math.random() * 300 + 100}px`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
754
  }
755
+
756
+ line.style.left = `${Math.random() * 100}%`;
757
+ line.style.top = `${Math.random() * 100}%`;
758
+ line.style.opacity = Math.random() * 0.5 + 0.1;
759
+ bg.appendChild(line);
760
+ }
761
 
762
+ for (let i = 0; i < 200; i++) {
763
+ const dot = document.createElement('div');
764
+ dot.className = 'grid-dot';
765
+ dot.style.left = `${Math.random() * 100}%`;
766
+ dot.style.top = `${Math.random() * 100}%`;
767
+ bg.appendChild(dot);
768
+ }
 
 
769
 
770
+ for (let i = 0; i < 15; i++) {
771
+ const hex = document.createElement('div');
772
+ hex.className = 'hexagon';
773
+ hex.style.left = `${Math.random() * 100}%`;
774
+ hex.style.top = `${Math.random() * 100}%`;
775
+ hex.style.transform = `rotate(${Math.random() * 360}deg)`;
776
+ hex.style.animation = `float ${Math.random() * 10 + 5}s infinite ease-in-out`;
777
+ bg.appendChild(hex);
778
  }
779
+
780
+ for (let i = 0; i < 8; i++) {
781
+ const pulse = document.createElement('div');
782
+ pulse.className = 'pulse';
783
+ pulse.style.left = `${Math.random() * 100}%`;
784
+ pulse.style.top = `${Math.random() * 100}%`;
785
+ pulse.style.animationDelay = `${Math.random() * 2}s`;
786
+ bg.appendChild(pulse);
787
+ }
788
+ }
789
+
790
+ createTechBackground();
791
+
792
+ // ローディング状態を管理
793
+ let loadingCount = 0;
794
+ let totalToLoad = 6; // 動画 + 5つの音声ファイル
795
+ let lastUpdateTime = 0;
796
+ const updateInterval = 1;
797
+
798
+ function checkLoadingComplete() {
799
+ loadingCount++;
800
+ if (loadingCount >= totalToLoad) {
801
+ setTimeout(function() {
802
+ const loadingOverlay = document.getElementById('loadingOverlay');
803
+ loadingOverlay.style.opacity = '0';
804
  setTimeout(function() {
805
+ loadingOverlay.style.display = 'none';
806
+ }, 1000);
807
+ }, 500);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
808
  }
809
+ }
810
+
811
+ function handleError(error, message) {
812
+ console.error(message, error);
813
+ window.alert(`${message}\n\nエラー詳細: ${error.message}`);
814
+ }
815
+
816
+ // Web Audio Context の初期化
817
+ let audioContext;
818
+ try {
819
+ audioContext = new (window.AudioContext || window.webkitAudioContext)();
820
+ } catch (e) {
821
+ console.error('Web Audio APIがサポートされていません:', e);
822
+ }
823
 
824
+ // 要素を取得
825
+ const video = document.getElementById('video');
826
+ const videoContainer = document.getElementById('video-container');
827
+ const playPauseBtn = document.getElementById('play-pause-btn');
828
+ const timeDisplay = document.getElementById('time-display');
829
+ const progressContainer = document.getElementById('progress-container');
830
+ const progressBar = document.getElementById('progress-bar');
831
+ const progressTime = document.getElementById('progress-time');
832
+ const volumeBtn = document.getElementById('volume-btn');
833
+ const volumeSlider = document.getElementById('volume-slider');
834
+ const speedSlider = document.getElementById('speed-slider');
835
+ const speedValue = document.getElementById('speed-value');
836
+ const playbackSpeedSlider = document.getElementById('playback-speed');
837
+ const playbackSpeedValue = document.getElementById('playback-speed-value');
838
+ const fullscreenBtn = document.getElementById('fullscreen-btn');
839
+ const startTimeInput = document.getElementById('start-time');
840
+ const endTimeInput = document.getElementById('end-time');
841
+ const loopCheckbox = document.getElementById('loop');
842
+ const globalVolumeSlider = document.getElementById('global-volume');
843
+ const globalVolumeValue = document.getElementById('global-volume-value');
844
+ const audioSliders = document.querySelectorAll('.audio-slider');
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
+ let currentVolumes = { p: 0, a: 1, t: 1, s: 1, k: 0 };
863
+
864
+ // 初期化
865
+ let videoDuration = 0;
866
+ let isPlaying = false;
867
+ let lastVolume = 1;
868
+ let currentPlaybackRate = 1;
869
+ let isFullscreen = false;
870
+ let lastSyncTime = 0;
871
+ let syncDriftLog = [];
872
+
873
+ // 音声ファイルをロード (改良版)
874
+ function loadAudioFiles() {
875
+ audioFiles.forEach(file => {
876
+ try {
877
+ const audio = new Audio(`${file}.mp3`);
878
+ audio.preload = 'auto';
879
+ audio.loop = false;
880
+ audioElements[file] = audio;
881
+
882
+ audio.addEventListener('loadedmetadata', function() {
883
+ console.log(`${file}.mp3 loaded`);
884
+ checkLoadingComplete();
885
+ });
886
+
887
+ audio.addEventListener('error', function() {
888
+ console.error(`音声ファイル読み込みエラー (${file}.mp3):`, audio.error);
889
+ checkLoadingComplete();
890
+ });
891
+ } catch (error) {
892
+ console.error(`音声ファイル初期化エラー (${file}.mp3):`, error);
893
+ checkLoadingComplete();
894
  }
895
+ });
896
+ }
897
 
898
+ // 音声を結合する関数
899
+ async function combineAudio() {
900
+ if (!audioContext) {
901
+ combineStatus.textContent = "Web Audio APIが利用できません";
902
+ return;
903
+ }
904
 
905
+ combineButton.disabled = true;
906
+ combineStatus.textContent = "音声を合成中...";
 
 
 
 
 
 
 
 
907
 
908
+ try {
909
+ // 現在の音量設定を保存
910
+ audioFiles.forEach(file => {
911
+ currentVolumes[file] = parseFloat(document.querySelector(`.audio-slider[data-audio="${file}"]`).value);
912
+ });
913
 
914
+ // 各音声ファイルをデコード
915
+ const audioBufferPromises = audioFiles.map(async file => {
916
+ const audio = audioElements[file];
917
+ if (!audio) return null;
918
 
919
+ const response = await fetch(`${file}.mp3`);
920
+ const arrayBuffer = await response.arrayBuffer();
921
+ return await audioContext.decodeAudioData(arrayBuffer);
922
+ });
 
 
923
 
924
+ // すべての音声バッファを取得
925
+ const buffers = await Promise.all(audioBufferPromises);
926
+ audioFiles.forEach((file, index) => {
927
+ audioBuffers[file] = buffers[index];
928
+ });
929
 
930
+ // 最長の音声バッファの長さを取得
931
+ const maxDuration = Math.max(...buffers.filter(b => b).map(b => b.duration));
932
 
933
+ // 新しい音声バッファを作成
934
+ combinedAudioBuffer = audioContext.createBuffer(
935
+ 2, // ステレオ
936
+ audioContext.sampleRate * maxDuration,
937
+ audioContext.sampleRate
938
+ );
939
 
940
+ // 各音声バッファを結合
941
+ for (let file of audioFiles) {
942
+ if (!audioBuffers[file]) continue;
 
943
 
944
+ const buffer = audioBuffers[file];
945
+ const volume = currentVolumes[file];
946
+
947
+ // 音量が0の場合はスキップ
948
+ if (volume === 0) continue;
949
 
950
+ // 各チャンネルに音声を加算
951
  for (let channel = 0; channel < 2; channel++) {
952
+ const inputData = buffer.getChannelData(channel % buffer.numberOfChannels);
953
  const outputData = combinedAudioBuffer.getChannelData(channel);
 
 
 
 
 
 
 
954
 
955
+ for (let i = 0; i < inputData.length; i++) {
956
+ outputData[i] += inputData[i] * volume;
 
 
957
  }
958
  }
 
 
 
 
 
 
 
 
 
 
959
  }
 
960
 
961
+ // 音量を正規化 (クリッピング防止)
962
+ for (let channel = 0; channel < 2; channel++) {
963
+ const outputData = combinedAudioBuffer.getChannelData(channel);
964
+ let max = 0;
965
+
966
+ for (let i = 0; i < outputData.length; i++) {
967
+ if (Math.abs(outputData[i]) > max) {
968
+ max = Math.abs(outputData[i]);
969
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
970
  }
971
 
972
+ if (max > 1) {
973
+ for (let i = 0; i < outputData.length; i++) {
974
+ outputData[i] /= max;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
975
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
976
  }
 
977
  }
 
978
 
979
+ isAudioCombined = true;
980
+ combineStatus.textContent = "音声の合成が完了しました";
981
+ enablePlayerControls();
982
+ previewSection.style.display = 'block';
983
+
984
+ // 合成後に音量と再生速度を適用
985
+ applyVolume();
986
+ applyPlaybackRate();
987
+
988
+ } catch (error) {
989
+ console.error('音声合成エラー:', error);
990
+ combineStatus.textContent = "音声の合成に失敗しました";
991
+ combineButton.disabled = false;
992
  }
993
+ }
994
 
995
+ // 音量を適用
996
+ function applyVolume() {
997
+ if (!isAudioCombined) return;
 
 
 
 
 
 
 
 
 
 
998
 
999
+ const globalVolume = parseFloat(globalVolumeSlider.value);
1000
+ video.volume = globalVolume;
1001
+ volumeSlider.value = globalVolume;
1002
+ updateVolumeIcon();
1003
+ }
1004
+
1005
+ // 再生速度を適用
1006
+ function applyPlaybackRate() {
1007
+ if (!isAudioCombined) return;
1008
 
1009
+ const speed = parseFloat(playbackSpeedSlider.value);
1010
+ currentPlaybackRate = speed;
1011
+ video.playbackRate = speed;
 
 
 
 
 
 
1012
 
1013
+ if (combinedAudioSource) {
1014
+ combinedAudioSource.playbackRate.value = speed;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1015
  }
1016
 
1017
+ speedValue.textContent = speed.toFixed(2) + 'x';
1018
+ playbackSpeedValue.textContent = speed.toFixed(2) + 'x';
1019
+ speedSlider.value = speed;
1020
+ }
1021
+
1022
+ // プレイヤーコントロールを有効化
1023
+ function enablePlayerControls() {
1024
+ disabledOverlay.style.display = 'none';
1025
+ playPauseBtn.disabled = false;
1026
+ volumeBtn.disabled = false;
1027
+ volumeSlider.disabled = false;
1028
+ speedSlider.disabled = false;
1029
+ fullscreenBtn.disabled = false;
1030
+ startTimeInput.disabled = false;
1031
+ endTimeInput.disabled = false;
1032
+ loopCheckbox.disabled = false;
1033
+ globalVolumeSlider.disabled = false;
1034
+ setStartTimeBtn.disabled = false;
1035
+ setEndTimeBtn.disabled = false;
1036
+ playbackSpeedSlider.disabled = false;
1037
+
1038
+ // 合成後に音量と再生速度スライダーを有効化
1039
+ volumeSlider.disabled = false;
1040
+ speedSlider.disabled = false;
1041
+ playbackSpeedSlider.disabled = false;
1042
+ }
1043
+
1044
+ // プレビュー再生
1045
+ function togglePreview() {
1046
+ if (!isAudioCombined || !combinedAudioBuffer) return;
1047
+
1048
+ if (previewButton.textContent === '▶') {
1049
+ // 再生
1050
+ if (combinedAudioSource) {
1051
+ combinedAudioSource.stop();
1052
  }
1053
+
1054
+ combinedAudioSource = audioContext.createBufferSource();
1055
+ combinedAudioSource.buffer = combinedAudioBuffer;
1056
+ combinedAudioSource.connect(audioContext.destination);
1057
+ combinedAudioSource.start(0);
1058
+
1059
+ previewButton.textContent = '⏸';
1060
+
1061
+ // プレビューの時間表示を更新
1062
+ const updatePreviewTime = () => {
1063
+ if (!combinedAudioSource || !isAudioCombined) return;
 
1064
 
1065
+ const currentTime = audioContext.currentTime - combinedAudioSource.startTime;
1066
+ const duration = combinedAudioBuffer.duration;
1067
 
1068
+ if (currentTime >= duration) {
1069
+ previewButton.textContent = '▶';
1070
+ previewTime.textContent = `00:00 / ${formatTime(duration)}`;
1071
+ return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1072
  }
 
 
 
 
 
 
 
 
 
 
 
1073
 
1074
+ previewTime.textContent = `${formatTime(currentTime)} / ${formatTime(duration)}`;
1075
+ requestAnimationFrame(updatePreviewTime);
1076
+ };
1077
+
1078
+ updatePreviewTime();
1079
+
1080
+ combinedAudioSource.onended = () => {
1081
+ previewButton.textContent = '▶';
1082
+ previewTime.textContent = `00:00 / ${formatTime(combinedAudioBuffer.duration)}`;
1083
+ };
1084
+ } else {
1085
+ // 一時停止
1086
+ if (combinedAudioSource) {
1087
+ combinedAudioSource.stop();
1088
+ combinedAudioSource = null;
1089
  }
1090
+ previewButton.textContent = '▶';
1091
+ }
1092
+ }
1093
+
1094
+ // 時間をフォーマットするヘルパー関数
1095
+ function formatTime(seconds) {
1096
+ const mins = Math.floor(seconds / 60);
1097
+ const secs = Math.floor(seconds % 60);
1098
+ return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
1099
+ }
1100
+
1101
+ // 動画のメタデータが読み込まれたら
1102
+ video.addEventListener('loadedmetadata', function() {
1103
+ try {
1104
+ videoDuration = video.duration;
1105
+ endTimeInput.value = videoDuration.toFixed(2);
1106
+ endTimeInput.max = videoDuration;
1107
+ startTimeInput.max = videoDuration - 0.1;
1108
+ updateTimeDisplay();
1109
+ checkLoadingComplete();
1110
+ } catch (error) {
1111
+ handleError(error, '動画メタデータ読み込み中にエラーが発生しました');
1112
  }
1113
+ });
1114
+
1115
+ // 動画エラー処理
1116
+ video.addEventListener('error', function() {
1117
+ handleError(video.error, '動画読み込み中にエラーが発生しました');
1118
+ });
1119
+
1120
+ // 再生ボタンクリック
1121
+ playPauseBtn.addEventListener('click', function() {
1122
+ const endTime = parseFloat(endTimeInput.value) || videoDuration;
1123
+ if (video.currentTime >= endTime) {
1124
+ const startTime = parseFloat(startTimeInput.value) || 0;
1125
+ seekMedia(startTime);
1126
+ }
1127
+ togglePlayPause();
1128
+ });
1129
+
1130
+ // 時間表示を更新
1131
+ function updateTimeDisplay() {
1132
+ const now = performance.now();
1133
+ if (now - lastUpdateTime < updateInterval && !isFullscreen) return;
1134
+ lastUpdateTime = now;
1135
 
1136
+ try {
1137
+ const currentTime = video.currentTime;
1138
  const duration = video.duration || videoDuration;
1139
+
1140
+ const currentMinutes = Math.floor(currentTime / 60);
1141
+ const currentSeconds = Math.floor(currentTime % 60);
1142
+ const currentMilliseconds = Math.floor((currentTime % 1) * 100);
1143
+ const durationMinutes = Math.floor(duration / 60);
1144
+ const durationSeconds = Math.floor(duration % 60);
1145
+ const durationMilliseconds = Math.floor((duration % 1) * 100);
1146
+
1147
+ timeDisplay.textContent =
1148
+ `${String(currentMinutes).padStart(2, '0')}:${String(currentSeconds).padStart(2, '0')}.${String(currentMilliseconds).padStart(2, '0')} / ` +
1149
+ `${String(durationMinutes).padStart(2, '0')}:${String(durationSeconds).padStart(2, '0')}.${String(durationMilliseconds).padStart(2, '0')}`;
1150
+
1151
+ const progressPercent = (currentTime / duration) * 100;
1152
+ progressBar.style.width = `${progressPercent}%`;
1153
+ } catch (error) {
1154
+ console.error('時間表示更新エラー:', error);
1155
+ }
1156
+ }
1157
+
1158
+ // 再生/一時停止をトグル
1159
+ function togglePlayPause() {
1160
+ if (isPlaying) {
1161
+ pauseMedia();
1162
+ } else {
1163
+ playMedia();
1164
+ }
1165
+ }
1166
+
1167
+ // 再生関数 (改良版)
1168
+ function playMedia() {
1169
+ try {
1170
+ const duration = video.duration || videoDuration;
1171
+ const startTime = parseFloat(startTimeInput.value) || 0;
1172
  const endTime = parseFloat(endTimeInput.value) || duration;
1173
 
1174
+ if (video.currentTime >= endTime) {
1175
+ video.currentTime = startTime;
1176
+ }
1177
+
1178
+ // 動画を再生
1179
+ const playPromise = video.play();
1180
+
1181
+ if (playPromise !== undefined) {
1182
+ playPromise.then(() => {
1183
+ isPlaying = true;
1184
+ playPauseBtn.textContent = '⏸';
1185
 
1186
+ // Web Audio APIで合成音声を再生
1187
  if (combinedAudioSource) {
1188
  combinedAudioSource.stop();
 
 
 
 
 
1189
  }
1190
+
1191
+ combinedAudioSource = audioContext.createBufferSource();
1192
+ combinedAudioSource.buffer = combinedAudioBuffer;
1193
+ combinedAudioSource.connect(audioContext.destination);
1194
+ combinedAudioSource.start(0, video.currentTime);
1195
+
1196
+ // 再生速度を設定
1197
+ video.playbackRate = currentPlaybackRate;
1198
+ combinedAudioSource.playbackRate.value = currentPlaybackRate;
1199
+
1200
+ // 動画と音声の同期を維持
1201
+ combinedAudioSource.onended = () => {
1202
+ if (loopCheckbox.checked) {
1203
+ video.currentTime = startTime;
1204
+ playMedia();
1205
+ } else {
1206
+ pauseMedia();
1207
+ }
1208
+ };
1209
+ }).catch(error => {
1210
+ console.error('動画再生エラー:', error);
1211
+ });
1212
  }
1213
+ } catch (error) {
1214
+ console.error('メディア再生エラー:', error);
1215
+ }
1216
+ }
1217
+
1218
+ // 一時停止関数
1219
+ function pauseMedia() {
1220
+ try {
1221
+ video.pause();
1222
+ isPlaying = false;
1223
+ playPauseBtn.textContent = '▶';
1224
 
1225
+ if (combinedAudioSource) {
1226
+ combinedAudioSource.stop();
1227
+ combinedAudioSource = null;
1228
+ }
1229
+ } catch (error) {
1230
+ console.error('メディア一時停止エラー:', error);
1231
+ }
1232
+ }
1233
+
1234
+ // 時間更新時の処理
1235
+ video.addEventListener('timeupdate', function() {
1236
+ const duration = video.duration || videoDuration;
1237
+ const endTime = parseFloat(endTimeInput.value) || duration;
1238
 
1239
+ if (video.currentTime >= endTime && endTime > 0) {
1240
+ if (loopCheckbox.checked) {
 
 
1241
  const startTime = parseFloat(startTimeInput.value) || 0;
1242
+ video.currentTime = startTime;
 
 
 
1243
 
1244
  if (combinedAudioSource) {
1245
  combinedAudioSource.stop();
1246
  combinedAudioSource = audioContext.createBufferSource();
1247
+ combinedAudioSource.buffer = combinedAudioBuffer;
1248
  combinedAudioSource.connect(audioContext.destination);
1249
+ combinedAudioSource.start(0, startTime);
1250
+ combinedAudioSource.playbackRate.value = currentPlaybackRate;
 
 
 
1251
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1252
  } else {
1253
+ pauseMedia();
1254
+ video.currentTime = endTime;
1255
  }
1256
  }
1257
 
1258
+ updateTimeDisplay();
1259
+ });
1260
+
1261
+ // プログレスバークリックでシーク
1262
+ progressContainer.addEventListener('click', function(e) {
1263
+ if (!video.duration) return;
 
 
1264
 
1265
+ const rect = this.getBoundingClientRect();
1266
+ const pos = (e.clientX - rect.left) / rect.width;
1267
+ const seekTime = pos * video.duration;
 
 
 
 
 
1268
 
1269
+ seekMedia(seekTime);
1270
+ });
1271
+
1272
+ // 指定した時間にシーク (改良版)
1273
+ function seekMedia(time) {
1274
+ try {
1275
+ const duration = video.duration || videoDuration;
1276
+ const startTime = parseFloat(startTimeInput.value) || 0;
1277
+ const endTime = parseFloat(endTimeInput.value) || duration;
1278
+
1279
+ const seekTime = Math.max(startTime, Math.min(time, endTime));
1280
+ video.currentTime = seekTime;
1281
 
1282
  if (combinedAudioSource) {
1283
+ combinedAudioSource.stop();
1284
+ combinedAudioSource = audioContext.createBufferSource();
1285
+ combinedAudioBuffer = combinedAudioBuffer;
1286
+ combinedAudioSource.connect(audioContext.destination);
1287
+
1288
+ if (isPlaying) {
1289
+ combinedAudioSource.start(0, seekTime);
1290
+ combinedAudioSource.playbackRate.value = currentPlaybackRate;
1291
+ }
1292
  }
1293
+ } catch (error) {
1294
+ console.error('メディアシークエラー:', error);
1295
  }
1296
+ }
1297
+
1298
+ // プログレスバー上でマウス移動時に時間を表示
1299
+ progressContainer.addEventListener('mousemove', function(e) {
1300
+ if (!video.duration) return;
1301
 
1302
+ const rect = this.getBoundingClientRect();
1303
+ const pos = (e.clientX - rect.left) / rect.width;
1304
+ const hoverTime = pos * video.duration;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1305
 
1306
+ const minutes = Math.floor(hoverTime / 60);
1307
+ const seconds = Math.floor(hoverTime % 60);
1308
+ const milliseconds = Math.floor((hoverTime % 1) * 100);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1309
 
1310
+ progressTime.textContent = `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}.${String(milliseconds).padStart(2, '0')}`;
1311
+ progressTime.style.display = 'block';
1312
+ progressTime.style.left = `${pos * 100}%`;
1313
+ });
1314
+
1315
+ progressContainer.addEventListener('mouseleave', function() {
1316
+ progressTime.style.display = 'none';
1317
+ });
1318
+
1319
+ // 動画クリックで再生/一時停止
1320
+ video.addEventListener('click', function() {
1321
+ togglePlayPause();
1322
+ });
1323
+
1324
+ // 音量コントロール
1325
+ volumeSlider.addEventListener('input', function() {
1326
+ if (!isAudioCombined) return;
1327
+ video.volume = this.value;
1328
+ lastVolume = this.value;
1329
+ updateVolumeIcon();
1330
+ });
1331
+
1332
+ // 音量ボタン
1333
+ volumeBtn.addEventListener('click', function() {
1334
+ if (!isAudioCombined) return;
1335
+
1336
+ if (video.volume > 0) {
1337
+ lastVolume = video.volume;
1338
+ video.volume = 0;
1339
+ volumeSlider.value = 0;
1340
+ } else {
1341
+ video.volume = lastVolume;
1342
+ volumeSlider.value = lastVolume;
1343
+ }
1344
+ updateVolumeIcon();
1345
+ });
1346
+
1347
+ // 音量アイコンを更新
1348
+ function updateVolumeIcon() {
1349
+ if (video.volume === 0) {
1350
+ volumeBtn.textContent = '🔇';
1351
+ } else if (video.volume < 0.5) {
1352
+ volumeBtn.textContent = '🔈';
1353
+ } else {
1354
+ volumeBtn.textContent = '🔊';
1355
+ }
1356
+ }
1357
+
1358
+ // 再生速度スライダー (動画プレイヤー)
1359
+ speedSlider.addEventListener('input', function() {
1360
+ if (!isAudioCombined) return;
1361
+
1362
+ const speed = parseFloat(this.value);
1363
+ speedValue.textContent = speed.toFixed(2) + 'x';
1364
+ playbackSpeedSlider.value = speed;
1365
+ playbackSpeedValue.textContent = speed.toFixed(2) + 'x';
1366
+ updatePlaybackRate(speed);
1367
+ });
1368
+
1369
+ // 再生速度スライダー (設定メニュー)
1370
+ playbackSpeedSlider.addEventListener('input', function() {
1371
+ if (!isAudioCombined) return;
1372
+
1373
+ const speed = parseFloat(this.value);
1374
+ playbackSpeedValue.textContent = speed.toFixed(2) + 'x';
1375
+ speedSlider.value = speed;
1376
+ speedValue.textContent = speed.toFixed(2) + 'x';
1377
+ updatePlaybackRate(speed);
1378
+ });
1379
+
1380
+ function updatePlaybackRate(speed) {
1381
+ if (!isAudioCombined) return;
1382
+
1383
+ currentPlaybackRate = speed;
1384
+ video.playbackRate = speed;
1385
 
1386
+ if (combinedAudioSource) {
1387
+ combinedAudioSource.playbackRate.value = speed;
1388
+ }
1389
+ }
1390
+
1391
+ // 全画面ボタン
1392
+ fullscreenBtn.addEventListener('click', function() {
1393
+ if (!isFullscreen) {
1394
+ if (videoContainer.requestFullscreen) {
1395
+ videoContainer.requestFullscreen();
1396
+ } else if (videoContainer.webkitRequestFullscreen) {
1397
+ videoContainer.webkitRequestFullscreen();
1398
+ } else if (videoContainer.msRequestFullscreen) {
1399
+ videoContainer.msRequestFullscreen();
1400
+ }
1401
+ } else {
1402
+ if (document.exitFullscreen) {
1403
+ document.exitFullscreen();
1404
+ } else if (document.webkitExitFullscreen) {
1405
+ document.webkitExitFullscreen();
1406
+ } else if (document.msExitFullscreen) {
1407
+ document.msExitFullscreen();
1408
+ }
1409
+ }
1410
+ });
1411
+
1412
+ // 全画面変更イベント
1413
+ document.addEventListener('fullscreenchange', handleFullscreenChange);
1414
+ document.addEventListener('webkitfullscreenchange', handleFullscreenChange);
1415
+ document.addEventListener('msfullscreenchange', handleFullscreenChange);
1416
+
1417
+ function handleFullscreenChange() {
1418
+ isFullscreen = !!(document.fullscreenElement || document.webkitFullscreenElement || document.msFullscreenElement);
1419
+ fullscreenBtn.textContent = isFullscreen ? '⛶' : '⛶';
1420
+ video.controls = false;
1421
+ }
1422
+
1423
+ // キーボードイベント (ESCで全画面終了)
1424
+ document.addEventListener('keydown', function(e) {
1425
+ if (e.key === 'Escape' && isFullscreen) {
1426
+ if (document.exitFullscreen) {
1427
+ document.exitFullscreen();
1428
+ } else if (document.webkitExitFullscreen) {
1429
+ document.webkitExitFullscreen();
1430
+ } else if (document.msExitFullscreen) {
1431
+ document.msExitFullscreen();
1432
+ }
1433
+ }
1434
+ });
1435
+
1436
+ // ボリュームスライダーのイベント
1437
+ audioSliders.forEach((slider, index) => {
1438
+ slider.addEventListener('input', function() {
1439
  const value = parseFloat(this.value);
1440
+ volumeValues[index].textContent = value.toFixed(2);
1441
 
1442
  // スライダーの背景を更新
1443
+ const percent = value * 100;
1444
  this.style.backgroundSize = `${percent}% 100%`;
1445
  });
1446
+ });
1447
+
1448
+ // 全体音量スライダーのイベント
1449
+ globalVolumeSlider.addEventListener('input', function() {
1450
+ const value = parseFloat(this.value);
1451
+ globalVolumeValue.textContent = value.toFixed(1);
1452
 
1453
+ // スライダーの背景を更新
1454
+ const percent = (value - this.min) / (this.max - this.min) * 100;
1455
+ this.style.backgroundSize = `${percent}% 100%`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1456
 
1457
+ // 合成後に音量を適用
1458
+ if (isAudioCombined) {
1459
+ applyVolume();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1460
  }
 
 
1461
  });
1462
+
1463
+ // ループ設定変更時
1464
+ loopCheckbox.addEventListener('change', function() {
1465
+ // 合成音声ではループは動画に依存する
1466
+ });
1467
+
1468
+ // 現在の秒数を開始時間に設定
1469
+ setStartTimeBtn.addEventListener('click', function() {
1470
+ startTimeInput.value = video.currentTime.toFixed(2);
1471
+ });
1472
+
1473
+ // 現在の秒数を終了時間に設定
1474
+ setEndTimeBtn.addEventListener('click', function() {
1475
+ endTimeInput.value = video.currentTime.toFixed(2);
1476
+ });
1477
+
1478
+ // 合成ボタンクリック
1479
+ combineButton.addEventListener('click', combineAudio);
1480
+
1481
+ // プレビューボタンクリック
1482
+ previewButton.addEventListener('click', togglePreview);
1483
+
1484
+ // 初期化
1485
+ loadAudioFiles();
1486
+ updateVolumeIcon();
1487
+ volumeSlider.value = video.volume;
1488
+ video.controls = false;
1489
+
1490
+ // スライダーの背景を初期化
1491
+ function initSliderBackgrounds() {
1492
+ const sliders = [
1493
+ volumeSlider,
1494
+ speedSlider,
1495
+ globalVolumeSlider,
1496
+ playbackSpeedSlider,
1497
+ ...audioSliders
1498
+ ];
1499
+
1500
+ sliders.forEach(slider => {
1501
+ if (slider) {
1502
+ slider.style.backgroundImage = 'linear-gradient(#64ffda, #64ffda)';
1503
+ slider.style.backgroundRepeat = 'no-repeat';
1504
+
1505
+ if (slider === globalVolumeSlider) {
1506
+ const percent = (slider.value - slider.min) / (slider.max - slider.min) * 100;
1507
+ slider.style.backgroundSize = `${percent}% 100%`;
1508
+ } else {
1509
+ slider.style.backgroundSize = `${slider.value * 100}% 100%`;
1510
+ }
1511
+ }
1512
+ });
1513
+ }
1514
+
1515
+ initSliderBackgrounds();
1516
+ });
1517
  </script>
1518
  </body>
1519
  </html>