質問編集履歴
1
ご回答を参考に、アプリを修正しました。以前のせていたHTMLのテキストを見ることができるリンクを追加しました。
test
CHANGED
File without changes
|
test
CHANGED
@@ -16,36 +16,15 @@
|
|
16
16
|
t.mp3 → テノール音源
|
17
17
|
|
18
18
|
### 該当のソースコード
|
19
|
+
かなり省略しています。
|
19
20
|
|
20
21
|
```HTML
|
21
|
-
<!DOCTYPE html>
|
22
|
-
<html lang="ja">
|
23
|
-
<head>
|
24
|
-
<meta charset="UTF-8">
|
25
|
-
<meta name="robots" content="noindex" />
|
26
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
27
|
-
<title>「…」パート選択式プレイヤー</title>
|
28
|
-
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0" />
|
29
|
-
<style>
|
30
|
-
/* 省略 */
|
31
|
-
</style>
|
32
|
-
</head>
|
33
|
-
<body>
|
34
|
-
|
35
|
-
<div class="container">
|
36
|
-
<h1>…</h1>
|
37
|
-
<
|
22
|
+
<button id="play">再生</button>
|
38
|
-
<button id="toggle-mode"><span class="material-symbols-outlined">brightness_6</span> テーマ切替</button>
|
39
|
-
<button onclick="helpDisplay()"><span class="material-symbols-outlined">sticky_note_2</span> 使い方</button><br>
|
40
|
-
<button id="play"><span class="material-symbols-outlined">play_arrow</span> 再生</button>
|
41
|
-
<button id="stop">
|
23
|
+
<button id="stop">てい止</button><br>
|
42
24
|
<label><input type="checkbox" id="repeat">リピート</label>
|
43
25
|
|
44
26
|
<h3>再生パート選択</h3>
|
45
|
-
<label><input type="checkbox" id="cb1" checked>~ measure 12 Alto</label>
|
46
|
-
<label><input type="checkbox" id="cb
|
27
|
+
<label><input type="checkbox" id="cb1" checked>4~12 Alt.</label>・・・(すべてのパート)
|
47
|
-
<label><input type="checkbox" id="cba" checked>アルト</label>
|
48
|
-
<label><input type="checkbox" id="cbt" checked>テノール</label>
|
49
28
|
|
50
29
|
<h3>音量</h3>
|
51
30
|
<div class="slider-container">
|
@@ -60,193 +39,256 @@
|
|
60
39
|
</div>
|
61
40
|
</div>
|
62
41
|
|
63
|
-
<audio id="p" src="p.mp3"></audio>
|
42
|
+
<audio id="p" src="p.mp3"></audio>…(すべてのMP3)
|
64
|
-
<audio id="all" src="all.mp3"></audio>
|
65
|
-
<audio id="1" src="1.mp3"></audio>
|
66
|
-
<audio id="s" src="s.mp3"></audio>
|
67
|
-
<audio id="a" src="a.mp3"></audio>
|
68
|
-
<audio id="t" src="t.mp3"></audio>
|
69
43
|
|
70
44
|
<script>
|
45
|
+
document.addEventListener("DOMContentLoaded", function() {
|
46
|
+
// グローバル変数
|
47
|
+
let audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
48
|
+
let gainNode = audioCtx.createGain();
|
49
|
+
gainNode.connect(audioCtx.destination);
|
50
|
+
// 初期音量をスライダーに合わせる
|
51
|
+
gainNode.gain.value = document.getElementById("volume").value / 100;
|
52
|
+
|
53
|
+
// 各ファイルのパスマッピング
|
71
|
-
|
54
|
+
const fileList = {
|
72
|
-
|
55
|
+
"p": "p.mp3",…(すべてのMP3)
|
73
|
-
all: document.getElementById("all"),
|
74
|
-
1: document.getElementById("1"),
|
75
|
-
s: document.getElementById("s"),
|
76
|
-
a: document.getElementById("a"),
|
77
|
-
t: document.getElementById("t"),
|
78
|
-
|
56
|
+
};
|
57
|
+
|
79
|
-
|
58
|
+
// デコード済みAudioBufferを格納する辞書
|
80
|
-
|
59
|
+
const buffers = {};
|
81
|
-
|
60
|
+
let filesToLoad = Object.keys(fileList).length;
|
82
|
-
|
61
|
+
let errorOccurred = false;
|
83
|
-
|
62
|
+
const loadDiv = document.getElementById("load");
|
84
|
-
|
63
|
+
loadDiv.style.display = "block";
|
64
|
+
|
65
|
+
// ファイルを読み込み、デコードする関数
|
66
|
+
function loadBuffer(key, url) {
|
67
|
+
return fetch(url)
|
68
|
+
.then(response => {
|
69
|
+
if (!response.ok) throw new Error("Network response was not ok");
|
70
|
+
return response.arrayBuffer();
|
85
|
-
}
|
71
|
+
})
|
86
|
-
|
72
|
+
.then(arrayBuffer => audioCtx.decodeAudioData(arrayBuffer))
|
87
|
-
|
73
|
+
.then(audioBuffer => {
|
88
|
-
|
74
|
+
buffers[key] = audioBuffer;
|
89
|
-
|
90
|
-
document.getElementById("play").addEventListener("click", togglePlayPause);
|
91
|
-
document.getElementById("stop").addEventListener("click", stopAudio);
|
92
|
-
document.getElementById("repeat").addEventListener("change", updateRepeat);
|
93
|
-
document.getElementById("volume").addEventListener("input", adjustVolume);
|
94
|
-
document.getElementById("seek").addEventListener("input", seekAudio);
|
95
|
-
document.getElementById("toggle-mode").addEventListener("click", toggleMode);
|
96
|
-
|
97
|
-
Object.values(checkboxes).forEach(cb => cb.addEventListener("change", updateAudioSelection));
|
98
|
-
|
99
|
-
function updateAudioSelection() {
|
100
|
-
stopAudio();
|
101
|
-
playAudio();
|
102
|
-
}
|
103
|
-
|
104
|
-
function togglePlayPause() {
|
105
|
-
if (playing) {
|
106
|
-
|
75
|
+
filesToLoad--;
|
107
|
-
|
76
|
+
if (filesToLoad === 0 && !errorOccurred) {
|
108
|
-
|
77
|
+
loadDiv.style.display = "none";
|
78
|
+
console.log("全オーディオファイルの読み込み完了 (Web Audio API)");
|
109
79
|
}
|
80
|
+
})
|
81
|
+
.catch(err => {
|
82
|
+
errorOccurred = true;
|
83
|
+
loadDiv.style.display = "none";
|
84
|
+
alert("オーディオファイルの読み込みに失敗しました: " + url);
|
85
|
+
});
|
110
|
-
|
86
|
+
}
|
87
|
+
|
111
|
-
|
88
|
+
Object.keys(fileList).forEach(key => {
|
89
|
+
loadBuffer(key, fileList[key]);
|
90
|
+
});
|
91
|
+
|
92
|
+
// 再生状態管理用グローバル変数
|
93
|
+
let isPlaying = false;
|
94
|
+
let playStartTime = 0; // 再生開始時のaudioCtx.currentTime
|
95
|
+
let pauseOffset = 0; // 再生済みの秒数(再生中止時に蓄積)
|
96
|
+
let activeSources = []; // 現在再生中のAudioBufferSourceNode群
|
97
|
+
let selectedTrackIds = []; // チェックボックスから選択された音源IDの配列
|
98
|
+
let trackDuration = 0; // 選択したトラックの再生時間(最大値)
|
99
|
+
|
100
|
+
// チェックボックスの状態により再生する音源を決定
|
112
|
-
|
101
|
+
function updateSelectedTracks() {
|
102
|
+
// チェック対象:cb1,cb2,cb3,cb4, cbs, cba, cbt
|
103
|
+
const checkboxIds = ["cb1", "cb2", "cb3", "cb4", "cbs", "cba", "cbt"];
|
104
|
+
let allChecked = true;
|
113
|
-
|
105
|
+
checkboxIds.forEach(id => {
|
114
|
-
|
106
|
+
if (!document.getElementById(id).checked) {
|
107
|
+
allChecked = false;
|
108
|
+
}
|
109
|
+
});
|
110
|
+
if (allChecked) {
|
115
|
-
|
111
|
+
selectedTrackIds = ["all"];
|
116
|
-
|
112
|
+
} else {
|
113
|
+
// 常に「p」を追加
|
114
|
+
let tracks = ["p"];
|
115
|
+
const mapping = {
|
116
|
+
"cb1": "1",
|
117
|
+
"cb2": "2",
|
118
|
+
"cb3": "3",
|
119
|
+
"cb4": "4",
|
120
|
+
"cbs": "s",
|
121
|
+
"cba": "a",
|
122
|
+
"cbt": "t"
|
123
|
+
};
|
117
|
-
|
124
|
+
Object.keys(mapping).forEach(cbId => {
|
118
|
-
|
125
|
+
if (document.getElementById(cbId).checked) {
|
119
|
-
|
126
|
+
tracks.push(mapping[cbId]);
|
120
|
-
}
|
121
|
-
});
|
122
127
|
}
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
fu
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
}
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
}
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
128
|
+
});
|
129
|
+
selectedTrackIds = tracks;
|
130
|
+
}
|
131
|
+
}
|
132
|
+
|
133
|
+
// 選択した各音源の再生を開始(pauseOffsetから再生)
|
134
|
+
function startPlayback() {
|
135
|
+
updateSelectedTracks();
|
136
|
+
|
137
|
+
// 再生時間の決定:全チェックの場合は「all」それ以外は各トラックの最大値
|
138
|
+
if (selectedTrackIds.length === 1 && selectedTrackIds[0] === "all") {
|
139
|
+
trackDuration = buffers["all"].duration;
|
140
|
+
} else {
|
141
|
+
trackDuration = Math.max(...selectedTrackIds.map(id => buffers[id].duration));
|
142
|
+
}
|
143
|
+
|
144
|
+
activeSources = [];
|
145
|
+
selectedTrackIds.forEach(id => {
|
146
|
+
let source = audioCtx.createBufferSource();
|
147
|
+
source.buffer = buffers[id];
|
148
|
+
source.loop = document.getElementById("repeat").checked;
|
149
|
+
source.connect(gainNode);
|
150
|
+
// ※ 各ソースにonendedを設定すると、早く終了したものが先に発火する可能性があるため、
|
151
|
+
// 終了判定はupdateSeekBar内で行います。
|
152
|
+
if (pauseOffset < source.buffer.duration) {
|
153
|
+
source.start(0, pauseOffset);
|
154
|
+
}
|
155
|
+
activeSources.push(source);
|
156
|
+
});
|
157
|
+
playStartTime = audioCtx.currentTime;
|
158
|
+
isPlaying = true;
|
159
|
+
document.getElementById("play").innerHTML = '<span class="material-symbols-outlined">pause</span> 一時停止';
|
160
|
+
document.getElementById("stop").style.display = "inline";
|
161
|
+
updateSeekBar();
|
162
|
+
}
|
163
|
+
|
164
|
+
// 再生/一時停止の切り替え
|
165
|
+
function togglePlayPause() {
|
166
|
+
if (!isPlaying) {
|
167
|
+
if (audioCtx.state === "suspended") {
|
168
|
+
audioCtx.resume();
|
169
|
+
}
|
170
|
+
startPlayback();
|
171
|
+
} else {
|
172
|
+
pauseAudio();
|
173
|
+
}
|
174
|
+
}
|
175
|
+
|
176
|
+
// 一時停止処理:現在再生中のソースを停止し、経過時間を記録
|
177
|
+
function pauseAudio() {
|
178
|
+
if (isPlaying) {
|
179
|
+
pauseOffset += audioCtx.currentTime - playStartTime;
|
180
|
+
activeSources.forEach(source => {
|
181
|
+
try { source.stop(); } catch(e) {}
|
182
|
+
});
|
183
|
+
activeSources = [];
|
184
|
+
isPlaying = false;
|
185
|
+
document.getElementById("play").innerHTML = '<span class="material-symbols-outlined">play_arrow</span> 再生';
|
186
|
+
}
|
187
|
+
}
|
188
|
+
|
189
|
+
// 停止処理:再生停止&再生位置リセット
|
190
|
+
function stopAudio() {
|
191
|
+
if (isPlaying) {
|
192
|
+
activeSources.forEach(source => {
|
193
|
+
try { source.stop(); } catch(e) {}
|
194
|
+
});
|
195
|
+
}
|
196
|
+
activeSources = [];
|
197
|
+
isPlaying = false;
|
198
|
+
pauseOffset = 0;
|
199
|
+
document.getElementById("play").innerHTML = '<span class="material-symbols-outlined">play_arrow</span> 再生';
|
200
|
+
document.getElementById("stop").style.display = "none";
|
201
|
+
document.getElementById("seek").value = 0;
|
202
|
+
document.getElementById("time-text").textContent = formatTime(0) + " / " + formatTime(trackDuration);
|
203
|
+
}
|
204
|
+
|
205
|
+
// リピートチェックボックスの変更(再生中のソースに対してループ設定を反映)
|
206
|
+
document.getElementById("repeat").addEventListener("change", function() {
|
207
|
+
activeSources.forEach(source => {
|
208
|
+
source.loop = document.getElementById("repeat").checked;
|
209
|
+
});
|
210
|
+
});
|
211
|
+
|
212
|
+
// 音量調整:GainNodeのgain値を変更
|
213
|
+
document.getElementById("volume").addEventListener("input", function() {
|
214
|
+
let vol = document.getElementById("volume").value;
|
215
|
+
gainNode.gain.value = vol / 100;
|
216
|
+
document.getElementById("volume-text").textContent = vol + "%";
|
217
|
+
});
|
218
|
+
|
219
|
+
// シークバー操作:ユーザー操作時は再生中なら再スタート、停止中なら再生位置更新
|
220
|
+
document.getElementById("seek").addEventListener("input", function() {
|
221
|
+
let seekValue = parseFloat(document.getElementById("seek").value);
|
222
|
+
pauseOffset = seekValue;
|
223
|
+
if (isPlaying) {
|
224
|
+
activeSources.forEach(source => {
|
225
|
+
try { source.stop(); } catch(e) {}
|
226
|
+
});
|
227
|
+
activeSources = [];
|
228
|
+
playStartTime = audioCtx.currentTime;
|
229
|
+
startPlayback();
|
230
|
+
}
|
231
|
+
updateSeekBar();
|
232
|
+
});
|
233
|
+
|
234
|
+
// チェックボックス(パート選択)の変更で再生中なら再スタート(修正版)
|
235
|
+
const checkboxIds = ["cb1", "cb2", "cb3", "cb4", "cbs", "cba", "cbt"];
|
236
|
+
checkboxIds.forEach(id => {
|
237
|
+
document.getElementById(id).addEventListener("change", function() {
|
238
|
+
if (isPlaying) {
|
239
|
+
// 現在の再生位置を計算して更新(再生位置の同期を実現)
|
240
|
+
let currentPosition = pauseOffset + (audioCtx.currentTime - playStartTime);
|
241
|
+
pauseOffset = currentPosition;
|
242
|
+
activeSources.forEach(source => {
|
243
|
+
try { source.stop(); } catch(e) {}
|
244
|
+
});
|
245
|
+
activeSources = [];
|
246
|
+
playStartTime = audioCtx.currentTime;
|
247
|
+
startPlayback();
|
248
|
+
}
|
249
|
+
});
|
250
|
+
});
|
251
|
+
|
252
|
+
|
253
|
+
// シークバーの更新(再生中はrequestAnimationFrameで更新)
|
254
|
+
function updateSeekBar() {
|
255
|
+
let currentTime = isPlaying ? pauseOffset + (audioCtx.currentTime - playStartTime) : pauseOffset;
|
256
|
+
if (currentTime > trackDuration) {
|
257
|
+
currentTime = trackDuration;
|
258
|
+
}
|
259
|
+
document.getElementById("seek").max = trackDuration;
|
260
|
+
document.getElementById("seek").value = currentTime;
|
261
|
+
document.getElementById("time-text").textContent = formatTime(currentTime) + " / " + formatTime(trackDuration);
|
262
|
+
// リピートがOFFかつ再生が終了していたら自動停止
|
263
|
+
if (isPlaying && !document.getElementById("repeat").checked && currentTime >= trackDuration) {
|
264
|
+
stopAudio();
|
265
|
+
return;
|
266
|
+
}
|
267
|
+
if (isPlaying) {
|
268
|
+
requestAnimationFrame(updateSeekBar);
|
269
|
+
}
|
270
|
+
}
|
271
|
+
|
272
|
+
// 秒数を m:ss 表記に変換
|
273
|
+
function formatTime(seconds) {
|
274
|
+
let min = Math.floor(seconds / 60);
|
275
|
+
let sec = Math.floor(seconds % 60);
|
276
|
+
return min + ":" + sec.toString().padStart(2, "0");
|
277
|
+
}
|
278
|
+
|
279
|
+
// テーマ切替(ダークモード)
|
280
|
+
// キーボードショートカット
|
281
|
+
(省略)
|
282
|
+
|
283
|
+
// 再生/停止ボタンのイベントリスナー設定
|
284
|
+
document.getElementById("play").addEventListener("click", togglePlayPause);
|
285
|
+
document.getElementById("stop").addEventListener("click", stopAudio);
|
286
|
+
|
287
|
+
// ページロード時の初期化(firstLoadingを短時間表示)
|
288
|
+
(省略)
|
186
289
|
</script>
|
187
|
-
<script>
|
188
|
-
document.addEventListener("keydown", function(event) {
|
189
|
-
let volumeSlider = document.querySelector("#volume");
|
190
|
-
let seekSlider = document.querySelector("#seek");
|
191
|
-
let playButton = document.querySelector("#play");
|
192
|
-
let stopButton = document.querySelector("#stop");
|
193
|
-
|
194
|
-
switch (event.key) {
|
195
|
-
case " ":
|
196
|
-
case "p":
|
197
|
-
case "P":
|
198
|
-
if (playButton) playButton.click();
|
199
|
-
event.preventDefault(); // スペースキーのスクロールを防ぐ
|
200
|
-
break;
|
201
|
-
case "s":
|
202
|
-
case "S":
|
203
|
-
if (stopButton) stopButton.click();
|
204
|
-
break;
|
205
|
-
case "ArrowUp":
|
206
|
-
if (volumeSlider) {
|
207
|
-
let max = parseInt(volumeSlider.max) || 100;
|
208
|
-
volumeSlider.value = Math.min(parseInt(volumeSlider.value) + 1, max);
|
209
|
-
volumeSlider.dispatchEvent(new Event("input"));
|
210
|
-
}
|
211
|
-
break;
|
212
|
-
case "ArrowDown":
|
213
|
-
if (volumeSlider) {
|
214
|
-
volumeSlider.value = Math.max(parseInt(volumeSlider.value) - 1, 0);
|
215
|
-
volumeSlider.dispatchEvent(new Event("input"));
|
216
|
-
}
|
217
|
-
break;
|
218
|
-
case "ArrowRight":
|
219
|
-
if (seekSlider) {
|
220
|
-
let max = parseInt(seekSlider.max) || 360;
|
221
|
-
seekSlider.value = Math.min(parseInt(seekSlider.value) + 1, max);
|
222
|
-
seekSlider.dispatchEvent(new Event("input"));
|
223
|
-
}
|
224
|
-
break;
|
225
|
-
case "ArrowLeft":
|
226
|
-
if (seekSlider) {
|
227
|
-
seekSlider.value = Math.max(parseInt(seekSlider.value) - 1, 0);
|
228
|
-
seekSlider.dispatchEvent(new Event("input"));
|
229
|
-
}
|
230
|
-
break;
|
231
|
-
}
|
232
|
-
});
|
233
|
-
function helpDisplay() {
|
234
|
-
alert(
|
235
|
-
"パート選択式プレイヤー 使い方\n" +
|
236
|
-
"\n◆ショートカットキー\n" +
|
237
|
-
"・再生/一時停止 → スペースキーまたはPキー\n" +
|
238
|
-
"・停止 → Sキー\n・音量上下 → 上下矢印キー" +
|
239
|
-
"\n・再生位置変更 → 左右矢印キー\n" +
|
240
|
-
"\n◆注意事項\n" +
|
241
|
-
"・ご利用の機器の処理能力やインターネット速度などによりパートがずれることがあります。ずれた場合はページを再読み込みして再度お試しください。\n" +
|
242
|
-
"・パートのずれを防止するため、リピート機能は使用せず、再生1回ごとにページを再読み込みすることをお勧めします。\n" +
|
243
|
-
"・初期状態で再生位置の操作を行うとスライダーの表示が乱れますが、再生ボタンを押すと50秒地点から再生されます。"
|
244
|
-
);
|
245
|
-
}
|
246
|
-
</script>
|
247
|
-
|
248
|
-
</body>
|
249
|
-
</html>
|
250
290
|
```
|
251
291
|
|
292
|
+
[質問投稿時のときのHTML](https://writening.net/page?pXDfwP)
|
293
|
+
|
252
294
|
よろしくお願いいたします。
|