質問編集履歴

1

ご回答を参考に、アプリを修正しました。以前のせていたHTMLのテキストを見ることができるリンクを追加しました。

2025/03/04 14:29

投稿

KAN-KOFUN
KAN-KOFUN

スコア18

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
- <p class="title">合唱・作曲</p>
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"><span class="material-symbols-outlined">stop</span> 停止</button><br>
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="cbs" checked>ソプラノ</label><br>
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
- const audioFiles = {
54
+ const fileList = {
72
- p: document.getElementById("p"),
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
- const checkboxes = {
59
+ const buffers = {};
81
- 1: document.getElementById("cb1"),
60
+ let filesToLoad = Object.keys(fileList).length;
82
- s: document.getElementById("cbs"),
61
+ let errorOccurred = false;
83
- a: document.getElementById("cba"),
62
+ const loadDiv = document.getElementById("load");
84
- t: document.getElementById("cbt"),
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
- let playing = false;
73
+ .then(audioBuffer => {
88
- let selectedAudios = [];
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
- pauseAudio();
75
+ filesToLoad--;
107
- } else {
76
+ if (filesToLoad === 0 && !errorOccurred) {
108
- playAudio();
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
- function playAudio() {
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
- selectedAudios = [audioFiles.p];
105
+ checkboxIds.forEach(id => {
114
- if (Object.values(checkboxes).every(cb => cb.checked)) {
106
+ if (!document.getElementById(id).checked) {
107
+ allChecked = false;
108
+ }
109
+ });
110
+ if (allChecked) {
115
- selectedAudios = [audioFiles.all];
111
+ selectedTrackIds = ["all"];
116
- } else {
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
- Object.keys(checkboxes).forEach(key => {
124
+ Object.keys(mapping).forEach(cbId => {
118
- if (checkboxes[key].checked) {
125
+ if (document.getElementById(cbId).checked) {
119
- selectedAudios.push(audioFiles[key]);
126
+ tracks.push(mapping[cbId]);
120
- }
121
- });
122
127
  }
123
-
124
- selectedAudios.forEach(audio => {
125
- audio.currentTime = document.getElementById("seek").value;
126
- audio.play();
127
- audio.loop = document.getElementById("repeat").checked;
128
- });
129
-
130
- playing = true;
131
- document.getElementById("play").innerHTML = "<span class=\"material-symbols-outlined\">pause</span> 一時停止";
132
- document.getElementById("stop").style.display = "inline";
133
- updateSeekBar();
134
- }
135
-
136
- function pauseAudio() {
137
- selectedAudios.forEach(audio => audio.pause());
138
- playing = false;
139
- document.getElementById("play").innerHTML = "<span class=\"material-symbols-outlined\">play_arrow</span> 再生";
140
- }
141
-
142
- function stopAudio() {
143
- Object.values(audioFiles).forEach(audio => {
144
- audio.pause();
145
- audio.currentTime = 0;
146
- });
147
- playing = false;
148
- document.getElementById("play").innerHTML = "<span class=\"material-symbols-outlined\">play_arrow</span> 再生";
149
- document.getElementById("stop").style.display = "none";
150
- }
151
-
152
- function updateRepeat() {
153
- selectedAudios.forEach(audio => audio.loop = document.getElementById("repeat").checked);
154
- }
155
-
156
- function adjustVolume() {
157
- let volume = document.getElementById("volume").value / 100; // スライダーの値を0から1の範囲に変換
158
- document.getElementById("volume-text").textContent = `${Math.round(volume * 100)}%`;
159
- Object.values(audioFiles).forEach(audio => audio.volume = volume);
160
- }
161
-
162
- function seekAudio() {
163
- let seekValue = document.getElementById("seek").value;
164
- selectedAudios.forEach(audio => audio.currentTime = seekValue);
165
- updateSeekBar();
166
- }
167
-
168
- function updateSeekBar() {
169
- let maxDuration = Math.max(...selectedAudios.map(a => a.duration || 0));
170
- let currentTime = Math.max(...selectedAudios.map(a => a.currentTime || 0));
171
- document.getElementById("seek").max = maxDuration;
172
- document.getElementById("seek").value = currentTime;
173
- document.getElementById("time-text").textContent = `${formatTime(currentTime)} / ${formatTime(maxDuration)}`;
174
- if (playing) requestAnimationFrame(updateSeekBar);
175
- }
176
-
177
- function formatTime(seconds) {
178
- let min = Math.floor(seconds / 60);
179
- let sec = Math.floor(seconds % 60);
180
- return `${min}:${sec.toString().padStart(2, "0")}`;
181
- }
182
-
183
- function toggleMode() {
184
- document.body.classList.toggle("dark-mode");
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
  よろしくお願いいたします。