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>
|