soiz1 commited on
Commit
c607128
·
1 Parent(s): 5ab9fd1

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +1041 -1107
index.html CHANGED
@@ -1235,438 +1235,418 @@ window.addEventListener('load', async () => {
1235
  <!-- 全画面時のロックボタン -->
1236
  <button class="lock-controls-btn" id="lock-controls-btn" title="コントロールバーを固定">🔒</button>
1237
  <script>
1238
- document.addEventListener('DOMContentLoaded', function() {
1239
- // 同期管理用の変数
1240
- let lastSyncTime = 0;
1241
- let isBuffering = false;
1242
- let syncDriftLog = [];
1243
- let syncCheckInterval;
1244
- let audioContext;
1245
- let controlsHideTimeout;
1246
- let isControlsLocked = false;
1247
- let controlsVisible = true;
1248
- let isCheckingSync = false;
1249
- let isInBackgroundTab = false;
1250
- let isBlackVideoMode = false; // 消画モードフラグ
1251
-
1252
- try {
1253
- audioContext = new (window.AudioContext || window.webkitAudioContext)();
1254
- } catch (e) {
1255
- console.error('Web Audio APIがサポートされていません:', e);
1256
- }
1257
-
1258
- // テクノロジー風背景を生成
1259
- function createTechBackground() {
1260
- const bg = document.getElementById('techBg');
1261
-
1262
- for (let i = 0; i < 200; i++) {
1263
- const line = document.createElement('div');
1264
- line.className = 'circuit-line';
1265
 
1266
- const isHorizontal = Math.random() > 0.5;
1267
- if (isHorizontal) {
1268
- line.style.width = `${Math.random() * 300 + 100}px`;
1269
- line.style.height = '1px';
1270
- } else {
1271
- line.style.width = '1px';
1272
- line.style.height = `${Math.random() * 300 + 100}px`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1273
  }
1274
 
1275
- line.style.left = `${Math.random() * 100}%`;
1276
- line.style.top = `${Math.random() * 100}%`;
1277
- line.style.opacity = Math.random() * 0.5 + 0.1;
1278
- bg.appendChild(line);
1279
- }
1280
-
1281
- for (let i = 0; i < 200; i++) {
1282
- const dot = document.createElement('div');
1283
- dot.className = 'grid-dot';
1284
- dot.style.left = `${Math.random() * 100}%`;
1285
- dot.style.top = `${Math.random() * 100}%`;
1286
- bg.appendChild(dot);
1287
- }
1288
-
1289
- for (let i = 0; i < 15; i++) {
1290
- const hex = document.createElement('div');
1291
- hex.className = 'hexagon';
1292
- hex.style.left = `${Math.random() * 100}%`;
1293
- hex.style.top = `${Math.random() * 100}%`;
1294
- hex.style.transform = `rotate(${Math.random() * 360}deg)`;
1295
- hex.style.animation = `float ${Math.random() * 10 + 5}s infinite ease-in-out`;
1296
- bg.appendChild(hex);
1297
- }
1298
-
1299
- for (let i = 0; i < 8; i++) {
1300
- const pulse = document.createElement('div');
1301
- pulse.className = 'pulse';
1302
- pulse.style.left = `${Math.random() * 100}%`;
1303
- pulse.style.top = `${Math.random() * 100}%`;
1304
- pulse.style.animationDelay = `${Math.random() * 2}s`;
1305
- bg.appendChild(pulse);
1306
- }
1307
- }
1308
-
1309
- createTechBackground();
1310
-
1311
- const urlParams = new URLSearchParams(window.location.search);
1312
- const isTMode = urlParams.has('mode') && urlParams.get('mode') === 't';
1313
- const basePath = isTMode ? '/t/' : '/';
1314
-
1315
- // ローディング状態を管理
1316
- let loadingCount = 0;
1317
- let totalToLoad = 6; // 動画 + 5つの音声ファイル
1318
- let lastUpdateTime = 0;
1319
- const updateInterval = 1;
1320
-
1321
- function checkLoadingComplete() {
1322
- loadingCount++;
1323
- if (loadingCount >= totalToLoad) {
1324
- setTimeout(function() {
1325
- const loadingOverlay = document.getElementById('loadingOverlay');
1326
- loadingOverlay.style.opacity = '0';
1327
- setTimeout(function() {
1328
- loadingOverlay.style.display = 'none';
1329
- }, 1000);
1330
- }, 500);
1331
- }
1332
- }
1333
-
1334
- function handleError(error, message) {
1335
- console.error(message, error);
1336
- window.alert(`${message}\n\nエラー詳細: ${error.message}`);
1337
- }
1338
-
1339
- // 要素を取得
1340
- const video = document.getElementById('video');
1341
- video.preservesPitch = true;
1342
- video.mozPreservesPitch = true; // Firefox用
1343
- video.webkitPreservesPitch = true; // 古いWebKit用
1344
- const videoContainer = document.getElementById('video-container');
1345
- const playPauseBtn = document.getElementById('play-pause-btn');
1346
- const timeDisplay = document.getElementById('time-display');
1347
- const progressContainer = document.getElementById('progress-container');
1348
- const progressBar = document.getElementById('progress-bar');
1349
- const progressTime = document.getElementById('progress-time');
1350
- const volumeBtn = document.getElementById('volume-btn');
1351
- const volumeSlider = document.getElementById('volume-slider');
1352
- const speedSlider = document.getElementById('speed-slider');
1353
- const speedValue = document.getElementById('speed-value');
1354
- const playbackSpeedSlider = document.getElementById('playback-speed');
1355
- const playbackSpeedValue = document.getElementById('playback-speed-value');
1356
- const fullscreenBtn = document.getElementById('fullscreen-btn');
1357
- const startTimeInput = document.getElementById('start-time');
1358
- const endTimeInput = document.getElementById('end-time');
1359
- const loopCheckbox = document.getElementById('loop');
1360
- const globalVolumeSlider = document.getElementById('global-volume');
1361
- const globalVolumeValue = document.getElementById('global-volume-value');
1362
- const audioSliders = document.querySelectorAll('.audio-slider');
1363
- const volumeValues = document.querySelectorAll('.volume-value');
1364
- const setStartTimeBtn = document.getElementById('set-start-time');
1365
- const setEndTimeBtn = document.getElementById('set-end-time');
1366
- const resetEndTimeBtn = document.getElementById('reset-end-time');
1367
- const disabledOverlay = document.getElementById('disabledOverlay');
1368
- const combineButton = document.getElementById('combine-button');
1369
- const combineStatus = document.getElementById('combine-status');
1370
- const previewSection = document.getElementById('preview-section');
1371
- const previewButton = document.getElementById('preview-button');
1372
- const previewTime = document.getElementById('preview-time');
1373
- const bufferingIndicator = document.getElementById('buffering-indicator');
1374
- const syncStatus = document.getElementById('sync-status');
1375
- const syncStatusText = document.getElementById('sync-status-text');
1376
- const syncStatusClose = document.getElementById('sync-status-close');
1377
- const lockControlsBtn = document.getElementById('lock-controls-btn');
1378
- const startMarker = document.getElementById('start-marker');
1379
- const endMarker = document.getElementById('end-marker');
1380
- const tempoInput = document.getElementById('tempo');
1381
- const tempoSpeedValue = document.getElementById('tempo-speed-value');
1382
- const videoControls = document.querySelector('.video-controls');
1383
- const applyTimeBtn = document.getElementById('apply-time-btn');
1384
- const blackVideoToggle = document.createElement('button');
1385
- blackVideoToggle.className = 'control-button';
1386
- blackVideoToggle.id = 'black-video-btn';
1387
- blackVideoToggle.textContent = '消画モード';
1388
- blackVideoToggle.title = '動画を黒画面に切り替え(読み込み遅延防止)';
1389
- document.querySelector('.main-controls').appendChild(blackVideoToggle);
1390
-
1391
- // 音声オブジェクトを作成
1392
- const audioElements = {};
1393
- const audioBuffers = {};
1394
- const audioFiles = ['p', 'a', 't', 's', 'k'];
1395
- let combinedAudioElement = null;
1396
- let isAudioCombined = false;
1397
- let currentVolumes = { p: 0, a: 1, t: 1, s: 1, k: 0 };
1398
-
1399
- // 初期化
1400
- let videoDuration = 0;
1401
- let isPlaying = false;
1402
- let lastVolume = 1;
1403
- let currentPlaybackRate = 1;
1404
- let isFullscreen = false;
1405
- let isTimeApplied = false; // 時間設定が適用されたかどうか
1406
-
1407
- async function enterPiP() {
1408
- if (!document.pictureInPictureElement && !video.paused) {
1409
- try {
1410
- await video.requestPictureInPicture();
1411
- } catch (err) {
1412
- console.warn('PiP開始失敗:', err);
1413
  }
1414
- }
1415
- }
1416
-
1417
- async function exitPiP() {
1418
- if (document.pictureInPictureElement) {
1419
- try {
1420
- await document.exitPictureInPicture();
1421
- } catch (err) {
1422
- console.warn('PiP終了失敗:', err);
 
 
 
 
 
 
 
 
 
1423
  }
1424
- }
1425
- }
1426
 
1427
- // 動画のバッファリング状態を監視
1428
- video.addEventListener('waiting', function() {
1429
- isBuffering = true;
1430
- bufferingIndicator.style.display = 'block';
1431
- if (combinedAudioElement) {
1432
- combinedAudioElement.pause();
1433
- }
1434
- });
1435
-
1436
- video.addEventListener('playing', function() {
1437
- isBuffering = false;
1438
- bufferingIndicator.style.display = 'none';
1439
- if (combinedAudioElement && isPlaying) {
1440
- syncAudioWithVideo();
1441
- }
1442
- });
1443
-
1444
- video.addEventListener('suspend', function() {
1445
- console.log('動画の読み込みが一時停止しました');
1446
- });
1447
-
1448
- video.addEventListener('stalled', function() {
1449
- console.log('動画の読み込みが停滞しました');
1450
- if (isPlaying) {
1451
- pauseMedia();
1452
- }
1453
- });
1454
-
1455
- function startSyncCheck() {
1456
- if (isCheckingSync) return;
1457
- isCheckingSync = true;
1458
- if (syncCheckInterval) clearInterval(syncCheckInterval);
1459
- syncCheckInterval = setInterval(checkSync, 1000);
1460
  }
1461
 
1462
- function stopSyncCheck() {
1463
- isCheckingSync = false;
1464
- if (syncCheckInterval) clearInterval(syncCheckInterval);
1465
- }
1466
 
1467
- function checkSync() {
1468
- if (!isAudioCombined || !isPlaying || isBuffering || isInBackgroundTab) return;
1469
-
1470
- const videoTime = video.currentTime;
1471
- const audioTime = combinedAudioElement.currentTime;
1472
- const drift = videoTime - audioTime;
1473
-
1474
- syncDriftLog.push(drift);
1475
- if (syncDriftLog.length > 5) syncDriftLog.shift();
1476
-
1477
- const avgDrift = syncDriftLog.reduce((a, b) => a + b, 0) / syncDriftLog.length;
1478
- syncStatusText.textContent = `同期ズレ: ${avgDrift.toFixed(3)}秒`;
1479
-
1480
- if (Math.abs(avgDrift) > 0.1) {
1481
- console.log(`同期ズレを修正: ${avgDrift.toFixed(3)}秒`);
1482
- syncAudioWithVideo();
1483
  }
1484
  }
1485
-
1486
- applyTimeBtn.addEventListener('click', function() {
1487
- // 時間設定を適用
1488
- isTimeApplied = true;
1489
-
1490
- // 現在再生中なら一時停止
1491
- const wasPlaying = isPlaying;
1492
- if (isPlaying) {
1493
- pauseMedia();
1494
- }
1495
-
1496
- // 開始時間と終了時間を取得
1497
- const startTime = parseFloat(startTimeInput.value) || 0;
1498
- const endTime = parseFloat(endTimeInput.value) || video.duration;
1499
-
1500
- // 現在位置が開始時間より前なら開始時間に移動
1501
- if (video.currentTime < startTime) {
1502
- video.currentTime = startTime;
1503
- if (combinedAudioElement) {
1504
- combinedAudioElement.currentTime = startTime;
1505
- }
1506
- }
1507
- // 現在位置が終了時間より後なら開始時間に移動
1508
- else if (video.currentTime > endTime) {
1509
- video.currentTime = startTime;
1510
- if (combinedAudioElement) {
1511
- combinedAudioElement.currentTime = startTime;
1512
- }
1513
- }
1514
-
1515
- // 再生中だった場合は再開
1516
- if (wasPlaying) {
1517
- playMedia();
1518
- }
1519
-
1520
- // マーカーを更新
1521
- updateProgressMarkers();
1522
- });
1523
-
1524
- // 音声を動画に同期させる関数
1525
- function syncAudioWithVideo() {
1526
- if (!isAudioCombined || !isPlaying) return;
1527
-
1528
- const currentTime = video.currentTime;
1529
-
1530
  if (combinedAudioElement) {
1531
- combinedAudioElement.currentTime = currentTime;
1532
- if (isPlaying) {
1533
- combinedAudioElement.play().catch(e => console.error('音声再生エラー:', e));
1534
- }
1535
  }
1536
-
1537
- // ズレ記録をリセット
1538
- syncDriftLog = [];
1539
- lastSyncTime = performance.now();
1540
  }
1541
-
1542
- // 音声ファイルをロード
1543
- function loadAudioFiles() {
1544
- audioFiles.forEach(file => {
1545
- try {
1546
- const audio = new Audio(`${basePath}${file}.mp3`);
1547
- audio.preload = 'auto';
1548
- audio.loop = false;
1549
- audioElements[file] = audio;
1550
-
1551
- audio.addEventListener('loadedmetadata', function() {
1552
- console.log(`${basePath}${file}.mp3 loaded`);
1553
- checkLoadingComplete();
1554
- });
1555
-
1556
- audio.addEventListener('error', function() {
1557
- console.error(`音声ファイル読み込みエラー (${basePath}${file}.mp3):`, audio.error);
1558
- checkLoadingComplete();
1559
- });
1560
- } catch (error) {
1561
- console.error(`音声ファイル初期化エラー (${basePath}${file}.mp3):`, error);
1562
- checkLoadingComplete();
1563
- }
1564
- });
1565
  }
 
 
 
 
1566
 
1567
- // 音声を結合する関数
1568
- async function combineAudio() {
1569
- // 再生中なら停止
1570
- if (isPlaying) {
1571
- pauseMedia();
1572
- }
1573
-
1574
- combineButton.disabled = true;
1575
- combineStatus.textContent = "音声を合成中...";
1576
-
1577
- try {
1578
- // 現在の音量設定を保存
1579
- audioFiles.forEach(file => {
1580
- currentVolumes[file] = parseFloat(document.querySelector(`.audio-slider[data-audio="${file}"]`).value);
1581
- });
1582
-
1583
- // 各音声ファイルをデコード
1584
- const audioBufferPromises = audioFiles.map(async file => {
1585
- const audio = audioElements[file];
1586
- if (!audio) return null;
1587
-
1588
- // 音量が0の場合はスキップ
1589
- if (currentVolumes[file] === 0) return null;
1590
-
1591
- const response = await fetch(`${basePath}${file}.mp3`);
1592
- const arrayBuffer = await response.arrayBuffer();
1593
- return await audioContext.decodeAudioData(arrayBuffer);
1594
- });
1595
-
1596
- // すべての音声バッファを取得
1597
- const buffers = await Promise.all(audioBufferPromises);
1598
- audioFiles.forEach((file, index) => {
1599
- audioBuffers[file] = buffers[index];
1600
- });
1601
-
1602
- // 最長の音声バッファの長さを取得
1603
- const maxDuration = Math.max(...buffers.filter(b => b).map(b => b.duration));
1604
-
1605
- // 新しい音声バッファを作成
1606
- const combinedAudioBuffer = audioContext.createBuffer(
1607
- 2, // ステレオ
1608
- audioContext.sampleRate * maxDuration,
1609
- audioContext.sampleRate
1610
- );
1611
-
1612
- // 各音声バッファを結合
1613
- for (let file of audioFiles) {
1614
- if (!audioBuffers[file] || currentVolumes[file] === 0) continue;
1615
-
1616
- const buffer = audioBuffers[file];
1617
- const volume = currentVolumes[file];
1618
-
1619
- // 各チャンネルに音声を加算
1620
- for (let channel = 0; channel < 2; channel++) {
1621
- const inputData = buffer.getChannelData(channel % buffer.numberOfChannels);
1622
- const outputData = combinedAudioBuffer.getChannelData(channel);
1623
-
1624
- for (let i = 0; i < inputData.length; i++) {
1625
- outputData[i] += inputData[i] * volume;
1626
- }
1627
- }
1628
- }
1629
-
1630
- // 音量を正規化 (クリッピング防止)
1631
- for (let channel = 0; channel < 2; channel++) {
1632
- const outputData = combinedAudioBuffer.getChannelData(channel);
1633
- let max = 0;
1634
-
1635
- for (let i = 0; i < outputData.length; i++) {
1636
- if (Math.abs(outputData[i]) > max) {
1637
- max = Math.abs(outputData[i]);
1638
  }
 
 
 
 
1639
  }
1640
-
1641
- if (max > 1) {
1642
- for (let i = 0; i < outputData.length; i++) {
1643
- outputData[i] /= max;
1644
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1645
  }
1646
- }
1647
-
1648
- // AudioBufferをBlobに変換
1649
- const blob = bufferToWave(combinedAudioBuffer);
1650
- const url = URL.createObjectURL(blob);
1651
-
1652
- // 新しいaudio要素を作成
1653
- if (combinedAudioElement) {
1654
- combinedAudioElement.pause();
1655
- URL.revokeObjectURL(combinedAudioElement.src);
1656
- }
1657
- combinedAudioElement = new Audio(url);
1658
- combinedAudioElement.preservesPitch = true;
1659
- combinedAudioElement.mozPreservesPitch = true;
1660
- combinedAudioElement.webkitPreservesPitch = true;
1661
- combinedAudioElement.playbackRate = currentPlaybackRate;
1662
-
1663
- isAudioCombined = true;
1664
- isTimeApplied = false; // 音声再合成時に時間設定をリセット
1665
- combineStatus.textContent = "音声の合成が完了しました";
1666
- enablePlayerControls();
1667
-
1668
- combineButton.disabled = false;
1669
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1670
  document.addEventListener('visibilitychange', async () => {
1671
  if (document.hidden) {
1672
  // タブが非表示になった場合
@@ -1722,716 +1702,680 @@ document.addEventListener('DOMContentLoaded', function() {
1722
  }
1723
  });
1724
 
1725
- // 動画終了時に自動的にPiPを閉じる(次回再開のため)
1726
- video.addEventListener('ended', exitPiP);
1727
-
1728
- // 合成後に音量と再生速度を適用
1729
- applyVolume();
1730
- applyPlaybackRate();
1731
-
1732
- } catch (error) {
1733
- console.error('音声合成エラー:', error);
1734
- combineStatus.textContent = "音声の合成に失敗しました";
1735
- combineButton.disabled = false;
1736
- }
1737
- }
1738
-
1739
- function bufferToWave(abuffer) {
1740
- const numOfChan = abuffer.numberOfChannels,
1741
- length = abuffer.length * numOfChan * 2 + 44,
1742
- buffer = new ArrayBuffer(length),
1743
- view = new DataView(buffer),
1744
- channels = [],
1745
- sampleRate = abuffer.sampleRate;
1746
 
1747
- // posをletで宣言(constから変更)
1748
- let pos = 0;
1749
-
1750
- // write WAV header
1751
- setUint32(0x46464952); // "RIFF"
1752
- setUint32(length - 8); // file length - 8
1753
- setUint32(0x45564157); // "WAVE"
1754
-
1755
- setUint32(0x20746d66); // "fmt " chunk
1756
- setUint32(16); // length = 16
1757
- setUint16(1); // PCM (uncompressed)
1758
- setUint16(numOfChan);
1759
- setUint32(sampleRate);
1760
- setUint32(sampleRate * 2 * numOfChan);
1761
- setUint16(numOfChan * 2);
1762
- setUint16(16);
1763
-
1764
- setUint32(0x61746164); // "data" - chunk
1765
- setUint32(length - pos - 4);
1766
-
1767
- // write interleaved data
1768
- for (let i = 0; i < abuffer.length; i++) {
1769
- for (let channel = 0; channel < numOfChan; channel++) {
1770
- let sample = abuffer.getChannelData(channel)[i] * 0x7fff;
1771
- if (sample < -32768) sample = -32768;
1772
- if (sample > 32767) sample = 32767;
1773
- view.setInt16(pos, sample, true);
1774
- pos += 2;
1775
- }
1776
- }
1777
-
1778
- function setUint16(data) {
1779
- view.setUint16(pos, data, true);
1780
- pos += 2;
1781
- }
1782
-
1783
- function setUint32(data) {
1784
- view.setUint32(pos, data, true);
1785
- pos += 4;
1786
- }
1787
-
1788
- return new Blob([buffer], { type: 'audio/wav' });
1789
- }
1790
-
1791
- function applyVolume() {
1792
- if (!isAudioCombined) return;
1793
-
1794
- // ベース音量 (0-1)
1795
- const baseVolume = parseFloat(volumeSlider.value);
1796
- // グローバル音量係数 (0-10)
1797
- const globalVolume = parseFloat(globalVolumeSlider.value) / 10; // 0-1に変換
1798
-
1799
- // 最終音量 (0-1)
1800
- const finalVolume = Math.max(0, Math.min(1, baseVolume * globalVolume));
1801
-
1802
- // 動画と音声の音量を設定
1803
- video.volume = finalVolume;
1804
- if (combinedAudioElement) {
1805
- combinedAudioElement.volume = finalVolume;
1806
- }
1807
-
1808
- // 音量アイコンを更新
1809
- updateVolumeIcon();
1810
- }
1811
-
1812
- function applyPlaybackRate() {
1813
- if (!isAudioCombined) return;
1814
-
1815
- const speed = parseFloat(playbackSpeedSlider.value);
1816
- currentPlaybackRate = speed;
1817
- video.playbackRate = speed;
1818
-
1819
- if (combinedAudioElement) {
1820
- combinedAudioElement.playbackRate = speed;
1821
- }
1822
-
1823
- speedValue.textContent = speed.toFixed(2) + 'x';
1824
- playbackSpeedValue.textContent = speed.toFixed(2) + 'x';
1825
- speedSlider.value = speed;
1826
- }
1827
-
1828
- // プレイヤーコントロールを有効化
1829
- function enablePlayerControls() {
1830
- disabledOverlay.style.display = 'none';
1831
- playPauseBtn.disabled = false;
1832
- volumeBtn.disabled = false;
1833
- volumeSlider.disabled = false;
1834
- speedSlider.disabled = false;
1835
- fullscreenBtn.disabled = false;
1836
- startTimeInput.disabled = false;
1837
- endTimeInput.disabled = false;
1838
- resetEndTimeBtn.disabled = false;
1839
- loopCheckbox.disabled = false;
1840
- globalVolumeSlider.disabled = false;
1841
- setStartTimeBtn.disabled = false;
1842
- setEndTimeBtn.disabled = false;
1843
- playbackSpeedSlider.disabled = false;
1844
- applyTimeBtn.disabled = false;
1845
- blackVideoToggle.disabled = false;
1846
- }
1847
-
1848
- // プレビュー再生
1849
- function togglePreview() {
1850
- if (!isAudioCombined || !combinedAudioElement) return;
1851
-
1852
- if (previewButton.textContent === '▶') {
1853
- // 再生
1854
- combinedAudioElement.currentTime = 0;
1855
- combinedAudioElement.play()
1856
- .then(() => {
1857
- previewButton.textContent = '⏸';
1858
-
1859
- // プレビューの時間表示を更新
1860
- const updatePreviewTime = () => {
1861
- if (!combinedAudioElement || !isAudioCombined) return;
1862
-
1863
- const currentTime = combinedAudioElement.currentTime;
1864
- const duration = combinedAudioElement.duration;
1865
 
1866
- if (currentTime >= duration) {
1867
- previewButton.textContent = '▶';
1868
- previewTime.textContent = `00:00 / ${formatTime(duration)}`;
1869
- return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1870
  }
1871
-
1872
- previewTime.textContent = `${formatTime(currentTime)} / ${formatTime(duration)}`;
1873
- requestAnimationFrame(updatePreviewTime);
1874
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
1875
 
1876
- updatePreviewTime();
1877
- })
1878
- .catch(e => console.error('プレビュー再生エラー:', e));
1879
- } else {
1880
- // 一時停止
1881
- combinedAudioElement.pause();
1882
- previewButton.textContent = '▶';
1883
- }
1884
- }
1885
-
1886
- // 時間をフォーマットするヘルパー関数
1887
- function formatTime(seconds) {
1888
- const mins = Math.floor(seconds / 60);
1889
- const secs = Math.floor(seconds % 60);
1890
- return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
1891
- }
1892
-
1893
- // 動画のメタデータが読み込まれたら
1894
- video.addEventListener('loadedmetadata', function() {
1895
- try {
1896
- videoDuration = video.duration;
1897
- endTimeInput.value = videoDuration.toFixed(2);
1898
- endTimeInput.max = videoDuration;
1899
- startTimeInput.max = videoDuration - 0.1;
1900
- updateTimeDisplay();
1901
- updateProgressMarkers(); // マーカーを初期化
1902
- checkLoadingComplete();
1903
- } catch (error) {
1904
- handleError(error, '動画メタデータ読み込み中にエラーが発生しました');
1905
- }
1906
- });
1907
-
1908
- // 動画エラー処理
1909
- video.addEventListener('error', function() {
1910
- handleError(video.error, '動画読み込み中にエラーが発生しました');
1911
- });
1912
-
1913
- // 再生ボタンクリック
1914
- playPauseBtn.addEventListener('click', function() {
1915
- if (!isTimeApplied) {
1916
- alert('時間設定を適用してください');
1917
- return;
1918
- }
1919
-
1920
- const endTime = parseFloat(endTimeInput.value) || videoDuration;
1921
- if (video.currentTime >= endTime) {
1922
- const startTime = parseFloat(startTimeInput.value) || 0;
1923
- seekMedia(startTime);
1924
- }
1925
- togglePlayPause();
1926
- });
1927
-
1928
- // 時間表示を更新
1929
- function updateTimeDisplay() {
1930
- const now = performance.now();
1931
- if (now - lastUpdateTime < updateInterval && !isFullscreen) return;
1932
- lastUpdateTime = now;
1933
-
1934
- try {
1935
- const currentTime = video.currentTime;
1936
- const duration = video.duration || videoDuration;
1937
 
1938
- const currentMinutes = Math.floor(currentTime / 60);
1939
- const currentSeconds = Math.floor(currentTime % 60);
1940
- const currentMilliseconds = Math.floor((currentTime % 1) * 100);
1941
- const durationMinutes = Math.floor(duration / 60);
1942
- const durationSeconds = Math.floor(duration % 60);
1943
- const durationMilliseconds = Math.floor((duration % 1) * 100);
 
 
 
 
 
 
 
 
 
1944
 
1945
- timeDisplay.textContent =
1946
- `${String(currentMinutes).padStart(2, '0')}:${String(currentSeconds).padStart(2, '0')}.${String(currentMilliseconds).padStart(2, '0')} / ` +
1947
- `${String(durationMinutes).padStart(2, '0')}:${String(durationSeconds).padStart(2, '0')}.${String(durationMilliseconds).padStart(2, '0')}`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1948
 
1949
- const progressPercent = (currentTime / duration) * 100;
1950
- progressBar.style.width = `${progressPercent}%`;
1951
- } catch (error) {
1952
- console.error('時間表示更新エラー:', error);
1953
- }
1954
- }
1955
-
1956
- // 再生/一時停止をトグル
1957
- function togglePlayPause() {
1958
- if (isPlaying) {
1959
- pauseMedia();
1960
- } else {
1961
- playMedia();
1962
- }
1963
- }
1964
-
1965
- function playMedia() {
1966
- try {
1967
- const duration = video.duration || videoDuration;
1968
- const startTime = parseFloat(startTimeInput.value) || 0;
1969
- const endTime = parseFloat(endTimeInput.value) || duration;
1970
 
1971
- if (video.currentTime >= endTime) {
1972
- video.currentTime = startTime;
1973
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1974
 
1975
- const playPromise = video.play();
 
 
 
 
 
1976
 
1977
- if (playPromise !== undefined) {
1978
- playPromise.then(() => {
1979
- isPlaying = true;
1980
- playPauseBtn.textContent = '⏸';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1981
 
1982
- // 音声を同期して再生
1983
- if (isAudioCombined) {
1984
- combinedAudioElement.currentTime = video.currentTime;
1985
- combinedAudioElement.play().catch(e => console.error('音声再生エラー:', e));
1986
  }
1987
 
1988
- startSyncCheck();
1989
 
1990
- video.playbackRate = currentPlaybackRate;
1991
- }).catch(error => {
1992
- console.error('動画再生エラー:', error);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1993
  isPlaying = false;
1994
  playPauseBtn.textContent = '▶';
1995
- });
1996
  }
1997
- } catch (error) {
1998
- console.error('メディア再生エラー:', error);
1999
- isPlaying = false;
2000
- playPauseBtn.textContent = '▶';
2001
- }
2002
- }
2003
-
2004
- function pauseMedia() {
2005
- try {
2006
- video.pause();
2007
- isPlaying = false;
2008
- playPauseBtn.textContent = '▶';
2009
- stopSyncCheck();
2010
 
2011
- if (combinedAudioElement) {
2012
- combinedAudioElement.pause();
2013
- }
2014
- } catch (error) {
2015
- console.error('メディア一時停止エラー:', error);
2016
- }
2017
- }
2018
-
2019
- // 時間更新時の処理
2020
- video.addEventListener('timeupdate', function() {
2021
- if (!isTimeApplied) return;
2022
-
2023
- const duration = video.duration || videoDuration;
2024
- const endTime = parseFloat(endTimeInput.value) || duration;
2025
-
2026
- if (video.currentTime >= endTime && endTime > 0) {
2027
- if (loopCheckbox.checked) {
2028
- const startTime = parseFloat(startTimeInput.value) || 0;
2029
- video.currentTime = startTime;
2030
- if (combinedAudioElement) {
2031
- combinedAudioElement.currentTime = startTime;
2032
- }
2033
- } else {
2034
- pauseMedia();
2035
- video.currentTime = endTime;
2036
- if (combinedAudioElement) {
2037
- combinedAudioElement.currentTime = endTime;
2038
  }
2039
  }
2040
- }
2041
-
2042
- updateTimeDisplay();
2043
- });
2044
-
2045
- // プログレスバークリックでシーク
2046
- progressContainer.addEventListener('click', function(e) {
2047
- if (!video.duration) return;
2048
-
2049
- const rect = this.getBoundingClientRect();
2050
- const pos = (e.clientX - rect.left) / rect.width;
2051
- const seekTime = pos * video.duration;
2052
-
2053
- seekMedia(seekTime);
2054
- });
2055
 
2056
- // 指定した時間にシーク (改良版)
2057
- function seekMedia(time) {
2058
- try {
2059
- const duration = video.duration || videoDuration;
2060
  const startTime = parseFloat(startTimeInput.value) || 0;
2061
- const endTime = parseFloat(endTimeInput.value) || duration;
2062
-
2063
- const seekTime = Math.max(startTime, Math.min(time, endTime));
2064
- video.currentTime = seekTime;
2065
-
2066
  if (combinedAudioElement) {
2067
- combinedAudioElement.currentTime = seekTime;
2068
- if (isPlaying) {
2069
- combinedAudioElement.play().catch(e => console.error('音声再生エラー:', e));
2070
- }
2071
- }
2072
- } catch (error) {
2073
- console.error('メディアシークエラー:', error);
2074
- }
2075
- }
2076
-
2077
- // プログレスバー上でマウス移動時に時間を表示
2078
- progressContainer.addEventListener('mousemove', function(e) {
2079
- if (!video.duration) return;
2080
-
2081
- const rect = this.getBoundingClientRect();
2082
- const pos = (e.clientX - rect.left) / rect.width;
2083
- const hoverTime = pos * video.duration;
2084
-
2085
- const minutes = Math.floor(hoverTime / 60);
2086
- const seconds = Math.floor(hoverTime % 60);
2087
- const milliseconds = Math.floor((hoverTime % 1) * 100);
2088
-
2089
- progressTime.textContent = `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}.${String(milliseconds).padStart(2, '0')}`;
2090
- progressTime.style.display = 'block';
2091
- progressTime.style.left = `${pos * 100}%`;
2092
- });
2093
-
2094
- progressContainer.addEventListener('mouseleave', function() {
2095
- progressTime.style.display = 'none';
2096
- });
2097
-
2098
- // 動画クリックで再生/一時停止
2099
- video.addEventListener('click', function() {
2100
- togglePlayPause();
2101
- });
2102
-
2103
- volumeSlider.addEventListener('input', function() {
2104
- if (!isAudioCombined) return;
2105
-
2106
- lastVolume = parseFloat(this.value);
2107
- applyVolume();
2108
- });
2109
-
2110
- volumeBtn.addEventListener('click', function() {
2111
- if (!isAudioCombined) return;
2112
-
2113
- if (video.volume > 0) {
2114
- lastVolume = parseFloat(volumeSlider.value);
2115
- volumeSlider.value = 0;
2116
- } else {
2117
- volumeSlider.value = lastVolume;
2118
- }
2119
-
2120
- applyVolume();
2121
- });
2122
-
2123
- // 音量アイコンを更新
2124
- function updateVolumeIcon() {
2125
- if (video.volume === 0) {
2126
- volumeBtn.textContent = '🔇';
2127
- } else if (video.volume < 3) {
2128
- volumeBtn.textContent = '🔈';
2129
- } else {
2130
- volumeBtn.textContent = '🔊';
2131
- }
2132
- }
2133
-
2134
- // 再生速度スライダー (動画プレイヤー)
2135
- speedSlider.addEventListener('input', function() {
2136
- if (!isAudioCombined) return;
2137
-
2138
- const speed = parseFloat(this.value);
2139
- speedValue.textContent = speed.toFixed(2) + 'x';
2140
- playbackSpeedSlider.value = speed;
2141
- playbackSpeedValue.textContent = speed.toFixed(2) + 'x';
2142
- updatePlaybackRate(speed);
2143
- });
2144
-
2145
- // 再生速度スライダー (設定メニュー)
2146
- playbackSpeedSlider.addEventListener('input', function() {
2147
- if (!isAudioCombined) return;
2148
-
2149
- const speed = parseFloat(this.value);
2150
- playbackSpeedValue.textContent = speed.toFixed(2) + 'x';
2151
- speedSlider.value = speed;
2152
- speedValue.textContent = speed.toFixed(2) + 'x';
2153
- updatePlaybackRate(speed);
2154
- });
2155
-
2156
- // テンポ入力による再生速度更新
2157
- tempoInput.addEventListener('input', function() {
2158
- const tempo = parseFloat(this.value);
2159
- const baseTempo = isTMode ? 66 : 92;
2160
- const speed = tempo / baseTempo;
2161
-
2162
- const clampedSpeed = Math.max(0.001, Math.min(5.0, speed));
2163
-
2164
- playbackSpeedSlider.value = clampedSpeed;
2165
- playbackSpeedValue.textContent = clampedSpeed.toFixed(2) + 'x';
2166
- speedSlider.value = clampedSpeed;
2167
- speedValue.textContent = clampedSpeed.toFixed(2) + 'x';
2168
- tempoSpeedValue.textContent = clampedSpeed.toFixed(2) + 'x';
2169
-
2170
- updatePlaybackRate(clampedSpeed);
2171
- });
2172
-
2173
- function updatePlaybackRate(speed) {
2174
- if (!isAudioCombined) return;
2175
-
2176
- currentPlaybackRate = speed;
2177
- video.playbackRate = speed;
2178
-
2179
- // ピッチ保持を再設定
2180
- video.preservesPitch = true;
2181
- video.mozPreservesPitch = true;
2182
- video.webkitPreservesPitch = true;
2183
-
2184
- if (combinedAudioElement) {
2185
- combinedAudioElement.playbackRate = speed;
2186
-
2187
- // 合成音声のピッチ保持を設定
2188
- combinedAudioElement.preservesPitch = true;
2189
- combinedAudioElement.mozPreservesPitch = true;
2190
- combinedAudioElement.webkitPreservesPitch = true;
2191
- }
2192
- }
2193
-
2194
- // 全画面ボタン
2195
- fullscreenBtn.addEventListener('click', function() {
2196
- if (!isFullscreen) {
2197
- if (videoContainer.requestFullscreen) {
2198
- videoContainer.requestFullscreen();
2199
- } else if (videoContainer.webkitRequestFullscreen) {
2200
- videoContainer.webkitRequestFullscreen();
2201
- } else if (videoContainer.msRequestFullscreen) {
2202
- videoContainer.msRequestFullscreen();
2203
- }
2204
- } else {
2205
- if (document.exitFullscreen) {
2206
- document.exitFullscreen();
2207
- } else if (document.webkitExitFullscreen) {
2208
- document.webkitExitFullscreen();
2209
- } else if (document.msExitFullscreen) {
2210
- document.msExitFullscreen();
2211
  }
2212
- }
2213
- });
2214
-
2215
- // 全画面変更イベント
2216
- document.addEventListener('fullscreenchange', handleFullscreenChange);
2217
- document.addEventListener('webkitfullscreenchange', handleFullscreenChange);
2218
- document.addEventListener('msfullscreenchange', handleFullscreenChange);
2219
-
2220
- function handleFullscreenChange() {
2221
- isFullscreen = !!(document.fullscreenElement || document.webkitFullscreenElement || document.msFullscreenElement);
2222
- fullscreenBtn.textContent = isFullscreen ? '⛶' : '⛶';
2223
- video.controls = false;
2224
-
2225
- // 全画面時にロックボタンを表示
2226
- lockControlsBtn.style.display = isFullscreen ? 'flex' : 'none';
2227
-
2228
- // 全画面時にコントロールバー自動非表示機能を有効化
2229
- if (isFullscreen) {
2230
- resetControlsHideTimer();
2231
- document.addEventListener('mousemove', handleFullscreenMouseMove);
2232
  } else {
2233
- document.removeEventListener('mousemove', handleFullscreenMouseMove);
2234
- clearTimeout(controlsHideTimeout);
2235
- showControls();
2236
- }
2237
- }
2238
-
2239
- // 全画面時のマウス移動処���
2240
- function handleFullscreenMouseMove() {
2241
- if (!isControlsLocked) {
2242
- showControls();
2243
- resetControlsHideTimer();
2244
- }
2245
- }
2246
-
2247
- // コントロールバーを表示
2248
- function showControls() {
2249
- if (!controlsVisible) {
2250
- videoControls.style.opacity = '1';
2251
- controlsVisible = true;
2252
- }
2253
- }
2254
-
2255
- // コントロールバーを非表示
2256
- function hideControls() {
2257
- if (!isControlsLocked && controlsVisible) {
2258
- videoControls.style.opacity = '0';
2259
- controlsVisible = false;
2260
- }
2261
- }
2262
-
2263
- // コントロールバー非表示タイマーをリセット
2264
- function resetControlsHideTimer() {
2265
- clearTimeout(controlsHideTimeout);
2266
- if (!isControlsLocked) {
2267
- controlsHideTimeout = setTimeout(hideControls, 1500); // 1.5秒後に非表示
2268
  }
2269
  }
2270
 
2271
- // ロックボタンのクリック処理
2272
- lockControlsBtn.addEventListener('click', function() {
2273
- isControlsLocked = !isControlsLocked;
2274
- this.classList.toggle('locked', isControlsLocked);
2275
-
2276
- if (isControlsLocked) {
2277
- showControls();
2278
- clearTimeout(controlsHideTimeout);
2279
- } else {
2280
- resetControlsHideTimer();
2281
- }
2282
- });
2283
-
2284
- // キーボードイベント (ESCで全画面終了)
2285
- document.addEventListener('keydown', function(e) {
2286
- if (e.key === 'Escape' && isFullscreen) {
2287
- if (document.exitFullscreen) {
2288
- document.exitFullscreen();
2289
- } else if (document.webkitExitFullscreen) {
2290
- document.webkitExitFullscreen();
2291
- } else if (document.msExitFullscreen) {
2292
- document.msExitFullscreen();
2293
- }
2294
- }
2295
- });
2296
-
2297
- // ボリュームスライダーのイベント
2298
- audioSliders.forEach((slider, index) => {
2299
- slider.addEventListener('input', function() {
2300
- const value = parseFloat(this.value);
2301
- volumeValues[index].textContent = value.toFixed(2);
2302
-
2303
- const percent = value * 100;
2304
- this.style.backgroundSize = `${percent}% 100%`;
2305
- });
2306
- });
2307
-
2308
- globalVolumeSlider.addEventListener('input', function() {
2309
- const value = parseFloat(this.value);
2310
- globalVolumeValue.textContent = value.toFixed(1);
2311
-
2312
- const percent = (value - this.min) / (this.max - this.min) * 100;
2313
- this.style.backgroundSize = `${percent}% 100%`;
2314
-
2315
- applyVolume();
2316
- });
2317
-
2318
- // ループ設定変更時
2319
- loopCheckbox.addEventListener('change', function() {
2320
- // 合成音声ではループは動画に依存する
2321
- });
2322
-
2323
- // 現在の秒数を開始時間に設定
2324
- setStartTimeBtn.addEventListener('click', function() {
2325
- startTimeInput.value = video.currentTime.toFixed(2);
2326
- updateProgressMarkers();
2327
- });
2328
-
2329
- // 現在の秒数を終了時間に設定
2330
- setEndTimeBtn.addEventListener('click', function() {
2331
- endTimeInput.value = video.currentTime.toFixed(2);
2332
- updateProgressMarkers();
2333
- });
2334
-
2335
- // 終了時間を動画の長さにリセット
2336
- resetEndTimeBtn.addEventListener('click', function() {
2337
- endTimeInput.value = video.duration.toFixed(2);
2338
- updateProgressMarkers();
2339
- });
2340
-
2341
- // プログレスバーのマーカーを更新
2342
- function updateProgressMarkers() {
2343
- const duration = video.duration || videoDuration;
2344
- const startTime = parseFloat(startTimeInput.value) || 0;
2345
- const endTime = parseFloat(endTimeInput.value) || duration;
2346
-
2347
- if (duration > 0) {
2348
- // 開始時間が0の場合はマーカーを非表示
2349
- if (startTime <= 0) {
2350
- startMarker.style.display = 'none';
2351
- } else {
2352
- startMarker.style.left = `${(startTime / duration) * 100}%`;
2353
- startMarker.style.display = 'block';
2354
- }
2355
 
2356
- // 終了時間が動画の長さと同じ場合はマーカーを非表示
2357
- if (endTime >= duration) {
2358
- endMarker.style.display = 'none';
2359
- } else {
2360
- endMarker.style.left = `${(endTime / duration) * 100}%`;
2361
- endMarker.style.display = 'block';
2362
- }
2363
- }
2364
- }
2365
-
2366
- // 消画モード切り替え
2367
- blackVideoToggle.addEventListener('click', function() {
2368
- isBlackVideoMode = !isBlackVideoMode;
2369
-
2370
- if (isBlackVideoMode) {
2371
- // 消画モードON
2372
- video.style.filter = 'brightness(0)';
2373
- this.style.backgroundColor = '#64ffda';
2374
- this.style.color = '#0a192f';
2375
- } else {
2376
- // 消画モードOFF
2377
- video.style.filter = '';
2378
- this.style.backgroundColor = '';
2379
- this.style.color = '';
2380
- }
2381
- });
2382
-
2383
- // 合成ボタンクリック
2384
- combineButton.addEventListener('click', combineAudio);
2385
-
2386
- // プレビューボタンクリック
2387
- previewButton.addEventListener('click', togglePreview);
2388
-
2389
- // 同期ステータスを閉じる
2390
- syncStatusClose.addEventListener('click', function() {
2391
- syncStatus.style.display = 'none';
2392
- });
2393
-
2394
- // 初期化
2395
- loadAudioFiles();
2396
- updateVolumeIcon();
2397
- volumeSlider.value = video.volume;
2398
- video.controls = false;
2399
-
2400
- // スライダーの背景を初期化
2401
- function initSliderBackgrounds() {
2402
- const sliders = [
2403
- volumeSlider,
2404
- speedSlider,
2405
- globalVolumeSlider,
2406
- playbackSpeedSlider,
2407
- ...audioSliders
2408
- ];
2409
-
2410
- sliders.forEach(slider => {
2411
- if (slider) {
2412
- slider.style.backgroundImage = 'linear-gradient(#64ffda, #64ffda)';
2413
- slider.style.backgroundRepeat = 'no-repeat';
2414
 
2415
- if (slider === globalVolumeSlider) {
2416
- const percent = (slider.value - slider.min) / (slider.max - slider.min) * 100;
2417
- slider.style.backgroundSize = `${percent}% 100%`;
2418
- globalVolumeValue.textContent = slider.value;
2419
- } else {
2420
- slider.style.backgroundSize = `${slider.value * 100}% 100%`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2421
  }
2422
- }
2423
- });
2424
- }
2425
-
2426
- initSliderBackgrounds();
2427
- startSyncCheck(); // 同期チェックを開始
2428
-
2429
- // 初期テンポ設定
2430
- tempoInput.value = isTMode ? 66 : 92;
2431
- tempoInput.dispatchEvent(new Event('input'));
2432
- });
2433
-
2434
- // タイムマーカー関連のコード
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2435
  document.addEventListener('DOMContentLoaded', function() {
2436
  // まず必要なDOM要素を取得
2437
  const startMarker = document.getElementById('start-marker');
@@ -2530,21 +2474,11 @@ document.addEventListener('DOMContentLoaded', function() {
2530
  const endTime = parseFloat(endTimeInput.value) || duration;
2531
 
2532
  if (duration > 0) {
2533
- // 開始時間が0の場合はマーカーを非表示
2534
- if (startTime <= 0) {
2535
- if(startMarker) startMarker.style.display = 'none';
2536
- } else {
2537
- if(startMarker) startMarker.style.left = `${(startTime / duration) * 100}%`;
2538
- if(startMarker) startMarker.style.display = 'block';
2539
- }
2540
-
2541
- // 終了時間が���画の長さと同じ場合はマーカーを非表示
2542
- if (endTime >= duration) {
2543
- if(endMarker) endMarker.style.display = 'none';
2544
- } else {
2545
- if(endMarker) endMarker.style.left = `${(endTime / duration) * 100}%`;
2546
- if(endMarker) endMarker.style.display = 'block';
2547
- }
2548
  }
2549
  }
2550
  });
 
1235
  <!-- 全画面時のロックボタン -->
1236
  <button class="lock-controls-btn" id="lock-controls-btn" title="コントロールバーを固定">🔒</button>
1237
  <script>
1238
+ document.addEventListener('DOMContentLoaded', function() {
1239
+ // 同期管理用の変数
1240
+ let lastSyncTime = 0;
1241
+ let isBuffering = false;
1242
+ let syncDriftLog = [];
1243
+ let syncCheckInterval;
1244
+ let audioContext;
1245
+ let controlsHideTimeout;
1246
+ let isControlsLocked = false;
1247
+ let controlsVisible = true;
1248
+ let isCheckingSync = false;
1249
+ let isInBackgroundTab = false;
1250
+
1251
+ try {
1252
+ audioContext = new (window.AudioContext || window.webkitAudioContext)();
1253
+ } catch (e) {
1254
+ console.error('Web Audio APIがサポートされていません:', e);
1255
+ }
 
 
 
 
 
 
 
 
 
1256
 
1257
+ // テクノロジー風背景を生成
1258
+ function createTechBackground() {
1259
+ const bg = document.getElementById('techBg');
1260
+
1261
+ for (let i = 0; i < 200; i++) {
1262
+ const line = document.createElement('div');
1263
+ line.className = 'circuit-line';
1264
+
1265
+ const isHorizontal = Math.random() > 0.5;
1266
+ if (isHorizontal) {
1267
+ line.style.width = `${Math.random() * 300 + 100}px`;
1268
+ line.style.height = '1px';
1269
+ } else {
1270
+ line.style.width = '1px';
1271
+ line.style.height = `${Math.random() * 300 + 100}px`;
1272
+ }
1273
+
1274
+ line.style.left = `${Math.random() * 100}%`;
1275
+ line.style.top = `${Math.random() * 100}%`;
1276
+ line.style.opacity = Math.random() * 0.5 + 0.1;
1277
+ bg.appendChild(line);
1278
+ }
1279
+
1280
+ for (let i = 0; i < 200; i++) {
1281
+ const dot = document.createElement('div');
1282
+ dot.className = 'grid-dot';
1283
+ dot.style.left = `${Math.random() * 100}%`;
1284
+ dot.style.top = `${Math.random() * 100}%`;
1285
+ bg.appendChild(dot);
1286
+ }
1287
+
1288
+ for (let i = 0; i < 15; i++) {
1289
+ const hex = document.createElement('div');
1290
+ hex.className = 'hexagon';
1291
+ hex.style.left = `${Math.random() * 100}%`;
1292
+ hex.style.top = `${Math.random() * 100}%`;
1293
+ hex.style.transform = `rotate(${Math.random() * 360}deg)`;
1294
+ hex.style.animation = `float ${Math.random() * 10 + 5}s infinite ease-in-out`;
1295
+ bg.appendChild(hex);
1296
+ }
1297
+
1298
+ for (let i = 0; i < 8; i++) {
1299
+ const pulse = document.createElement('div');
1300
+ pulse.className = 'pulse';
1301
+ pulse.style.left = `${Math.random() * 100}%`;
1302
+ pulse.style.top = `${Math.random() * 100}%`;
1303
+ pulse.style.animationDelay = `${Math.random() * 2}s`;
1304
+ bg.appendChild(pulse);
1305
+ }
1306
+ }
1307
+
1308
+ createTechBackground();
1309
+
1310
+ const urlParams = new URLSearchParams(window.location.search);
1311
+ const isTMode = urlParams.has('mode') && urlParams.get('mode') === 't';
1312
+ const basePath = isTMode ? '/t/' : '/';
1313
+
1314
+ // ローディング状態を管理
1315
+ let loadingCount = 0;
1316
+ let totalToLoad = 6; // 動画 + 5つの音声ファイル
1317
+ let lastUpdateTime = 0;
1318
+ const updateInterval = 1;
1319
+
1320
+ function checkLoadingComplete() {
1321
+ loadingCount++;
1322
+ if (loadingCount >= totalToLoad) {
1323
+ setTimeout(function() {
1324
+ const loadingOverlay = document.getElementById('loadingOverlay');
1325
+ loadingOverlay.style.opacity = '0';
1326
+ setTimeout(function() {
1327
+ loadingOverlay.style.display = 'none';
1328
+ }, 1000);
1329
+ }, 500);
1330
+ }
1331
+ }
1332
+
1333
+ function handleError(error, message) {
1334
+ console.error(message, error);
1335
+ window.alert(`${message}\n\nエラー詳細: ${error.message}`);
1336
+ }
1337
+
1338
+ // 要素を取得
1339
+ const video = document.getElementById('video');
1340
+ video.preservesPitch = true;
1341
+ video.mozPreservesPitch = true; // Firefox用
1342
+ video.webkitPreservesPitch = true; // 古いWebKit用
1343
+ const videoContainer = document.getElementById('video-container');
1344
+ const playPauseBtn = document.getElementById('play-pause-btn');
1345
+ const timeDisplay = document.getElementById('time-display');
1346
+ const progressContainer = document.getElementById('progress-container');
1347
+ const progressBar = document.getElementById('progress-bar');
1348
+ const progressTime = document.getElementById('progress-time');
1349
+ const volumeBtn = document.getElementById('volume-btn');
1350
+ const volumeSlider = document.getElementById('volume-slider');
1351
+ const speedSlider = document.getElementById('speed-slider');
1352
+ const speedValue = document.getElementById('speed-value');
1353
+ const playbackSpeedSlider = document.getElementById('playback-speed');
1354
+ const playbackSpeedValue = document.getElementById('playback-speed-value');
1355
+ const fullscreenBtn = document.getElementById('fullscreen-btn');
1356
+ const startTimeInput = document.getElementById('start-time');
1357
+ const endTimeInput = document.getElementById('end-time');
1358
+ const loopCheckbox = document.getElementById('loop');
1359
+ const globalVolumeSlider = document.getElementById('global-volume');
1360
+ const globalVolumeValue = document.getElementById('global-volume-value');
1361
+ const audioSliders = document.querySelectorAll('.audio-slider');
1362
+ const volumeValues = document.querySelectorAll('.volume-value');
1363
+ const setStartTimeBtn = document.getElementById('set-start-time');
1364
+ const setEndTimeBtn = document.getElementById('set-end-time');
1365
+ const resetEndTimeBtn = document.getElementById('reset-end-time');
1366
+ const disabledOverlay = document.getElementById('disabledOverlay');
1367
+ const combineButton = document.getElementById('combine-button');
1368
+ const combineStatus = document.getElementById('combine-status');
1369
+ const previewSection = document.getElementById('preview-section');
1370
+ const previewButton = document.getElementById('preview-button');
1371
+ const previewTime = document.getElementById('preview-time');
1372
+ const bufferingIndicator = document.getElementById('buffering-indicator');
1373
+ const syncStatus = document.getElementById('sync-status');
1374
+ const syncStatusText = document.getElementById('sync-status-text');
1375
+ const syncStatusClose = document.getElementById('sync-status-close');
1376
+ const lockControlsBtn = document.getElementById('lock-controls-btn');
1377
+ const startMarker = document.getElementById('start-marker');
1378
+ const endMarker = document.getElementById('end-marker');
1379
+ const tempoInput = document.getElementById('tempo');
1380
+ const tempoSpeedValue = document.getElementById('tempo-speed-value');
1381
+ const videoControls = document.querySelector('.video-controls');
1382
+ const applyTimeBtn = document.getElementById('apply-time-btn');
1383
+
1384
+ // 音声オブジェクトを作成
1385
+ const audioElements = {};
1386
+ const audioBuffers = {};
1387
+ const audioFiles = ['p', 'a', 't', 's', 'k'];
1388
+ let combinedAudioElement = null;
1389
+ let isAudioCombined = false;
1390
+ let currentVolumes = { p: 0, a: 1, t: 1, s: 1, k: 0 };
1391
+
1392
+ // 初期化
1393
+ let videoDuration = 0;
1394
+ let isPlaying = false;
1395
+ let lastVolume = 1;
1396
+ let currentPlaybackRate = 1;
1397
+ let isFullscreen = false;
1398
+
1399
+ async function enterPiP() {
1400
+ if (!document.pictureInPictureElement && !video.paused) {
1401
+ try {
1402
+ await video.requestPictureInPicture();
1403
+ } catch (err) {
1404
+ console.warn('PiP開始失敗:', err);
1405
+ }
1406
+ }
1407
+ }
1408
+
1409
+ async function exitPiP() {
1410
+ if (document.pictureInPictureElement) {
1411
+ try {
1412
+ await document.exitPictureInPicture();
1413
+ } catch (err) {
1414
+ console.warn('PiP終了失敗:', err);
1415
+ }
1416
+ }
1417
+ }
1418
+
1419
+
1420
+ // 動画のバッファリング状態を監視
1421
+ video.addEventListener('waiting', function() {
1422
+ isBuffering = true;
1423
+ bufferingIndicator.style.display = 'block';
1424
+ if (combinedAudioElement) {
1425
+ combinedAudioElement.pause();
1426
+ }
1427
+ });
1428
+
1429
+ video.addEventListener('playing', function() {
1430
+ isBuffering = false;
1431
+ bufferingIndicator.style.display = 'none';
1432
+ if (combinedAudioElement && isPlaying) {
1433
+ syncAudioWithVideo();
1434
+ }
1435
+ });
1436
+
1437
+ video.addEventListener('suspend', function() {
1438
+ console.log('動画の読み込みが一時停止しました');
1439
+ });
1440
+
1441
+ video.addEventListener('stalled', function() {
1442
+ console.log('動画の読み込みが停滞しました');
1443
+ if (isPlaying) {
1444
+ pauseMedia();
1445
+ }
1446
+ });
1447
+
1448
+ function startSyncCheck() {
1449
+ if (isCheckingSync) return;
1450
+ isCheckingSync = true;
1451
+ if (syncCheckInterval) clearInterval(syncCheckInterval);
1452
+ syncCheckInterval = setInterval(checkSync, 1000);
1453
  }
1454
 
1455
+ function stopSyncCheck() {
1456
+ isCheckingSync = false;
1457
+ if (syncCheckInterval) clearInterval(syncCheckInterval);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1458
  }
1459
+
1460
+ function checkSync() {
1461
+ if (!isAudioCombined || !isPlaying || isBuffering || isInBackgroundTab) return;
1462
+
1463
+ const videoTime = video.currentTime;
1464
+ const audioTime = combinedAudioElement.currentTime;
1465
+ const drift = videoTime - audioTime;
1466
+
1467
+ syncDriftLog.push(drift);
1468
+ if (syncDriftLog.length > 5) syncDriftLog.shift();
1469
+
1470
+ const avgDrift = syncDriftLog.reduce((a, b) => a + b, 0) / syncDriftLog.length;
1471
+ syncStatusText.textContent = `同期ズレ: ${avgDrift.toFixed(3)}秒`;
1472
+
1473
+ if (Math.abs(avgDrift) > 0.1) {
1474
+ console.log(`同期ズレを修正: ${avgDrift.toFixed(3)}秒`);
1475
+ syncAudioWithVideo();
1476
+ }
1477
  }
 
 
1478
 
1479
+ applyTimeBtn.addEventListener('click', function() {
1480
+ // 現在再生中なら一時停止
1481
+ const wasPlaying = isPlaying;
1482
+ if (isPlaying) {
1483
+ pauseMedia();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1484
  }
1485
 
1486
+ // 開始時間と終了時間を取得
1487
+ const startTime = parseFloat(startTimeInput.value) || 0;
1488
+ const endTime = parseFloat(endTimeInput.value) || video.duration;
 
1489
 
1490
+ // 現在位置が開始時間より前なら開始時間に移動
1491
+ if (video.currentTime < startTime) {
1492
+ video.currentTime = startTime;
1493
+ if (combinedAudioElement) {
1494
+ combinedAudioElement.currentTime = startTime;
 
 
 
 
 
 
 
 
 
 
 
1495
  }
1496
  }
1497
+ // 現在位置が終了時間より後なら開始時間に移動
1498
+ else if (video.currentTime > endTime) {
1499
+ video.currentTime = startTime;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1500
  if (combinedAudioElement) {
1501
+ combinedAudioElement.currentTime = startTime;
 
 
 
1502
  }
 
 
 
 
1503
  }
1504
+
1505
+ // 再生中だった場合は再開
1506
+ if (wasPlaying) {
1507
+ playMedia();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1508
  }
1509
+
1510
+ // マーカーを更新
1511
+ updateProgressMarkers();
1512
+ });
1513
 
1514
+ // 音声を動画に同期させる関数
1515
+ function syncAudioWithVideo() {
1516
+ if (!isAudioCombined || !isPlaying) return;
1517
+
1518
+ const currentTime = video.currentTime;
1519
+
1520
+ if (combinedAudioElement) {
1521
+ combinedAudioElement.currentTime = currentTime;
1522
+ if (isPlaying) {
1523
+ combinedAudioElement.play().catch(e => console.error('音声再生エラー:', e));
1524
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1525
  }
1526
+
1527
+ // ズレ記録をリセット
1528
+ syncDriftLog = [];
1529
+ lastSyncTime = performance.now();
1530
  }
1531
+
1532
+ // 音声��ァイルをロード
1533
+ function loadAudioFiles() {
1534
+ audioFiles.forEach(file => {
1535
+ try {
1536
+ const audio = new Audio(`${basePath}${file}.mp3`);
1537
+ audio.preload = 'auto';
1538
+ audio.loop = false;
1539
+ audioElements[file] = audio;
1540
+
1541
+ audio.addEventListener('loadedmetadata', function() {
1542
+ console.log(`${basePath}${file}.mp3 loaded`);
1543
+ checkLoadingComplete();
1544
+ });
1545
+
1546
+ audio.addEventListener('error', function() {
1547
+ console.error(`音声ファイル読み込みエラー (${basePath}${file}.mp3):`, audio.error);
1548
+ checkLoadingComplete();
1549
+ });
1550
+ } catch (error) {
1551
+ console.error(`音声ファイル初期化エラー (${basePath}${file}.mp3):`, error);
1552
+ checkLoadingComplete();
1553
+ }
1554
+ });
1555
  }
1556
+
1557
+ // 音声を結合する関数
1558
+ async function combineAudio() {
1559
+ combineButton.disabled = true;
1560
+ combineStatus.textContent = "音声を合成中...";
1561
+
1562
+ try {
1563
+ // 現在の音量設定を保存
1564
+ audioFiles.forEach(file => {
1565
+ currentVolumes[file] = parseFloat(document.querySelector(`.audio-slider[data-audio="${file}"]`).value);
1566
+ });
1567
+
1568
+ // 各音声ファイルをデコード
1569
+ const audioBufferPromises = audioFiles.map(async file => {
1570
+ const audio = audioElements[file];
1571
+ if (!audio) return null;
1572
+
1573
+ // 音量が0の場合はスキップ
1574
+ if (currentVolumes[file] === 0) return null;
1575
+
1576
+ const response = await fetch(`${basePath}${file}.mp3`);
1577
+ const arrayBuffer = await response.arrayBuffer();
1578
+ return await audioContext.decodeAudioData(arrayBuffer);
1579
+ });
1580
+
1581
+ // すべての音声バッファを取得
1582
+ const buffers = await Promise.all(audioBufferPromises);
1583
+ audioFiles.forEach((file, index) => {
1584
+ audioBuffers[file] = buffers[index];
1585
+ });
1586
+
1587
+ // 最長の音声バッファの長さを取得
1588
+ const maxDuration = Math.max(...buffers.filter(b => b).map(b => b.duration));
1589
+
1590
+ // 新しい音声バッファを作成
1591
+ const combinedAudioBuffer = audioContext.createBuffer(
1592
+ 2, // ステレオ
1593
+ audioContext.sampleRate * maxDuration,
1594
+ audioContext.sampleRate
1595
+ );
1596
+
1597
+ // 各音声バッファを結合
1598
+ for (let file of audioFiles) {
1599
+ if (!audioBuffers[file] || currentVolumes[file] === 0) continue;
1600
+
1601
+ const buffer = audioBuffers[file];
1602
+ const volume = currentVolumes[file];
1603
+
1604
+ // 各チャンネルに音声を加算
1605
+ for (let channel = 0; channel < 2; channel++) {
1606
+ const inputData = buffer.getChannelData(channel % buffer.numberOfChannels);
1607
+ const outputData = combinedAudioBuffer.getChannelData(channel);
1608
+
1609
+ for (let i = 0; i < inputData.length; i++) {
1610
+ outputData[i] += inputData[i] * volume;
1611
+ }
1612
+ }
1613
+ }
1614
+
1615
+ // 音量を正規化 (クリッピング防止)
1616
+ for (let channel = 0; channel < 2; channel++) {
1617
+ const outputData = combinedAudioBuffer.getChannelData(channel);
1618
+ let max = 0;
1619
+
1620
+ for (let i = 0; i < outputData.length; i++) {
1621
+ if (Math.abs(outputData[i]) > max) {
1622
+ max = Math.abs(outputData[i]);
1623
+ }
1624
+ }
1625
+
1626
+ if (max > 1) {
1627
+ for (let i = 0; i < outputData.length; i++) {
1628
+ outputData[i] /= max;
1629
+ }
1630
+ }
1631
+ }
1632
+
1633
+ // AudioBufferをBlobに変換
1634
+ const blob = bufferToWave(combinedAudioBuffer);
1635
+ const url = URL.createObjectURL(blob);
1636
+
1637
+ // 新しいaudio要素を作成
1638
+ combinedAudioElement = new Audio(url);
1639
+ combinedAudioElement.preservesPitch = true;
1640
+ combinedAudioElement.mozPreservesPitch = true;
1641
+ combinedAudioElement.webkitPreservesPitch = true;
1642
+ combinedAudioElement.playbackRate = currentPlaybackRate;
1643
+
1644
+ isAudioCombined = true;
1645
+ combineStatus.textContent = "音声の合成が完了しました";
1646
+ enablePlayerControls();
1647
+
1648
+ combineButton.disabled = false;
1649
+
1650
  document.addEventListener('visibilitychange', async () => {
1651
  if (document.hidden) {
1652
  // タブが非表示になった場合
 
1702
  }
1703
  });
1704
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1705
 
1706
+ // 動画終了時に自動的にPiPを閉じる(次回再開のため)
1707
+ video.addEventListener('ended', exitPiP);
1708
+ // 合成後に音量と再生速度を適用
1709
+ applyVolume();
1710
+ applyPlaybackRate();
1711
+
1712
+ } catch (error) {
1713
+ console.error('音声合成エラー:', error);
1714
+ combineStatus.textContent = "音声の合成に失敗しました";
1715
+ combineButton.disabled = false;
1716
+ }
1717
+ }
1718
+
1719
+ function bufferToWave(abuffer) {
1720
+ const numOfChan = abuffer.numberOfChannels,
1721
+ length = abuffer.length * numOfChan * 2 + 44,
1722
+ buffer = new ArrayBuffer(length),
1723
+ view = new DataView(buffer),
1724
+ channels = [],
1725
+ sampleRate = abuffer.sampleRate;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1726
 
1727
+ // posをletで宣言(constから変更)
1728
+ let pos = 0;
1729
+
1730
+ // write WAV header
1731
+ setUint32(0x46464952); // "RIFF"
1732
+ setUint32(length - 8); // file length - 8
1733
+ setUint32(0x45564157); // "WAVE"
1734
+
1735
+ setUint32(0x20746d66); // "fmt " chunk
1736
+ setUint32(16); // length = 16
1737
+ setUint16(1); // PCM (uncompressed)
1738
+ setUint16(numOfChan);
1739
+ setUint32(sampleRate);
1740
+ setUint32(sampleRate * 2 * numOfChan);
1741
+ setUint16(numOfChan * 2);
1742
+ setUint16(16);
1743
+
1744
+ setUint32(0x61746164); // "data" - chunk
1745
+ setUint32(length - pos - 4);
1746
+
1747
+ // write interleaved data
1748
+ for (let i = 0; i < abuffer.length; i++) {
1749
+ for (let channel = 0; channel < numOfChan; channel++) {
1750
+ let sample = abuffer.getChannelData(channel)[i] * 0x7fff;
1751
+ if (sample < -32768) sample = -32768;
1752
+ if (sample > 32767) sample = 32767;
1753
+ view.setInt16(pos, sample, true);
1754
+ pos += 2;
1755
  }
1756
+ }
1757
+
1758
+ function setUint16(data) {
1759
+ view.setUint16(pos, data, true);
1760
+ pos += 2;
1761
+ }
1762
+
1763
+ function setUint32(data) {
1764
+ view.setUint32(pos, data, true);
1765
+ pos += 4;
1766
+ }
1767
+
1768
+ return new Blob([buffer], { type: 'audio/wav' });
1769
+ }
1770
+
1771
+ function applyVolume() {
1772
+ if (!isAudioCombined) return;
1773
 
1774
+ // ベース音量 (0-1)
1775
+ const baseVolume = parseFloat(volumeSlider.value);
1776
+ // グローバル音量係数 (0-10)
1777
+ const globalVolume = parseFloat(globalVolumeSlider.value) / 10; // 0-1に変換
1778
+
1779
+ // 最終音量 (0-1)
1780
+ const finalVolume = Math.max(0, Math.min(1, baseVolume * globalVolume));
1781
+
1782
+ // 動画と音声の音量を設定
1783
+ video.volume = finalVolume;
1784
+ if (combinedAudioElement) {
1785
+ combinedAudioElement.volume = finalVolume;
1786
+ }
1787
+
1788
+ // 音量アイコンを更新
1789
+ updateVolumeIcon();
1790
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1791
 
1792
+ function applyPlaybackRate() {
1793
+ if (!isAudioCombined) return;
1794
+
1795
+ const speed = parseFloat(playbackSpeedSlider.value);
1796
+ currentPlaybackRate = speed;
1797
+ video.playbackRate = speed;
1798
+
1799
+ if (combinedAudioElement) {
1800
+ combinedAudioElement.playbackRate = speed;
1801
+ }
1802
+
1803
+ speedValue.textContent = speed.toFixed(2) + 'x';
1804
+ playbackSpeedValue.textContent = speed.toFixed(2) + 'x';
1805
+ speedSlider.value = speed;
1806
+ }
1807
 
1808
+ // プレイヤーコントロールを有効化
1809
+ function enablePlayerControls() {
1810
+ disabledOverlay.style.display = 'none';
1811
+ playPauseBtn.disabled = false;
1812
+ volumeBtn.disabled = false;
1813
+ volumeSlider.disabled = false;
1814
+ speedSlider.disabled = false;
1815
+ fullscreenBtn.disabled = false;
1816
+ startTimeInput.disabled = false;
1817
+ endTimeInput.disabled = false;
1818
+ resetEndTimeBtn.disabled = false;
1819
+ loopCheckbox.disabled = false;
1820
+ globalVolumeSlider.disabled = false;
1821
+ setStartTimeBtn.disabled = false;
1822
+ setEndTimeBtn.disabled = false;
1823
+ playbackSpeedSlider.disabled = false;
1824
+ applyTimeBtn.disabled = false;
1825
+ }
1826
 
1827
+ // プレビュー再生
1828
+ function togglePreview() {
1829
+ if (!isAudioCombined || !combinedAudioElement) return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1830
 
1831
+ if (previewButton.textContent === '▶') {
1832
+ // 再生
1833
+ combinedAudioElement.currentTime = 0;
1834
+ combinedAudioElement.play()
1835
+ .then(() => {
1836
+ previewButton.textContent = '⏸';
1837
+
1838
+ // プレビューの時間表示を更新
1839
+ const updatePreviewTime = () => {
1840
+ if (!combinedAudioElement || !isAudioCombined) return;
1841
+
1842
+ const currentTime = combinedAudioElement.currentTime;
1843
+ const duration = combinedAudioElement.duration;
1844
+
1845
+ if (currentTime >= duration) {
1846
+ previewButton.textContent = '▶';
1847
+ previewTime.textContent = `00:00 / ${formatTime(duration)}`;
1848
+ return;
1849
+ }
1850
+
1851
+ previewTime.textContent = `${formatTime(currentTime)} / ${formatTime(duration)}`;
1852
+ requestAnimationFrame(updatePreviewTime);
1853
+ };
1854
+
1855
+ updatePreviewTime();
1856
+ })
1857
+ .catch(e => console.error('プレビュー再生エラー:', e));
1858
+ } else {
1859
+ // 一時停止
1860
+ combinedAudioElement.pause();
1861
+ previewButton.textContent = '▶';
1862
+ }
1863
+ }
1864
 
1865
+ // 時間をフォーマットするヘルパー関数
1866
+ function formatTime(seconds) {
1867
+ const mins = Math.floor(seconds / 60);
1868
+ const secs = Math.floor(seconds % 60);
1869
+ return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
1870
+ }
1871
 
1872
+ // 動画のメタデータが読み込まれたら
1873
+ video.addEventListener('loadedmetadata', function() {
1874
+ try {
1875
+ videoDuration = video.duration;
1876
+ endTimeInput.value = videoDuration.toFixed(2);
1877
+ endTimeInput.max = videoDuration;
1878
+ startTimeInput.max = videoDuration - 0.1;
1879
+ updateTimeDisplay();
1880
+ checkLoadingComplete();
1881
+ } catch (error) {
1882
+ handleError(error, '動画メタデータ読み込み中にエラーが発生しました');
1883
+ }
1884
+ });
1885
+
1886
+ // 動画エラー処理
1887
+ video.addEventListener('error', function() {
1888
+ handleError(video.error, '動画読み込み中にエラーが発生しました');
1889
+ });
1890
+
1891
+ // 再生ボタンクリック
1892
+ playPauseBtn.addEventListener('click', function() {
1893
+ const endTime = parseFloat(endTimeInput.value) || videoDuration;
1894
+ if (video.currentTime >= endTime) {
1895
+ const startTime = parseFloat(startTimeInput.value) || 0;
1896
+ seekMedia(startTime);
1897
+ }
1898
+ togglePlayPause();
1899
+ });
1900
+
1901
+ // 時間表示を更新
1902
+ function updateTimeDisplay() {
1903
+ const now = performance.now();
1904
+ if (now - lastUpdateTime < updateInterval && !isFullscreen) return;
1905
+ lastUpdateTime = now;
1906
+
1907
+ try {
1908
+ const currentTime = video.currentTime;
1909
+ const duration = video.duration || videoDuration;
1910
+
1911
+ const currentMinutes = Math.floor(currentTime / 60);
1912
+ const currentSeconds = Math.floor(currentTime % 60);
1913
+ const currentMilliseconds = Math.floor((currentTime % 1) * 100);
1914
+ const durationMinutes = Math.floor(duration / 60);
1915
+ const durationSeconds = Math.floor(duration % 60);
1916
+ const durationMilliseconds = Math.floor((duration % 1) * 100);
1917
+
1918
+ timeDisplay.textContent =
1919
+ `${String(currentMinutes).padStart(2, '0')}:${String(currentSeconds).padStart(2, '0')}.${String(currentMilliseconds).padStart(2, '0')} / ` +
1920
+ `${String(durationMinutes).padStart(2, '0')}:${String(durationSeconds).padStart(2, '0')}.${String(durationMilliseconds).padStart(2, '0')}`;
1921
+
1922
+ const progressPercent = (currentTime / duration) * 100;
1923
+ progressBar.style.width = `${progressPercent}%`;
1924
+ } catch (error) {
1925
+ console.error('時間表示更新エラー:', error);
1926
+ }
1927
+ }
1928
+
1929
+ // 再生/一時停止をトグル
1930
+ function togglePlayPause() {
1931
+ if (isPlaying) {
1932
+ pauseMedia();
1933
+ } else {
1934
+ playMedia();
1935
+ }
1936
+ }
1937
+
1938
+ function playMedia() {
1939
+ try {
1940
+ const duration = video.duration || videoDuration;
1941
+ const startTime = parseFloat(startTimeInput.value) || 0;
1942
+ const endTime = parseFloat(endTimeInput.value) || duration;
1943
 
1944
+ if (video.currentTime >= endTime) {
1945
+ video.currentTime = startTime;
 
 
1946
  }
1947
 
1948
+ const playPromise = video.play();
1949
 
1950
+ if (playPromise !== undefined) {
1951
+ playPromise.then(() => {
1952
+ isPlaying = true;
1953
+ playPauseBtn.textContent = '⏸';
1954
+
1955
+ // 音声を同期して再生
1956
+ if (isAudioCombined) {
1957
+ combinedAudioElement.currentTime = video.currentTime;
1958
+ combinedAudioElement.play().catch(e => console.error('音声再生エラー:', e));
1959
+ }
1960
+
1961
+ startSyncCheck();
1962
+
1963
+ video.playbackRate = currentPlaybackRate;
1964
+ }).catch(error => {
1965
+ console.error('動画再生エラー:', error);
1966
+ isPlaying = false;
1967
+ playPauseBtn.textContent = '���';
1968
+ });
1969
+ }
1970
+ } catch (error) {
1971
+ console.error('メディア再生エラー:', error);
1972
  isPlaying = false;
1973
  playPauseBtn.textContent = '▶';
1974
+ }
1975
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
1976
 
1977
+ function pauseMedia() {
1978
+ try {
1979
+ video.pause();
1980
+ isPlaying = false;
1981
+ playPauseBtn.textContent = '';
1982
+ stopSyncCheck();
1983
+
1984
+ if (combinedAudioElement) {
1985
+ combinedAudioElement.pause();
1986
+ }
1987
+ } catch (error) {
1988
+ console.error('メディア一時停止エラー:', error);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1989
  }
1990
  }
1991
+
1992
+ // 時間更新時の処理
1993
+ video.addEventListener('timeupdate', function() {
1994
+ const duration = video.duration || videoDuration;
1995
+ const endTime = parseFloat(endTimeInput.value) || duration;
 
 
 
 
 
 
 
 
 
 
1996
 
1997
+ if (video.currentTime >= endTime && endTime > 0) {
1998
+ if (loopCheckbox.checked) {
 
 
1999
  const startTime = parseFloat(startTimeInput.value) || 0;
2000
+ video.currentTime = startTime;
 
 
 
 
2001
  if (combinedAudioElement) {
2002
+ combinedAudioElement.currentTime = startTime;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2003
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2004
  } else {
2005
+ pauseMedia();
2006
+ video.currentTime = endTime;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2007
  }
2008
  }
2009
 
2010
+ updateTimeDisplay();
2011
+ });
2012
+
2013
+ // プログレスバークリックでシーク
2014
+ progressContainer.addEventListener('click', function(e) {
2015
+ if (!video.duration) return;
2016
+
2017
+ const rect = this.getBoundingClientRect();
2018
+ const pos = (e.clientX - rect.left) / rect.width;
2019
+ const seekTime = pos * video.duration;
2020
+
2021
+ seekMedia(seekTime);
2022
+ });
2023
+
2024
+ // 指定した時間にシーク (改良版)
2025
+ function seekMedia(time) {
2026
+ try {
2027
+ const duration = video.duration || videoDuration;
2028
+ const startTime = parseFloat(startTimeInput.value) || 0;
2029
+ const endTime = parseFloat(endTimeInput.value) || duration;
2030
+
2031
+ const seekTime = Math.max(startTime, Math.min(time, endTime));
2032
+ video.currentTime = seekTime;
2033
+
2034
+ if (combinedAudioElement) {
2035
+ combinedAudioElement.currentTime = seekTime;
2036
+ if (isPlaying) {
2037
+ combinedAudioElement.play().catch(e => console.error('音声再生エラー:', e));
2038
+ }
2039
+ }
2040
+ } catch (error) {
2041
+ console.error('メディアシークエラー:', error);
2042
+ }
2043
+ }
2044
+
2045
+ // プログレスバー上でマウス移動時に時間を表示
2046
+ progressContainer.addEventListener('mousemove', function(e) {
2047
+ if (!video.duration) return;
2048
+
2049
+ const rect = this.getBoundingClientRect();
2050
+ const pos = (e.clientX - rect.left) / rect.width;
2051
+ const hoverTime = pos * video.duration;
2052
+
2053
+ const minutes = Math.floor(hoverTime / 60);
2054
+ const seconds = Math.floor(hoverTime % 60);
2055
+ const milliseconds = Math.floor((hoverTime % 1) * 100);
2056
+
2057
+ progressTime.textContent = `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}.${String(milliseconds).padStart(2, '0')}`;
2058
+ progressTime.style.display = 'block';
2059
+ progressTime.style.left = `${pos * 100}%`;
2060
+ });
2061
+
2062
+ progressContainer.addEventListener('mouseleave', function() {
2063
+ progressTime.style.display = 'none';
2064
+ });
2065
+
2066
+ // 動画クリックで再生/一時停止
2067
+ video.addEventListener('click', function() {
2068
+ togglePlayPause();
2069
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2070
 
2071
+ volumeSlider.addEventListener('input', function() {
2072
+ if (!isAudioCombined) return;
2073
+
2074
+ lastVolume = parseFloat(this.value);
2075
+ applyVolume();
2076
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2077
 
2078
+ volumeBtn.addEventListener('click', function() {
2079
+ if (!isAudioCombined) return;
2080
+
2081
+ if (video.volume > 0) {
2082
+ lastVolume = parseFloat(volumeSlider.value);
2083
+ volumeSlider.value = 0;
2084
+ } else {
2085
+ volumeSlider.value = lastVolume;
2086
+ }
2087
+
2088
+ applyVolume();
2089
+ });
2090
+
2091
+ // 音量アイコンを更新
2092
+ function updateVolumeIcon() {
2093
+ if (video.volume === 0) {
2094
+ volumeBtn.textContent = '🔇';
2095
+ } else if (video.volume < 3) {
2096
+ volumeBtn.textContent = '🔈';
2097
+ } else {
2098
+ volumeBtn.textContent = '🔊';
2099
+ }
2100
  }
2101
+
2102
+ // 再生速度スライダー (動画プレイヤー)
2103
+ speedSlider.addEventListener('input', function() {
2104
+ if (!isAudioCombined) return;
2105
+
2106
+ const speed = parseFloat(this.value);
2107
+ speedValue.textContent = speed.toFixed(2) + 'x';
2108
+ playbackSpeedSlider.value = speed;
2109
+ playbackSpeedValue.textContent = speed.toFixed(2) + 'x';
2110
+ updatePlaybackRate(speed);
2111
+ });
2112
+
2113
+ // 再生速度スライダー (設定メニュー)
2114
+ playbackSpeedSlider.addEventListener('input', function() {
2115
+ if (!isAudioCombined) return;
2116
+
2117
+ const speed = parseFloat(this.value);
2118
+ playbackSpeedValue.textContent = speed.toFixed(2) + 'x';
2119
+ speedSlider.value = speed;
2120
+ speedValue.textContent = speed.toFixed(2) + 'x';
2121
+ updatePlaybackRate(speed);
2122
+ });
2123
+
2124
+ // テンポ入力による再生速度更新
2125
+ tempoInput.addEventListener('input', function() {
2126
+ const tempo = parseFloat(this.value);
2127
+ const baseTempo = isTMode ? 66 : 92;
2128
+ const speed = tempo / baseTempo;
2129
+
2130
+ const clampedSpeed = Math.max(0.001, Math.min(5.0, speed));
2131
+
2132
+ playbackSpeedSlider.value = clampedSpeed;
2133
+ playbackSpeedValue.textContent = clampedSpeed.toFixed(2) + 'x';
2134
+ speedSlider.value = clampedSpeed;
2135
+ speedValue.textContent = clampedSpeed.toFixed(2) + 'x';
2136
+ tempoSpeedValue.textContent = clampedSpeed.toFixed(2) + 'x';
2137
+
2138
+ updatePlaybackRate(clampedSpeed);
2139
+ });
2140
+
2141
+ function updatePlaybackRate(speed) {
2142
+ if (!isAudioCombined) return;
2143
+
2144
+ currentPlaybackRate = speed;
2145
+ video.playbackRate = speed;
2146
+
2147
+ // ピッチ保持を再設定
2148
+ video.preservesPitch = true;
2149
+ video.mozPreservesPitch = true;
2150
+ video.webkitPreservesPitch = true;
2151
+
2152
+ if (combinedAudioElement) {
2153
+ combinedAudioElement.playbackRate = speed;
2154
+
2155
+ // 合成音声のピッチ保持を設定
2156
+ combinedAudioElement.preservesPitch = true;
2157
+ combinedAudioElement.mozPreservesPitch = true;
2158
+ combinedAudioElement.webkitPreservesPitch = true;
2159
+ }
2160
+ }
2161
+
2162
+ // 全画面ボタン
2163
+ fullscreenBtn.addEventListener('click', function() {
2164
+ if (!isFullscreen) {
2165
+ if (videoContainer.requestFullscreen) {
2166
+ videoContainer.requestFullscreen();
2167
+ } else if (videoContainer.webkitRequestFullscreen) {
2168
+ videoContainer.webkitRequestFullscreen();
2169
+ } else if (videoContainer.msRequestFullscreen) {
2170
+ videoContainer.msRequestFullscreen();
2171
+ }
2172
+ } else {
2173
+ if (document.exitFullscreen) {
2174
+ document.exitFullscreen();
2175
+ } else if (document.webkitExitFullscreen) {
2176
+ document.webkitExitFullscreen();
2177
+ } else if (document.msExitFullscreen) {
2178
+ document.msExitFullscreen();
2179
+ }
2180
+ }
2181
+ });
2182
+
2183
+ // 全画面変更イベント
2184
+ document.addEventListener('fullscreenchange', handleFullscreenChange);
2185
+ document.addEventListener('webkitfullscreenchange', handleFullscreenChange);
2186
+ document.addEventListener('msfullscreenchange', handleFullscreenChange);
2187
+
2188
+ function handleFullscreenChange() {
2189
+ isFullscreen = !!(document.fullscreenElement || document.webkitFullscreenElement || document.msFullscreenElement);
2190
+ fullscreenBtn.textContent = isFullscreen ? '⛶' : '⛶';
2191
+ video.controls = false;
2192
+
2193
+ // 全画面時にロックボタンを表示
2194
+ lockControlsBtn.style.display = isFullscreen ? 'flex' : 'none';
2195
+
2196
+ // 全画面時にコントロールバー自動非表示機能を有効化
2197
+ if (isFullscreen) {
2198
+ resetControlsHideTimer();
2199
+ document.addEventListener('mousemove', handleFullscreenMouseMove);
2200
+ } else {
2201
+ document.removeEventListener('mousemove', handleFullscreenMouseMove);
2202
+ clearTimeout(controlsHideTimeout);
2203
+ showControls();
2204
+ }
2205
+ }
2206
+
2207
+ // 全画面時のマウス移動処理
2208
+ function handleFullscreenMouseMove() {
2209
+ if (!isControlsLocked) {
2210
+ showControls();
2211
+ resetControlsHideTimer();
2212
+ }
2213
+ }
2214
+
2215
+ // コントロールバーを表示
2216
+ function showControls() {
2217
+ if (!controlsVisible) {
2218
+ videoControls.style.opacity = '1';
2219
+ controlsVisible = true;
2220
+ }
2221
+ }
2222
+
2223
+ // コントロールバーを非表示
2224
+ function hideControls() {
2225
+ if (!isControlsLocked && controlsVisible) {
2226
+ videoControls.style.opacity = '0';
2227
+ controlsVisible = false;
2228
+ }
2229
+ }
2230
+
2231
+ // コントロールバー非表示タイマーをリセット
2232
+ function resetControlsHideTimer() {
2233
+ clearTimeout(controlsHideTimeout);
2234
+ if (!isControlsLocked) {
2235
+ controlsHideTimeout = setTimeout(hideControls, 1500); // 1.5秒後に非表示
2236
+ }
2237
+ }
2238
+
2239
+ // ロックボタンのクリック処理
2240
+ lockControlsBtn.addEventListener('click', function() {
2241
+ isControlsLocked = !isControlsLocked;
2242
+ this.classList.toggle('locked', isControlsLocked);
2243
+
2244
+ if (isControlsLocked) {
2245
+ showControls();
2246
+ clearTimeout(controlsHideTimeout);
2247
+ } else {
2248
+ resetControlsHideTimer();
2249
+ }
2250
+ });
2251
+
2252
+ // キーボードイベント (ESCで全画面終了)
2253
+ document.addEventListener('keydown', function(e) {
2254
+ if (e.key === 'Escape' && isFullscreen) {
2255
+ if (document.exitFullscreen) {
2256
+ document.exitFullscreen();
2257
+ } else if (document.webkitExitFullscreen) {
2258
+ document.webkitExitFullscreen();
2259
+ } else if (document.msExitFullscreen) {
2260
+ document.msExitFullscreen();
2261
+ }
2262
+ }
2263
+ });
2264
+
2265
+ // ボリュームスライダーのイベント
2266
+ audioSliders.forEach((slider, index) => {
2267
+ slider.addEventListener('input', function() {
2268
+ const value = parseFloat(this.value);
2269
+ volumeValues[index].textContent = value.toFixed(2);
2270
+
2271
+ const percent = value * 100;
2272
+ this.style.backgroundSize = `${percent}% 100%`;
2273
+ });
2274
+ });
2275
+
2276
+ globalVolumeSlider.addEventListener('input', function() {
2277
+ const value = parseFloat(this.value);
2278
+ globalVolumeValue.textContent = value.toFixed(1);
2279
+
2280
+ const percent = (value - this.min) / (this.max - this.min) * 100;
2281
+ this.style.backgroundSize = `${percent}% 100%`;
2282
+
2283
+ applyVolume();
2284
+ });
2285
+
2286
+ // ループ設定変更時
2287
+ loopCheckbox.addEventListener('change', function() {
2288
+ // 合成音声ではループは動画に依存する
2289
+ });
2290
+
2291
+ // 現在の秒数を開始時間に設定
2292
+ setStartTimeBtn.addEventListener('click', function() {
2293
+ startTimeInput.value = video.currentTime.toFixed(2);
2294
+ updateProgressMarkers();
2295
+ });
2296
+
2297
+ // 現在の秒数を終了時間に設定
2298
+ setEndTimeBtn.addEventListener('click', function() {
2299
+ endTimeInput.value = video.currentTime.toFixed(2);
2300
+ updateProgressMarkers();
2301
+ });
2302
+
2303
+ // 終了時間を動画の長さにリセット
2304
+ resetEndTimeBtn.addEventListener('click', function() {
2305
+ endTimeInput.value = video.duration.toFixed(2);
2306
+ updateProgressMarkers();
2307
+ });
2308
+
2309
+ // プログレスバーのマーカーを更新
2310
+ function updateProgressMarkers() {
2311
+ const duration = video.duration || videoDuration;
2312
+ const startTime = parseFloat(startTimeInput.value) || 0;
2313
+ const endTime = parseFloat(endTimeInput.value) || duration;
2314
+
2315
+ if (duration > 0) {
2316
+ startMarker.style.left = `${(startTime / duration) * 100}%`;
2317
+ endMarker.style.left = `${(endTime / duration) * 100}%`;
2318
+
2319
+ startMarker.style.display = 'block';
2320
+ endMarker.style.display = 'block';
2321
+ }
2322
+ }
2323
+
2324
+ // 開始/終了時間変更時にマーカーを更新
2325
+ startTimeInput.addEventListener('input', updateProgressMarkers);
2326
+ endTimeInput.addEventListener('input', updateProgressMarkers);
2327
+
2328
+ // 合成ボタンクリック
2329
+ combineButton.addEventListener('click', combineAudio);
2330
+
2331
+ // プレビューボタンクリック
2332
+ previewButton.addEventListener('click', togglePreview);
2333
+
2334
+ // 同期ステータスを閉じる
2335
+ syncStatusClose.addEventListener('click', function() {
2336
+ syncStatus.style.display = 'none';
2337
+ });
2338
+
2339
+ // 初期化
2340
+ loadAudioFiles();
2341
+ updateVolumeIcon();
2342
+ volumeSlider.value = video.volume;
2343
+ video.controls = false;
2344
+
2345
+ // スライダーの背景を初期化
2346
+ function initSliderBackgrounds() {
2347
+ const sliders = [
2348
+ volumeSlider,
2349
+ speedSlider,
2350
+ globalVolumeSlider,
2351
+ playbackSpeedSlider,
2352
+ ...audioSliders
2353
+ ];
2354
+
2355
+ sliders.forEach(slider => {
2356
+ if (slider) {
2357
+ slider.style.backgroundImage = 'linear-gradient(#64ffda, #64ffda)';
2358
+ slider.style.backgroundRepeat = 'no-repeat';
2359
+
2360
+ if (slider === globalVolumeSlider) {
2361
+ const percent = (slider.value - slider.min) / (slider.max - slider.min) * 100;
2362
+ slider.style.backgroundSize = `${percent}% 100%`;
2363
+ globalVolumeValue.textContent = slider.value;
2364
+ } else {
2365
+ slider.style.backgroundSize = `${slider.value * 100}% 100%`;
2366
+ }
2367
+ }
2368
+ });
2369
+ }
2370
+
2371
+ initSliderBackgrounds();
2372
+ startSyncCheck(); // 同期チェックを開始
2373
+
2374
+ // 初期テンポ設定
2375
+ tempoInput.value = isTMode ? 66 : 92;
2376
+ tempoInput.dispatchEvent(new Event('input'));
2377
+ });
2378
+ // タイムマーカー関連のコードを修正
2379
  document.addEventListener('DOMContentLoaded', function() {
2380
  // まず必要なDOM要素を取得
2381
  const startMarker = document.getElementById('start-marker');
 
2474
  const endTime = parseFloat(endTimeInput.value) || duration;
2475
 
2476
  if (duration > 0) {
2477
+ if(startMarker) startMarker.style.left = `${(startTime / duration) * 100}%`;
2478
+ if(endMarker) endMarker.style.left = `${(endTime / duration) * 100}%`;
2479
+
2480
+ if(startMarker) startMarker.style.display = 'block';
2481
+ if(endMarker) endMarker.style.display = 'block';
 
 
 
 
 
 
 
 
 
 
2482
  }
2483
  }
2484
  });