soiz1 commited on
Commit
f8a855f
·
1 Parent(s): 97e91f0

Update index.html

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