soiz1 commited on
Commit
b1bff68
·
1 Parent(s): 09b4ea0

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +305 -81
index.html CHANGED
@@ -138,6 +138,7 @@
138
  display: flex;
139
  flex-direction: column;
140
  gap: 10px;
 
141
  }
142
 
143
  .progress-container {
@@ -169,6 +170,19 @@
169
  white-space: nowrap;
170
  }
171
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  .main-controls {
173
  display: flex;
174
  align-items: center;
@@ -598,15 +612,53 @@
598
  .disabled-message p {
599
  margin-bottom: 15px;
600
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
601
  #buffering-indicator {
602
  position: absolute;
603
  top: 50%;
604
  left: 50%;
605
  transform: translate(-50%, -50%);
606
- background-color: rgba(0, 0, 0, 0.7);
607
- color: white;
608
- padding: 10px 20px;
609
- border-radius: 5px;
610
  z-index: 10;
611
  display: none;
612
  }
@@ -621,6 +673,39 @@
621
  border-radius: 3px;
622
  font-size: 12px;
623
  z-index: 5;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
624
  }
625
  </style>
626
  </head>
@@ -649,7 +734,6 @@ if ('serviceWorker' in navigator) {
649
  });
650
  });
651
  }
652
-
653
  </script>
654
  <!-- テクノロジー風背景 -->
655
  <div class="tech-background" id="techBg"></div>
@@ -667,7 +751,7 @@ if ('serviceWorker' in navigator) {
667
  </div>
668
 
669
  <h1>高度な音声動画プレイヤー</h1>
670
- <div class="settings">
671
  <h2>サービスワーカー設定</h2>
672
  <div class="setting-item">
673
  <label><input type="checkbox" id="sw-video" checked> 動画ファイル (/v.mp4)</label>
@@ -717,9 +801,12 @@ if ('serviceWorker' in navigator) {
717
  <div class="container">
718
  <div class="video-container" id="video-container">
719
  <!-- バッファリングインジケーター -->
720
- <div id="buffering-indicator">読み込み中...</div>
721
  <!-- 同期ステータス -->
722
- <div class="sync-status" id="sync-status"></div>
 
 
 
723
 
724
  <!-- 無効状態のオーバー��イ -->
725
  <div class="disabled-overlay" id="disabledOverlay">
@@ -736,6 +823,8 @@ if ('serviceWorker' in navigator) {
736
  <div class="progress-container" id="progress-container">
737
  <div class="progress-bar" id="progress-bar"></div>
738
  <div class="progress-time" id="progress-time">00:00</div>
 
 
739
  </div>
740
  <div class="main-controls">
741
  <button class="control-button" id="play-pause-btn" disabled>▶</button>
@@ -769,12 +858,18 @@ if ('serviceWorker' in navigator) {
769
  <div>
770
  <input type="number" id="end-time" min="0" value="0" step="0.01" disabled>
771
  <button class="time-set-button" id="set-end-time" disabled>現在の秒数に設定</button>
 
772
  </div>
773
  </div>
774
  <div class="setting-item">
775
  <label for="loop">ループ再生:</label>
776
  <input type="checkbox" id="loop" disabled>
777
  </div>
 
 
 
 
 
778
  <div class="setting-item">
779
  <div class="global-volume-container">
780
  <label>全体音量係数:</label>
@@ -835,6 +930,9 @@ if ('serviceWorker' in navigator) {
835
  </div>
836
  </div>
837
 
 
 
 
838
  <script>
839
  document.addEventListener('DOMContentLoaded', function() {
840
  // 同期管理用の変数
@@ -843,6 +941,9 @@ document.addEventListener('DOMContentLoaded', function() {
843
  let syncDriftLog = [];
844
  let syncCheckInterval;
845
  let audioContext;
 
 
 
846
 
847
  try {
848
  audioContext = new (window.AudioContext || window.webkitAudioContext)();
@@ -933,9 +1034,9 @@ document.addEventListener('DOMContentLoaded', function() {
933
 
934
  // 要素を取得
935
  const video = document.getElementById('video');
936
- video.preservesPitch = true;
937
- video.mozPreservesPitch = true; // Firefox用
938
- video.webkitPreservesPitch = true; // 古いWebKit用
939
  const videoContainer = document.getElementById('video-container');
940
  const playPauseBtn = document.getElementById('play-pause-btn');
941
  const timeDisplay = document.getElementById('time-display');
@@ -958,6 +1059,7 @@ video.webkitPreservesPitch = true; // 古いWebKit用
958
  const volumeValues = document.querySelectorAll('.volume-value');
959
  const setStartTimeBtn = document.getElementById('set-start-time');
960
  const setEndTimeBtn = document.getElementById('set-end-time');
 
961
  const disabledOverlay = document.getElementById('disabledOverlay');
962
  const combineButton = document.getElementById('combine-button');
963
  const combineStatus = document.getElementById('combine-status');
@@ -966,7 +1068,14 @@ video.webkitPreservesPitch = true; // 古いWebKit用
966
  const previewTime = document.getElementById('preview-time');
967
  const bufferingIndicator = document.getElementById('buffering-indicator');
968
  const syncStatus = document.getElementById('sync-status');
969
-
 
 
 
 
 
 
 
970
 
971
  // 音声オブジェクトを作成
972
  const audioElements = {};
@@ -1036,7 +1145,7 @@ video.webkitPreservesPitch = true; // 古いWebKit用
1036
  const avgDrift = syncDriftLog.reduce((a, b) => a + b, 0) / syncDriftLog.length;
1037
 
1038
  // ズレ表示を更新
1039
- syncStatus.textContent = `同期ズレ: ${avgDrift.toFixed(3)}秒`;
1040
 
1041
  // ズレが大きい場合(0.1秒以上)に修正
1042
  if (Math.abs(avgDrift) > 0.1) {
@@ -1104,6 +1213,9 @@ video.webkitPreservesPitch = true; // 古いWebKit用
1104
  const audio = audioElements[file];
1105
  if (!audio) return null;
1106
 
 
 
 
1107
  const response = await fetch(`${basePath}${file}.mp3`);
1108
  const arrayBuffer = await response.arrayBuffer();
1109
  return await audioContext.decodeAudioData(arrayBuffer);
@@ -1127,14 +1239,11 @@ video.webkitPreservesPitch = true; // 古いWebKit用
1127
 
1128
  // 各音声バッファを結合
1129
  for (let file of audioFiles) {
1130
- if (!audioBuffers[file]) continue;
1131
 
1132
  const buffer = audioBuffers[file];
1133
  const volume = currentVolumes[file];
1134
 
1135
- // 音量が0の場合はスキップ
1136
- if (volume === 0) continue;
1137
-
1138
  // 各チャンネルに音声を加算
1139
  for (let channel = 0; channel < 2; channel++) {
1140
  const inputData = buffer.getChannelData(channel % buffer.numberOfChannels);
@@ -1191,58 +1300,58 @@ video.webkitPreservesPitch = true; // 古いWebKit用
1191
  }
1192
  }
1193
 
1194
- function bufferToWave(abuffer) {
1195
- const numOfChan = abuffer.numberOfChannels,
1196
- length = abuffer.length * numOfChan * 2 + 44,
1197
- buffer = new ArrayBuffer(length),
1198
- view = new DataView(buffer),
1199
- channels = [],
1200
- sampleRate = abuffer.sampleRate;
1201
-
1202
- // posをletで宣言(constから変更)
1203
- let pos = 0;
1204
-
1205
- // write WAV header
1206
- setUint32(0x46464952); // "RIFF"
1207
- setUint32(length - 8); // file length - 8
1208
- setUint32(0x45564157); // "WAVE"
1209
-
1210
- setUint32(0x20746d66); // "fmt " chunk
1211
- setUint32(16); // length = 16
1212
- setUint16(1); // PCM (uncompressed)
1213
- setUint16(numOfChan);
1214
- setUint32(sampleRate);
1215
- setUint32(sampleRate * 2 * numOfChan);
1216
- setUint16(numOfChan * 2);
1217
- setUint16(16);
1218
-
1219
- setUint32(0x61746164); // "data" - chunk
1220
- setUint32(length - pos - 4);
1221
-
1222
- // write interleaved data
1223
- for (let i = 0; i < abuffer.length; i++) {
1224
- for (let channel = 0; channel < numOfChan; channel++) {
1225
- let sample = abuffer.getChannelData(channel)[i] * 0x7fff;
1226
- if (sample < -32768) sample = -32768;
1227
- if (sample > 32767) sample = 32767;
1228
- view.setInt16(pos, sample, true);
 
 
 
 
 
 
1229
  pos += 2;
1230
  }
1231
- }
1232
 
1233
- function setUint16(data) {
1234
- view.setUint16(pos, data, true);
1235
- pos += 2;
1236
- }
1237
 
1238
- function setUint32(data) {
1239
- view.setUint32(pos, data, true);
1240
- pos += 4;
1241
  }
1242
 
1243
- return new Blob([buffer], { type: 'audio/wav' });
1244
- }
1245
-
1246
  function applyVolume() {
1247
  if (!isAudioCombined) return;
1248
 
@@ -1290,6 +1399,7 @@ function bufferToWave(abuffer) {
1290
  fullscreenBtn.disabled = false;
1291
  startTimeInput.disabled = false;
1292
  endTimeInput.disabled = false;
 
1293
  loopCheckbox.disabled = false;
1294
  globalVolumeSlider.disabled = false;
1295
  setStartTimeBtn.disabled = false;
@@ -1585,26 +1695,44 @@ function bufferToWave(abuffer) {
1585
  updatePlaybackRate(speed);
1586
  });
1587
 
1588
- function updatePlaybackRate(speed) {
1589
- if (!isAudioCombined) return;
1590
-
1591
- currentPlaybackRate = speed;
1592
- video.playbackRate = speed;
1593
-
1594
- // ピッチ保持を再設定
1595
- video.preservesPitch = true;
1596
- video.mozPreservesPitch = true;
1597
- video.webkitPreservesPitch = true;
 
 
 
 
 
 
 
1598
 
1599
- if (combinedAudioElement) {
1600
- combinedAudioElement.playbackRate = speed;
 
 
 
1601
 
1602
- // 合成音声のピッチ保持を設定
1603
- combinedAudioElement.preservesPitch = true;
1604
- combinedAudioElement.mozPreservesPitch = true;
1605
- combinedAudioElement.webkitPreservesPitch = true;
 
 
 
 
 
 
 
 
 
1606
  }
1607
- }
1608
 
1609
  // 全画面ボタン
1610
  fullscreenBtn.addEventListener('click', function() {
@@ -1636,8 +1764,66 @@ function updatePlaybackRate(speed) {
1636
  isFullscreen = !!(document.fullscreenElement || document.webkitFullscreenElement || document.msFullscreenElement);
1637
  fullscreenBtn.textContent = isFullscreen ? '⛶' : '⛶';
1638
  video.controls = false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1639
  }
1640
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1641
  // キーボードイベント (ESCで全画面終了)
1642
  document.addEventListener('keydown', function(e) {
1643
  if (e.key === 'Escape' && isFullscreen) {
@@ -1680,19 +1866,51 @@ function updatePlaybackRate(speed) {
1680
  // 現在の秒数を開始時間に設定
1681
  setStartTimeBtn.addEventListener('click', function() {
1682
  startTimeInput.value = video.currentTime.toFixed(2);
 
1683
  });
1684
 
1685
  // 現在の秒数を終了時間に設定
1686
  setEndTimeBtn.addEventListener('click', function() {
1687
  endTimeInput.value = video.currentTime.toFixed(2);
 
1688
  });
1689
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1690
  // 合成ボタンクリック
1691
  combineButton.addEventListener('click', combineAudio);
1692
 
1693
  // プレビューボタンクリック
1694
  previewButton.addEventListener('click', togglePreview);
1695
 
 
 
 
 
 
1696
  // 初期化
1697
  loadAudioFiles();
1698
  updateVolumeIcon();
@@ -1727,5 +1945,11 @@ function updatePlaybackRate(speed) {
1727
 
1728
  initSliderBackgrounds();
1729
  startSyncCheck(); // 同期チェックを開始
 
 
 
 
1730
  });
1731
- </script>
 
 
 
138
  display: flex;
139
  flex-direction: column;
140
  gap: 10px;
141
+ transition: opacity 0.3s;
142
  }
143
 
144
  .progress-container {
 
170
  white-space: nowrap;
171
  }
172
 
173
+ /* プログレスバーのマーカー */
174
+ .progress-marker {
175
+ position: absolute;
176
+ bottom: -5px;
177
+ width: 0;
178
+ height: 0;
179
+ border-left: 5px solid transparent;
180
+ border-right: 5px solid transparent;
181
+ border-top: 10px solid #ff5555;
182
+ transform: translateX(-50%);
183
+ z-index: 2;
184
+ }
185
+
186
  .main-controls {
187
  display: flex;
188
  align-items: center;
 
612
  .disabled-message p {
613
  margin-bottom: 15px;
614
  }
615
+
616
+ /* 新しいローダースタイル */
617
+ .loader {
618
+ width: 80px;
619
+ aspect-ratio: 1;
620
+ border: 10px solid #000;
621
+ box-sizing: border-box;
622
+ background:
623
+ radial-gradient(farthest-side,#fff 98%,#0000) 50%/20px 20px,
624
+ radial-gradient(farthest-side,#fff 98%,#0000) 50%/20px 20px,
625
+ radial-gradient(farthest-side,#fff 98%,#0000) 50%/20px 20px,
626
+ radial-gradient(farthest-side,#fff 98%,#0000) 50%/20px 20px,
627
+ radial-gradient(farthest-side,#fff 98%,#0000) 50%/80% 80%,
628
+ #000;
629
+ background-repeat: no-repeat;
630
+ filter: blur(4px) contrast(10);
631
+ animation: squarePulse 1s infinite alternate;
632
+ }
633
+
634
+ @keyframes squarePulse {
635
+ 0% {
636
+ background-position:
637
+ 50% 50%, 50% 50%, 50% 50%, 50% 50%, 50% 50%, 50% 50%;
638
+ }
639
+ 25% {
640
+ background-position:
641
+ 50% 0, 50% 50%, 50% 50%, 50% 50%, 50% 50%, 50% 50%;
642
+ }
643
+ 50% {
644
+ background-position:
645
+ 50% 0, 50% 100%, 50% 50%, 50% 50%, 50% 50%, 50% 50%;
646
+ }
647
+ 75% {
648
+ background-position:
649
+ 50% 0, 50% 100%, 0 50%, 50% 50%, 50% 50%, 50% 50%;
650
+ }
651
+ 100% {
652
+ background-position:
653
+ 50% 0, 50% 100%, 0 50%, 100% 50%, 50% 50%, 50% 50%;
654
+ }
655
+ }
656
+
657
  #buffering-indicator {
658
  position: absolute;
659
  top: 50%;
660
  left: 50%;
661
  transform: translate(-50%, -50%);
 
 
 
 
662
  z-index: 10;
663
  display: none;
664
  }
 
673
  border-radius: 3px;
674
  font-size: 12px;
675
  z-index: 5;
676
+ display: flex;
677
+ align-items: center;
678
+ gap: 5px;
679
+ }
680
+
681
+ .sync-status button {
682
+ background: none;
683
+ border: none;
684
+ color: #fff;
685
+ cursor: pointer;
686
+ font-size: 12px;
687
+ }
688
+
689
+ .lock-controls-btn {
690
+ position: fixed;
691
+ bottom: 20px;
692
+ right: 20px;
693
+ background-color: rgba(0, 0, 0, 0.7);
694
+ border: none;
695
+ color: #fff;
696
+ width: 36px;
697
+ height: 36px;
698
+ border-radius: 50%;
699
+ display: flex;
700
+ align-items: center;
701
+ justify-content: center;
702
+ cursor: pointer;
703
+ z-index: 100;
704
+ display: none;
705
+ }
706
+
707
+ .lock-controls-btn.locked {
708
+ color: #64ffda;
709
  }
710
  </style>
711
  </head>
 
734
  });
735
  });
736
  }
 
737
  </script>
738
  <!-- テクノロジー風背景 -->
739
  <div class="tech-background" id="techBg"></div>
 
751
  </div>
752
 
753
  <h1>高度な音声動画プレイヤー</h1>
754
+ <div class="settings" hidden>
755
  <h2>サービスワーカー設定</h2>
756
  <div class="setting-item">
757
  <label><input type="checkbox" id="sw-video" checked> 動画ファイル (/v.mp4)</label>
 
801
  <div class="container">
802
  <div class="video-container" id="video-container">
803
  <!-- バッファリングインジケーター -->
804
+ <div id="buffering-indicator"><div class="loader"></div></div>
805
  <!-- 同期ステータス -->
806
+ <div class="sync-status" id="sync-status">
807
+ <span id="sync-status-text"></span>
808
+ <button id="sync-status-close">×</button>
809
+ </div>
810
 
811
  <!-- 無効状態のオーバー��イ -->
812
  <div class="disabled-overlay" id="disabledOverlay">
 
823
  <div class="progress-container" id="progress-container">
824
  <div class="progress-bar" id="progress-bar"></div>
825
  <div class="progress-time" id="progress-time">00:00</div>
826
+ <div class="progress-marker" id="start-marker" style="left: 0%; display: none;"></div>
827
+ <div class="progress-marker" id="end-marker" style="left: 100%; display: none;"></div>
828
  </div>
829
  <div class="main-controls">
830
  <button class="control-button" id="play-pause-btn" disabled>▶</button>
 
858
  <div>
859
  <input type="number" id="end-time" min="0" value="0" step="0.01" disabled>
860
  <button class="time-set-button" id="set-end-time" disabled>現在の秒数に設定</button>
861
+ <button class="time-set-button" id="reset-end-time" disabled>動画の長さに戻す</button>
862
  </div>
863
  </div>
864
  <div class="setting-item">
865
  <label for="loop">ループ再生:</label>
866
  <input type="checkbox" id="loop" disabled>
867
  </div>
868
+ <div class="setting-item">
869
+ <label for="tempo">テンポ (BPM):</label>
870
+ <input type="number" id="tempo" min="40" max="200" value="92" step="1">
871
+ <span id="tempo-speed-value">1.00x</span>
872
+ </div>
873
  <div class="setting-item">
874
  <div class="global-volume-container">
875
  <label>全体音量係数:</label>
 
930
  </div>
931
  </div>
932
 
933
+ <!-- 全画面時のロックボタン -->
934
+ <button class="lock-controls-btn" id="lock-controls-btn" title="コントロールバーを固定">🔒</button>
935
+
936
  <script>
937
  document.addEventListener('DOMContentLoaded', function() {
938
  // 同期管理用の変数
 
941
  let syncDriftLog = [];
942
  let syncCheckInterval;
943
  let audioContext;
944
+ let controlsHideTimeout;
945
+ let isControlsLocked = false;
946
+ let controlsVisible = true;
947
 
948
  try {
949
  audioContext = new (window.AudioContext || window.webkitAudioContext)();
 
1034
 
1035
  // 要素を取得
1036
  const video = document.getElementById('video');
1037
+ video.preservesPitch = true;
1038
+ video.mozPreservesPitch = true; // Firefox用
1039
+ video.webkitPreservesPitch = true; // 古いWebKit用
1040
  const videoContainer = document.getElementById('video-container');
1041
  const playPauseBtn = document.getElementById('play-pause-btn');
1042
  const timeDisplay = document.getElementById('time-display');
 
1059
  const volumeValues = document.querySelectorAll('.volume-value');
1060
  const setStartTimeBtn = document.getElementById('set-start-time');
1061
  const setEndTimeBtn = document.getElementById('set-end-time');
1062
+ const resetEndTimeBtn = document.getElementById('reset-end-time');
1063
  const disabledOverlay = document.getElementById('disabledOverlay');
1064
  const combineButton = document.getElementById('combine-button');
1065
  const combineStatus = document.getElementById('combine-status');
 
1068
  const previewTime = document.getElementById('preview-time');
1069
  const bufferingIndicator = document.getElementById('buffering-indicator');
1070
  const syncStatus = document.getElementById('sync-status');
1071
+ const syncStatusText = document.getElementById('sync-status-text');
1072
+ const syncStatusClose = document.getElementById('sync-status-close');
1073
+ const lockControlsBtn = document.getElementById('lock-controls-btn');
1074
+ const startMarker = document.getElementById('start-marker');
1075
+ const endMarker = document.getElementById('end-marker');
1076
+ const tempoInput = document.getElementById('tempo');
1077
+ const tempoSpeedValue = document.getElementById('tempo-speed-value');
1078
+ const videoControls = document.querySelector('.video-controls');
1079
 
1080
  // 音声オブジェクトを作成
1081
  const audioElements = {};
 
1145
  const avgDrift = syncDriftLog.reduce((a, b) => a + b, 0) / syncDriftLog.length;
1146
 
1147
  // ズレ表示を更新
1148
+ syncStatusText.textContent = `同期ズレ: ${avgDrift.toFixed(3)}秒`;
1149
 
1150
  // ズレが大きい場合(0.1秒以上)に修正
1151
  if (Math.abs(avgDrift) > 0.1) {
 
1213
  const audio = audioElements[file];
1214
  if (!audio) return null;
1215
 
1216
+ // 音量が0の場合はスキップ
1217
+ if (currentVolumes[file] === 0) return null;
1218
+
1219
  const response = await fetch(`${basePath}${file}.mp3`);
1220
  const arrayBuffer = await response.arrayBuffer();
1221
  return await audioContext.decodeAudioData(arrayBuffer);
 
1239
 
1240
  // 各音声バッファを結合
1241
  for (let file of audioFiles) {
1242
+ if (!audioBuffers[file] || currentVolumes[file] === 0) continue;
1243
 
1244
  const buffer = audioBuffers[file];
1245
  const volume = currentVolumes[file];
1246
 
 
 
 
1247
  // 各チャンネルに音声を加算
1248
  for (let channel = 0; channel < 2; channel++) {
1249
  const inputData = buffer.getChannelData(channel % buffer.numberOfChannels);
 
1300
  }
1301
  }
1302
 
1303
+ function bufferToWave(abuffer) {
1304
+ const numOfChan = abuffer.numberOfChannels,
1305
+ length = abuffer.length * numOfChan * 2 + 44,
1306
+ buffer = new ArrayBuffer(length),
1307
+ view = new DataView(buffer),
1308
+ channels = [],
1309
+ sampleRate = abuffer.sampleRate;
1310
+
1311
+ // posをletで宣言(constから変更)
1312
+ let pos = 0;
1313
+
1314
+ // write WAV header
1315
+ setUint32(0x46464952); // "RIFF"
1316
+ setUint32(length - 8); // file length - 8
1317
+ setUint32(0x45564157); // "WAVE"
1318
+
1319
+ setUint32(0x20746d66); // "fmt " chunk
1320
+ setUint32(16); // length = 16
1321
+ setUint16(1); // PCM (uncompressed)
1322
+ setUint16(numOfChan);
1323
+ setUint32(sampleRate);
1324
+ setUint32(sampleRate * 2 * numOfChan);
1325
+ setUint16(numOfChan * 2);
1326
+ setUint16(16);
1327
+
1328
+ setUint32(0x61746164); // "data" - chunk
1329
+ setUint32(length - pos - 4);
1330
+
1331
+ // write interleaved data
1332
+ for (let i = 0; i < abuffer.length; i++) {
1333
+ for (let channel = 0; channel < numOfChan; channel++) {
1334
+ let sample = abuffer.getChannelData(channel)[i] * 0x7fff;
1335
+ if (sample < -32768) sample = -32768;
1336
+ if (sample > 32767) sample = 32767;
1337
+ view.setInt16(pos, sample, true);
1338
+ pos += 2;
1339
+ }
1340
+ }
1341
+
1342
+ function setUint16(data) {
1343
+ view.setUint16(pos, data, true);
1344
  pos += 2;
1345
  }
 
1346
 
1347
+ function setUint32(data) {
1348
+ view.setUint32(pos, data, true);
1349
+ pos += 4;
1350
+ }
1351
 
1352
+ return new Blob([buffer], { type: 'audio/wav' });
 
 
1353
  }
1354
 
 
 
 
1355
  function applyVolume() {
1356
  if (!isAudioCombined) return;
1357
 
 
1399
  fullscreenBtn.disabled = false;
1400
  startTimeInput.disabled = false;
1401
  endTimeInput.disabled = false;
1402
+ resetEndTimeBtn.disabled = false;
1403
  loopCheckbox.disabled = false;
1404
  globalVolumeSlider.disabled = false;
1405
  setStartTimeBtn.disabled = false;
 
1695
  updatePlaybackRate(speed);
1696
  });
1697
 
1698
+ // テンポ入力による再生速度更新
1699
+ tempoInput.addEventListener('input', function() {
1700
+ const tempo = parseFloat(this.value);
1701
+ const baseTempo = isTMode ? 66 : 92;
1702
+ const speed = tempo / baseTempo;
1703
+
1704
+ // 速度を0.5~2.0の範囲に制限
1705
+ const clampedSpeed = Math.max(0.5, Math.min(2.0, speed));
1706
+
1707
+ playbackSpeedSlider.value = clampedSpeed;
1708
+ playbackSpeedValue.textContent = clampedSpeed.toFixed(2) + 'x';
1709
+ speedSlider.value = clampedSpeed;
1710
+ speedValue.textContent = clampedSpeed.toFixed(2) + 'x';
1711
+ tempoSpeedValue.textContent = clampedSpeed.toFixed(2) + 'x';
1712
+
1713
+ updatePlaybackRate(clampedSpeed);
1714
+ });
1715
 
1716
+ function updatePlaybackRate(speed) {
1717
+ if (!isAudioCombined) return;
1718
+
1719
+ currentPlaybackRate = speed;
1720
+ video.playbackRate = speed;
1721
 
1722
+ // ピッチ保持を再設定
1723
+ video.preservesPitch = true;
1724
+ video.mozPreservesPitch = true;
1725
+ video.webkitPreservesPitch = true;
1726
+
1727
+ if (combinedAudioElement) {
1728
+ combinedAudioElement.playbackRate = speed;
1729
+
1730
+ // 合成音声のピッチ保持を設定
1731
+ combinedAudioElement.preservesPitch = true;
1732
+ combinedAudioElement.mozPreservesPitch = true;
1733
+ combinedAudioElement.webkitPreservesPitch = true;
1734
+ }
1735
  }
 
1736
 
1737
  // 全画面ボタン
1738
  fullscreenBtn.addEventListener('click', function() {
 
1764
  isFullscreen = !!(document.fullscreenElement || document.webkitFullscreenElement || document.msFullscreenElement);
1765
  fullscreenBtn.textContent = isFullscreen ? '⛶' : '⛶';
1766
  video.controls = false;
1767
+
1768
+ // 全画面時にロックボタンを表示
1769
+ lockControlsBtn.style.display = isFullscreen ? 'flex' : 'none';
1770
+
1771
+ // 全画面時にコントロールバー自動非表示機能を有効化
1772
+ if (isFullscreen) {
1773
+ resetControlsHideTimer();
1774
+ document.addEventListener('mousemove', handleFullscreenMouseMove);
1775
+ } else {
1776
+ document.removeEventListener('mousemove', handleFullscreenMouseMove);
1777
+ clearTimeout(controlsHideTimeout);
1778
+ showControls();
1779
+ }
1780
+ }
1781
+
1782
+ // 全画面時のマウス移動処理
1783
+ function handleFullscreenMouseMove() {
1784
+ if (!isControlsLocked) {
1785
+ showControls();
1786
+ resetControlsHideTimer();
1787
+ }
1788
+ }
1789
+
1790
+ // コントロールバーを表示
1791
+ function showControls() {
1792
+ if (!controlsVisible) {
1793
+ videoControls.style.opacity = '1';
1794
+ controlsVisible = true;
1795
+ }
1796
+ }
1797
+
1798
+ // コントロールバーを非表示
1799
+ function hideControls() {
1800
+ if (!isControlsLocked && controlsVisible) {
1801
+ videoControls.style.opacity = '0';
1802
+ controlsVisible = false;
1803
+ }
1804
  }
1805
 
1806
+ // コントロールバー非表示タイマーをリセット
1807
+ function resetControlsHideTimer() {
1808
+ clearTimeout(controlsHideTimeout);
1809
+ if (!isControlsLocked) {
1810
+ controlsHideTimeout = setTimeout(hideControls, 1500); // 1.5秒後に非表示
1811
+ }
1812
+ }
1813
+
1814
+ // ロックボタンのクリック処理
1815
+ lockControlsBtn.addEventListener('click', function() {
1816
+ isControlsLocked = !isControlsLocked;
1817
+ this.classList.toggle('locked', isControlsLocked);
1818
+
1819
+ if (isControlsLocked) {
1820
+ showControls();
1821
+ clearTimeout(controlsHideTimeout);
1822
+ } else {
1823
+ resetControlsHideTimer();
1824
+ }
1825
+ });
1826
+
1827
  // キーボードイベント (ESCで全画面終了)
1828
  document.addEventListener('keydown', function(e) {
1829
  if (e.key === 'Escape' && isFullscreen) {
 
1866
  // 現在の秒数を開始時間に設定
1867
  setStartTimeBtn.addEventListener('click', function() {
1868
  startTimeInput.value = video.currentTime.toFixed(2);
1869
+ updateProgressMarkers();
1870
  });
1871
 
1872
  // 現在の秒数を終了時間に設定
1873
  setEndTimeBtn.addEventListener('click', function() {
1874
  endTimeInput.value = video.currentTime.toFixed(2);
1875
+ updateProgressMarkers();
1876
  });
1877
 
1878
+ // 終了時間を動画の長さにリセット
1879
+ resetEndTimeBtn.addEventListener('click', function() {
1880
+ endTimeInput.value = video.duration.toFixed(2);
1881
+ updateProgressMarkers();
1882
+ });
1883
+
1884
+ // プログレスバーのマーカーを更新
1885
+ function updateProgressMarkers() {
1886
+ const duration = video.duration || videoDuration;
1887
+ const startTime = parseFloat(startTimeInput.value) || 0;
1888
+ const endTime = parseFloat(endTimeInput.value) || duration;
1889
+
1890
+ if (duration > 0) {
1891
+ startMarker.style.left = `${(startTime / duration) * 100}%`;
1892
+ endMarker.style.left = `${(endTime / duration) * 100}%`;
1893
+
1894
+ startMarker.style.display = 'block';
1895
+ endMarker.style.display = 'block';
1896
+ }
1897
+ }
1898
+
1899
+ // 開始/終了時間変更時にマーカーを更新
1900
+ startTimeInput.addEventListener('input', updateProgressMarkers);
1901
+ endTimeInput.addEventListener('input', updateProgressMarkers);
1902
+
1903
  // 合成ボタンクリック
1904
  combineButton.addEventListener('click', combineAudio);
1905
 
1906
  // プレビューボタンクリック
1907
  previewButton.addEventListener('click', togglePreview);
1908
 
1909
+ // 同期ステータスを閉じる
1910
+ syncStatusClose.addEventListener('click', function() {
1911
+ syncStatus.style.display = 'none';
1912
+ });
1913
+
1914
  // 初期化
1915
  loadAudioFiles();
1916
  updateVolumeIcon();
 
1945
 
1946
  initSliderBackgrounds();
1947
  startSyncCheck(); // 同期チェックを開始
1948
+
1949
+ // 初期テンポ設定
1950
+ tempoInput.value = isTMode ? 66 : 92;
1951
+ tempoInput.dispatchEvent(new Event('input'));
1952
  });
1953
+ </script>
1954
+ </body>
1955
+ </html>