soiz1 commited on
Commit
3a27bf6
·
1 Parent(s): 9388300

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +658 -717
index.html CHANGED
@@ -735,771 +735,702 @@
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,23 +1438,33 @@ document.addEventListener('DOMContentLoaded', function() {
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>
 
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
+ audio.load().catch(e => {
1293
+ console.error(`音声ファイル読み込みエラー (${file}.mp3):`, e);
1294
+ });
1295
+ } catch (error) {
1296
+ handleError(error, `音声ファイル初期化中にエラーが発生しました (${file}.mp3)`);
1297
+ checkLoadingComplete(); // エラー時もカウント
1298
+ }
1299
+ });
1300
  }
1301
+
1302
+ // ボリュームスライダーのイベント (修正版)
1303
+ audioSliders.forEach((slider, index) => {
1304
+ slider.addEventListener('input', function() {
1305
+ try {
1306
+ const value = parseFloat(this.value);
1307
+ volumeValues[index].textContent = value.toFixed(2);
1308
+
1309
+ if (audioElements[this.dataset.audio]) {
1310
+ const globalVolume = parseFloat(globalVolumeSlider.value) / 10;
1311
+ const calculatedVolume = value * globalVolume;
1312
+ audioElements[this.dataset.audio].volume = calculatedVolume;
1313
+
1314
+ // スライダーの背景を更新
1315
+ updateSliderBackgrounds();
1316
+ }
1317
+ } catch (error) {
1318
+ handleError(error, '音声ボリューム設定中にエラーが発生しました');
1319
+ }
1320
+ });
1321
 
1322
+ // 初期値を1.00に設定
1323
+ slider.value = 1;
1324
+ volumeValues[index].textContent = '1.00';
1325
+ });
1326
+
1327
+ // 全体音量スライダーのイベント (修正版)
1328
+ globalVolumeSlider.addEventListener('input', function() {
1329
+ try {
1330
+ const value = parseFloat(this.value);
1331
+ globalVolumeValue.textContent = value.toFixed(1);
1332
+
1333
+ audioFiles.forEach(file => {
1334
+ if (audioElements[file]) {
1335
+ const volumeSlider = document.querySelector(`.audio-slider[data-audio="${file}"]`);
1336
+ if (volumeSlider) {
1337
+ const sliderValue = parseFloat(volumeSlider.value);
1338
+ const calculatedVolume = sliderValue * (value / 10);
1339
+ audioElements[file].volume = calculatedVolume;
1340
+ }
1341
+ }
1342
+ });
1343
+
1344
+ // スライダーの背景を更新
1345
+ updateSliderBackgrounds();
1346
+ } catch (error) {
1347
+ handleError(error, '全体音量設定中にエラーが発生しました');
1348
+ }
1349
+ });
1350
+
1351
+ // 初期全体音量を5.0に設定
1352
+ globalVolumeSlider.value = 5;
1353
+ globalVolumeValue.textContent = '5.0';
1354
+
1355
+ // ループ設定変更時
1356
+ loopCheckbox.addEventListener('change', function() {
1357
+ try {
1358
+ audioFiles.forEach(file => {
1359
+ if (audioElements[file]) {
1360
+ audioElements[file].loop = this.checked;
1361
+ }
1362
+ });
1363
+ } catch (error) {
1364
+ handleError(error, 'ループ設定変更中にエラーが発生しました');
1365
+ }
1366
+ });
1367
+
1368
+ // 現在の秒数を開始時間に設定
1369
+ setStartTimeBtn.addEventListener('click', function() {
1370
+ try {
1371
+ startTimeInput.value = video.currentTime.toFixed(2);
1372
+ } catch (error) {
1373
+ handleError(error, '開始時間設定中にエラーが発生しました');
1374
+ }
1375
+ });
1376
+
1377
+ // 現在の秒数を終了時間に設定
1378
+ setEndTimeBtn.addEventListener('click', function() {
1379
+ try {
1380
+ endTimeInput.value = video.currentTime.toFixed(2);
1381
+ } catch (error) {
1382
+ handleError(error, '終了時間設定中にエラーが発生しました');
1383
+ }
1384
  });
 
 
 
 
 
 
1385
 
1386
  // スライダーの背景を更新
1387
+ function updateSliderBackgrounds() {
1388
+ // ボリュームスライダー
1389
+ const volumePercent = volumeSlider.value * 100;
1390
+ volumeSlider.style.backgroundSize = `${volumePercent}% 100%`;
1391
+
1392
+ // 再生速度スライダー
1393
+ const speedPercent = (speedSlider.value - speedSlider.min) / (speedSlider.max - speedSlider.min) * 100;
1394
+ speedSlider.style.backgroundSize = `${speedPercent}% 100%`;
1395
+
1396
+ // 全体音量スライダー
1397
+ const globalVolumePercent = (globalVolumeSlider.value - globalVolumeSlider.min) / (globalVolumeSlider.max - globalVolumeSlider.min) * 100;
1398
+ globalVolumeSlider.style.backgroundSize = `${globalVolumePercent}% 100%`;
1399
+
1400
+ // 再生速度スライダー (設定)
1401
+ const playbackSpeedPercent = (playbackSpeedSlider.value - playbackSpeedSlider.min) / (playbackSpeedSlider.max - playbackSpeedSlider.min) * 100;
1402
+ playbackSpeedSlider.style.backgroundSize = `${playbackSpeedPercent}% 100%`;
1403
+
1404
+ // 各音声スライダー
1405
+ audioSliders.forEach(slider => {
1406
+ const percent = slider.value * 100;
1407
+ slider.style.backgroundSize = `${percent}% 100%`;
1408
+ });
1409
+ }
1410
 
1411
+ // スライダーの初期背景を設定
1412
+ function initSliderBackgrounds() {
1413
+ // すべてのスライダーに背景を設定
1414
+ const sliders = [
1415
+ volumeSlider,
1416
+ speedSlider,
1417
+ globalVolumeSlider,
1418
+ playbackSpeedSlider,
1419
+ ...audioSliders
1420
+ ];
1421
+
1422
+ sliders.forEach(slider => {
1423
+ if (slider) {
1424
+ slider.style.backgroundImage = 'linear-gradient(#64ffda, #64ffda)';
1425
+ slider.style.backgroundRepeat = 'no-repeat';
1426
+ }
1427
+ });
1428
+
1429
+ updateSliderBackgrounds();
1430
  }
1431
+
1432
+ // スライダー変更時に背景を更新
1433
+ const allSliders = [
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1434
  volumeSlider,
1435
  speedSlider,
1436
  globalVolumeSlider,
 
1438
  ...audioSliders
1439
  ];
1440
 
1441
+ allSliders.forEach(slider => {
1442
  if (slider) {
1443
+ slider.addEventListener('input', updateSliderBackgrounds);
 
 
 
 
 
 
 
 
1444
  }
1445
  });
1446
+
1447
+ // 初期化
1448
+ loadAudioFiles();
1449
+ updateVolumeIcon();
1450
+ volumeSlider.value = video.volume;
1451
+ video.controls = false;
1452
+ initSliderBackgrounds();
1453
+
1454
+ // 初期音量設定を適用
1455
+ setTimeout(() => {
1456
+ audioFiles.forEach(file => {
1457
+ if (audioElements[file]) {
1458
+ const volumeSlider = document.querySelector(`.audio-slider[data-audio="${file}"]`);
1459
+ if (volumeSlider) {
1460
+ const value = parseFloat(volumeSlider.value);
1461
+ const globalVolume = parseFloat(globalVolumeSlider.value) / 10;
1462
+ audioElements[file].volume = value * globalVolume;
1463
+ }
1464
+ }
1465
+ });
1466
+ }, 1000);
1467
+ });
1468
  </script>
1469
  </body>
1470
  </html>