Update index.html
Browse files- 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"
|
721 |
<!-- 同期ステータス -->
|
722 |
-
<div class="sync-status" id="sync-status"
|
|
|
|
|
|
|
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 |
-
|
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 |
-
|
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 |
-
|
1196 |
-
|
1197 |
-
|
1198 |
-
|
1199 |
-
|
1200 |
-
|
1201 |
-
|
1202 |
-
|
1203 |
-
|
1204 |
-
|
1205 |
-
|
1206 |
-
|
1207 |
-
|
1208 |
-
|
1209 |
-
|
1210 |
-
|
1211 |
-
|
1212 |
-
|
1213 |
-
|
1214 |
-
|
1215 |
-
|
1216 |
-
|
1217 |
-
|
1218 |
-
|
1219 |
-
|
1220 |
-
|
1221 |
-
|
1222 |
-
|
1223 |
-
|
1224 |
-
|
1225 |
-
|
1226 |
-
|
1227 |
-
|
1228 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
1229 |
pos += 2;
|
1230 |
}
|
1231 |
-
}
|
1232 |
|
1233 |
-
|
1234 |
-
|
1235 |
-
|
1236 |
-
|
1237 |
|
1238 |
-
|
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 |
-
|
1589 |
-
|
1590 |
-
|
1591 |
-
|
1592 |
-
|
1593 |
-
|
1594 |
-
|
1595 |
-
|
1596 |
-
|
1597 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1598 |
|
1599 |
-
|
1600 |
-
|
|
|
|
|
|
|
1601 |
|
1602 |
-
//
|
1603 |
-
|
1604 |
-
|
1605 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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>
|