soiz1 commited on
Commit
e61a60b
·
1 Parent(s): 060d8a5

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +318 -375
index.html CHANGED
@@ -3,7 +3,7 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>音声合成プレイヤー</title>
7
  <style>
8
  body {
9
  font-family: 'Arial', sans-serif;
@@ -11,468 +11,411 @@
11
  color: #e6f1ff;
12
  margin: 0;
13
  padding: 20px;
 
 
 
14
  }
15
- .container {
16
- max-width: 1000px;
17
- margin: 0 auto;
18
- }
19
  h1 {
20
  color: #64ffda;
21
  text-align: center;
22
  margin-bottom: 30px;
23
- font-size: 2.5em;
 
 
24
  }
25
- .player-section {
 
26
  display: flex;
27
- flex-wrap: wrap;
28
- gap: 20px;
29
- margin-bottom: 30px;
 
 
 
 
30
  }
 
31
  .video-container {
32
- flex: 1;
33
- min-width: 300px;
34
- background: #112240;
35
- border-radius: 8px;
36
- padding: 15px;
37
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
38
  }
 
39
  video {
40
  width: 100%;
41
- border-radius: 4px;
 
42
  }
43
- .drop-area {
44
- flex: 1;
45
- min-width: 300px;
46
- background: #112240;
47
- border-radius: 8px;
48
- padding: 15px;
49
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
50
  }
51
- .drop-box {
52
- border: 2px dashed #64ffda;
53
- border-radius: 8px;
54
- padding: 20px;
55
- text-align: center;
56
- min-height: 100px;
57
  margin-bottom: 15px;
58
- transition: all 0.3s;
59
- }
60
- .drop-box.highlight {
61
- background-color: rgba(100, 255, 218, 0.1);
62
- border-color: #ffffff;
63
  }
64
- .sound-item {
65
- background: #233554;
66
- padding: 10px;
67
- margin-bottom: 8px;
68
- border-radius: 4px;
69
  display: flex;
70
- justify-content: space-between;
71
  align-items: center;
 
72
  }
73
- .sound-item button {
74
- background: #ff5555;
75
- border: none;
76
- color: white;
77
- border-radius: 4px;
78
- padding: 5px 10px;
79
- cursor: pointer;
80
- }
81
- .controls {
82
- background: #112240;
83
- border-radius: 8px;
84
- padding: 20px;
85
- margin-bottom: 20px;
86
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
87
- }
88
- .control-group {
89
- margin-bottom: 15px;
90
- }
91
- label {
92
- display: block;
93
- margin-bottom: 5px;
94
  color: #64ffda;
95
  }
96
- input[type="range"], input[type="number"] {
97
- width: 100%;
98
- background: #233554;
99
- border: none;
100
  height: 8px;
101
- border-radius: 4px;
 
 
102
  outline: none;
103
  }
 
104
  input[type="range"]::-webkit-slider-thumb {
105
  -webkit-appearance: none;
106
- width: 16px;
107
- height: 16px;
108
  background: #64ffda;
109
  border-radius: 50%;
110
  cursor: pointer;
111
  }
112
- input[type="number"] {
113
- padding: 8px;
114
- height: auto;
115
- border-radius: 4px;
116
- background: #233554;
117
- border: 1px solid #405676;
118
- color: white;
 
 
 
 
 
 
119
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  button {
121
- background: #64ffda;
122
- color: #0a192f;
123
- border: none;
124
  padding: 10px 20px;
125
- border-radius: 4px;
126
  cursor: pointer;
127
  font-weight: bold;
128
  transition: all 0.3s;
129
  }
 
130
  button:hover {
131
- background: #52e3c2;
132
- transform: translateY(-2px);
133
  }
134
- .button-group {
 
135
  display: flex;
136
  gap: 10px;
137
- margin-top: 15px;
138
  }
 
139
  .tech-decoration {
 
140
  height: 2px;
141
- background: linear-gradient(90deg, #64ffda, #0a192f);
142
  margin: 20px 0;
143
- position: relative;
144
- }
145
- .tech-decoration::after {
146
- content: "";
147
- position: absolute;
148
- top: -3px;
149
- right: 0;
150
- width: 8px;
151
- height: 8px;
152
- background: #64ffda;
153
- border-radius: 50%;
154
- }
155
- .hidden {
156
- display: none;
157
  }
158
  </style>
159
  </head>
160
  <body>
 
 
161
  <div class="container">
162
- <h1>音声合成プレイヤー</h1>
 
 
 
 
163
 
164
  <div class="tech-decoration"></div>
165
 
166
- <div class="player-section">
167
- <div class="video-container">
168
- <h2>動画プレビュー</h2>
169
- <video id="video" controls>
170
- <source src="v.mp4" type="video/mp4">
171
- お使いのブラウザは動画をサポートしていません。
172
- </video>
173
  </div>
174
-
175
- <div class="drop-area">
176
- <h2>音声ドロップエリア</h2>
177
- <div class="drop-box" id="dropBox">
178
- <p>音声ファイルをここにドラッグ&ドロップしてください</p>
179
- <p>(p.mp3, a.mp3, t.mp3, s.mp3)</p>
180
- </div>
181
- <div id="soundList"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  </div>
183
  </div>
184
 
185
  <div class="tech-decoration"></div>
186
 
187
- <div class="controls">
188
- <h2>設定メニュー</h2>
189
-
190
- <div class="control-group">
191
- <label for="startTime">再生開始秒数 (秒)</label>
192
- <input type="number" id="startTime" min="0" value="0" step="0.1">
193
- </div>
194
-
195
- <div class="control-group">
196
- <label for="endTime">再生終了秒数 (秒)</label>
197
- <input type="number" id="endTime" min="0" step="0.1">
198
  </div>
199
-
200
- <div class="control-group">
201
- <label for="volume">音量 (0-3)</label>
202
- <input type="range" id="volume" min="0" max="3" step="0.1" value="1">
203
- <span id="volumeValue">1</span>
204
  </div>
205
-
206
- <div class="control-group">
207
- <label for="playbackRate">再生速度 (0.5-2)</label>
208
- <input type="range" id="playbackRate" min="0.5" max="2" step="0.1" value="1">
209
- <span id="playbackRateValue">1</span>
210
  </div>
211
-
212
- <div class="control-group">
213
- <label>
214
- <input type="checkbox" id="loopCheckbox">
215
- ループ再生
216
- </label>
217
  </div>
218
-
219
- <div class="button-group">
220
- <button id="playButton">再生</button>
221
- <button id="pauseButton">一時停止</button>
222
- <button id="stopButton">停止</button>
223
  </div>
224
  </div>
 
 
 
 
 
 
 
 
225
  </div>
226
 
227
  <script>
228
- // 要素を取得
229
- const dropBox = document.getElementById('dropBox');
230
- const soundList = document.getElementById('soundList');
231
- const video = document.getElementById('video');
232
- const startTimeInput = document.getElementById('startTime');
233
- const endTimeInput = document.getElementById('endTime');
234
- const volumeInput = document.getElementById('volume');
235
- const volumeValue = document.getElementById('volumeValue');
236
- const playbackRateInput = document.getElementById('playbackRate');
237
- const playbackRateValue = document.getElementById('playbackRateValue');
238
- const loopCheckbox = document.getElementById('loopCheckbox');
239
- const playButton = document.getElementById('playButton');
240
- const pauseButton = document.getElementById('pauseButton');
241
- const stopButton = document.getElementById('stopButton');
242
-
243
- // 音声オブジェクトを保持する配列
244
- let audioContext;
245
- let audioBuffers = {};
246
- let soundSources = [];
247
- let videoDuration = 0;
248
-
249
- // 許可された音声ファイル
250
- const allowedSounds = ['p.mp3', 'a.mp3', 't.mp3', 's.mp3'];
251
-
252
- // ドラッグ&ドロップのイベントハンドラ
253
- ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
254
- dropBox.addEventListener(eventName, preventDefaults, false);
255
- });
256
-
257
- function preventDefaults(e) {
258
- e.preventDefault();
259
- e.stopPropagation();
260
- }
261
-
262
- ['dragenter', 'dragover'].forEach(eventName => {
263
- dropBox.addEventListener(eventName, highlight, false);
264
- });
265
-
266
- ['dragleave', 'drop'].forEach(eventName => {
267
- dropBox.addEventListener(eventName, unhighlight, false);
268
- });
269
-
270
- function highlight() {
271
- dropBox.classList.add('highlight');
272
- }
273
-
274
- function unhighlight() {
275
- dropBox.classList.remove('highlight');
276
- }
277
-
278
- dropBox.addEventListener('drop', handleDrop, false);
279
-
280
- function handleDrop(e) {
281
- const dt = e.dataTransfer;
282
- const files = dt.files;
283
 
284
- handleFiles(files);
285
- }
286
-
287
- function handleFiles(files) {
288
- [...files].forEach(file => {
289
- if (allowedSounds.includes(file.name)) {
290
- addSound(file.name);
291
- }
292
- });
293
- }
294
-
295
- // 音声を追加
296
- function addSound(filename) {
297
- // すでに追加済みかチェック
298
- if (audioBuffers[filename]) {
299
- alert(`${filename} は既に追加されています`);
300
- return;
301
- }
302
 
303
- // 音声リストに追加
304
- const soundItem = document.createElement('div');
305
- soundItem.className = 'sound-item';
306
- soundItem.innerHTML = `
307
- ${filename}
308
- <button data-filename="${filename}">削除</button>
309
- `;
310
- soundList.appendChild(soundItem);
311
 
312
- // 削除ボタンのイベントリスナー
313
- soundItem.querySelector('button').addEventListener('click', function() {
314
- removeSound(filename);
315
- });
316
 
317
- // 音声ファイルを読み込む
318
- loadSound(filename);
319
- }
320
-
321
- // 音声を削除
322
- function removeSound(filename) {
323
- // リストから削除
324
- const items = document.querySelectorAll('.sound-item');
325
- items.forEach(item => {
326
- if (item.querySelector('button').dataset.filename === filename) {
327
- item.remove();
328
- }
329
  });
330
 
331
- // バッファから削除
332
- delete audioBuffers[filename];
333
- }
334
-
335
- // 音声ファイルを読み込む
336
- async function loadSound(filename) {
337
- if (!audioContext) {
338
- audioContext = new (window.AudioContext || window.webkitAudioContext)();
 
 
 
 
 
339
  }
340
 
341
- try {
342
- const response = await fetch(filename);
343
- const arrayBuffer = await response.arrayBuffer();
344
- const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
345
- audioBuffers[filename] = audioBuffer;
346
 
347
- // 最初の音声が読み込まれたら動画の長さを設定
348
- if (Object.keys(audioBuffers).length === 1) {
349
- videoDuration = audioBuffer.duration;
350
- endTimeInput.value = videoDuration.toFixed(1);
351
- }
352
- } catch (error) {
353
- console.error('音声の読み込みに失敗しました:', error);
354
- alert(`音声 ${filename} の読み込みに失敗しました`);
355
- removeSound(filename);
356
- }
357
- }
358
-
359
- // 動画のメタデータが読み込まれたら
360
- video.addEventListener('loadedmetadata', function() {
361
- if (video.duration !== Infinity) {
362
- videoDuration = video.duration;
363
- endTimeInput.value = videoDuration.toFixed(1);
364
- }
365
- video.muted = true; // 動画の音声をミュート
366
- });
367
-
368
- // 音量スライダーの値を表示
369
- volumeInput.addEventListener('input', function() {
370
- volumeValue.textContent = this.value;
371
- });
372
-
373
- // 再生速度スライダーの値を表示
374
- playbackRateInput.addEventListener('input', function() {
375
- playbackRateValue.textContent = this.value;
376
- });
377
-
378
- // 再生ボタン
379
- playButton.addEventListener('click', function() {
380
- const startTime = parseFloat(startTimeInput.value);
381
- const endTime = parseFloat(endTimeInput.value);
382
- const volume = parseFloat(volumeInput.value);
383
- const playbackRate = parseFloat(playbackRateInput.value);
384
- const loop = loopCheckbox.checked;
385
-
386
- // バリデーション
387
- if (startTime >= endTime) {
388
- alert('終了時間は開始時間より大きくしてください');
389
- return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
  }
391
 
392
- if (Object.keys(audioBuffers).length === 0) {
393
- alert('音声が追加されていません');
394
- return;
 
 
 
 
 
 
 
 
 
 
395
  }
396
 
397
- // 既存の音声ソースを停止
398
- stopAllSounds();
399
-
400
- // 動画を設定
401
- video.currentTime = startTime;
402
- video.playbackRate = playbackRate;
403
- video.play();
404
-
405
- // 各音声を再生
406
- for (const filename in audioBuffers) {
407
- playSound(filename, startTime, endTime, volume, playbackRate, loop);
408
- }
409
- });
410
-
411
- // 音声を再生
412
- function playSound(filename, startTime, endTime, volume, playbackRate, loop) {
413
- if (!audioContext) {
414
- audioContext = new (window.AudioContext || window.webkitAudioContext)();
415
  }
416
 
417
- const source = audioContext.createBufferSource();
418
- source.buffer = audioBuffers[filename];
419
- source.playbackRate.value = playbackRate;
420
-
421
- const gainNode = audioContext.createGain();
422
- gainNode.gain.value = volume;
423
 
424
- source.connect(gainNode);
425
- gainNode.connect(audioContext.destination);
426
-
427
- source.start(0, startTime % audioBuffers[filename].duration);
428
-
429
- if (loop) {
430
- source.loop = true;
431
- source.loopStart = startTime % audioBuffers[filename].duration;
432
- source.loopEnd = endTime % audioBuffers[filename].duration;
433
- } else {
434
- source.stop(audioContext.currentTime + (endTime - startTime));
435
- }
436
 
437
- soundSources.push(source);
438
- }
439
-
440
- // 一時停止ボタン
441
- pauseButton.addEventListener('click', function() {
442
- video.pause();
443
- stopAllSounds();
444
- });
445
-
446
- // 停止ボタン
447
- stopButton.addEventListener('click', function() {
448
- video.pause();
449
- video.currentTime = parseFloat(startTimeInput.value);
450
- stopAllSounds();
451
- });
452
-
453
- // 全ての音声を停止
454
- function stopAllSounds() {
455
- soundSources.forEach(source => {
456
- try {
457
- source.stop();
458
- } catch (e) {
459
- console.log('音声ソースは既に停止しています');
460
  }
461
  });
462
- soundSources = [];
463
- }
464
-
465
- // 動画の再生位置が終了時間を超えたら停止
466
- video.addEventListener('timeupdate', function() {
467
- const endTime = parseFloat(endTimeInput.value);
468
- const loop = loopCheckbox.checked;
469
 
470
- if (!loop && video.currentTime >= endTime) {
471
- video.pause();
472
- stopAllSounds();
473
- } else if (loop && video.currentTime >= endTime) {
474
- video.currentTime = parseFloat(startTimeInput.value);
475
- }
476
  });
477
  </script>
478
  </body>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>音声動画プレイヤー</title>
7
  <style>
8
  body {
9
  font-family: 'Arial', sans-serif;
 
11
  color: #e6f1ff;
12
  margin: 0;
13
  padding: 20px;
14
+ display: flex;
15
+ flex-direction: column;
16
+ align-items: center;
17
  }
18
+
 
 
 
19
  h1 {
20
  color: #64ffda;
21
  text-align: center;
22
  margin-bottom: 30px;
23
+ border-bottom: 1px solid #64ffda;
24
+ padding-bottom: 10px;
25
+ width: 100%;
26
  }
27
+
28
+ .container {
29
  display: flex;
30
+ flex-direction: column;
31
+ width: 100%;
32
+ max-width: 800px;
33
+ background-color: #112240;
34
+ border-radius: 10px;
35
+ padding: 20px;
36
+ box-shadow: 0 0 20px rgba(100, 255, 218, 0.2);
37
  }
38
+
39
  .video-container {
40
+ position: relative;
41
+ width: 100%;
42
+ margin-bottom: 20px;
 
 
 
43
  }
44
+
45
  video {
46
  width: 100%;
47
+ border-radius: 5px;
48
+ background-color: #000;
49
  }
50
+
51
+ .controls {
52
+ display: flex;
53
+ flex-direction: column;
54
+ gap: 15px;
55
+ margin-bottom: 20px;
 
56
  }
57
+
58
+ .audio-controls {
59
+ display: flex;
60
+ flex-direction: column;
61
+ gap: 10px;
 
62
  margin-bottom: 15px;
 
 
 
 
 
63
  }
64
+
65
+ .audio-item {
 
 
 
66
  display: flex;
 
67
  align-items: center;
68
+ gap: 10px;
69
  }
70
+
71
+ .audio-item label {
72
+ min-width: 50px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  color: #64ffda;
74
  }
75
+
76
+ input[type="range"] {
77
+ flex-grow: 1;
 
78
  height: 8px;
79
+ -webkit-appearance: none;
80
+ background: #1e2a47;
81
+ border-radius: 5px;
82
  outline: none;
83
  }
84
+
85
  input[type="range"]::-webkit-slider-thumb {
86
  -webkit-appearance: none;
87
+ width: 18px;
88
+ height: 18px;
89
  background: #64ffda;
90
  border-radius: 50%;
91
  cursor: pointer;
92
  }
93
+
94
+ .settings {
95
+ background-color: #1e2a47;
96
+ padding: 15px;
97
+ border-radius: 5px;
98
+ margin-bottom: 20px;
99
+ }
100
+
101
+ .setting-item {
102
+ display: flex;
103
+ justify-content: space-between;
104
+ align-items: center;
105
+ margin-bottom: 10px;
106
  }
107
+
108
+ .setting-item:last-child {
109
+ margin-bottom: 0;
110
+ }
111
+
112
+ .setting-item label {
113
+ color: #ccd6f6;
114
+ }
115
+
116
+ input[type="number"], input[type="checkbox"], select {
117
+ background-color: #112240;
118
+ border: 1px solid #64ffda;
119
+ color: #e6f1ff;
120
+ padding: 5px;
121
+ border-radius: 3px;
122
+ }
123
+
124
  button {
125
+ background-color: #0a192f;
126
+ color: #64ffda;
127
+ border: 1px solid #64ffda;
128
  padding: 10px 20px;
129
+ border-radius: 5px;
130
  cursor: pointer;
131
  font-weight: bold;
132
  transition: all 0.3s;
133
  }
134
+
135
  button:hover {
136
+ background-color: #64ffda;
137
+ color: #0a192f;
138
  }
139
+
140
+ .buttons {
141
  display: flex;
142
  gap: 10px;
143
+ justify-content: center;
144
  }
145
+
146
  .tech-decoration {
147
+ width: 100%;
148
  height: 2px;
149
+ background: linear-gradient(90deg, transparent, #64ffda, transparent);
150
  margin: 20px 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  }
152
  </style>
153
  </head>
154
  <body>
155
+ <h1>音声動画プレイヤー</h1>
156
+
157
  <div class="container">
158
+ <div class="video-container">
159
+ <video id="video" muted>
160
+ <source src="v.mp4" type="video/mp4">
161
+ </video>
162
+ </div>
163
 
164
  <div class="tech-decoration"></div>
165
 
166
+ <div class="settings">
167
+ <h2>設定</h2>
168
+ <div class="setting-item">
169
+ <label for="start-time">再生開始秒数:</label>
170
+ <input type="number" id="start-time" min="0" value="0" step="0.1">
 
 
171
  </div>
172
+ <div class="setting-item">
173
+ <label for="end-time">再生終了秒数:</label>
174
+ <input type="number" id="end-time" min="0" value="0" step="0.1">
175
+ </div>
176
+ <div class="setting-item">
177
+ <label for="loop">ループ再生:</label>
178
+ <input type="checkbox" id="loop">
179
+ </div>
180
+ <div class="setting-item">
181
+ <label for="playback-rate">再生速度:</label>
182
+ <select id="playback-rate">
183
+ <option value="0.25">0.25x</option>
184
+ <option value="0.5">0.5x</option>
185
+ <option value="0.75">0.75x</option>
186
+ <option value="1" selected>1x</option>
187
+ <option value="1.25">1.25x</option>
188
+ <option value="1.5">1.5x</option>
189
+ <option value="2">2x</option>
190
+ <option value="3">3x</option>
191
+ </select>
192
+ </div>
193
+ <div class="setting-item">
194
+ <label for="global-volume">全体音量係数:</label>
195
+ <input type="range" id="global-volume" min="0" max="3" step="0.1" value="1">
196
+ <span id="global-volume-value">1</span>
197
  </div>
198
  </div>
199
 
200
  <div class="tech-decoration"></div>
201
 
202
+ <div class="audio-controls">
203
+ <h2>音声コントロール</h2>
204
+ <div class="audio-item">
205
+ <label>p.mp3</label>
206
+ <input type="range" class="volume-slider" data-audio="p" min="0" max="1" step="0.01" value="1">
207
+ <span class="volume-value">1</span>
 
 
 
 
 
208
  </div>
209
+ <div class="audio-item">
210
+ <label>a.mp3</label>
211
+ <input type="range" class="volume-slider" data-audio="a" min="0" max="1" step="0.01" value="1">
212
+ <span class="volume-value">1</span>
 
213
  </div>
214
+ <div class="audio-item">
215
+ <label>t.mp3</label>
216
+ <input type="range" class="volume-slider" data-audio="t" min="0" max="1" step="0.01" value="1">
217
+ <span class="volume-value">1</span>
 
218
  </div>
219
+ <div class="audio-item">
220
+ <label>s.mp3</label>
221
+ <input type="range" class="volume-slider" data-audio="s" min="0" max="1" step="0.01" value="1">
222
+ <span class="volume-value">1</span>
 
 
223
  </div>
224
+ <div class="audio-item">
225
+ <label>k.mp3</label>
226
+ <input type="range" class="volume-slider" data-audio="k" min="0" max="1" step="0.01" value="1">
227
+ <span class="volume-value">1</span>
 
228
  </div>
229
  </div>
230
+
231
+ <div class="tech-decoration"></div>
232
+
233
+ <div class="buttons">
234
+ <button id="play-btn">再生</button>
235
+ <button id="pause-btn">一時停止</button>
236
+ <button id="stop-btn">停止</button>
237
+ </div>
238
  </div>
239
 
240
  <script>
241
+ document.addEventListener('DOMContentLoaded', function() {
242
+ // 要素を取得
243
+ const video = document.getElementById('video');
244
+ const playBtn = document.getElementById('play-btn');
245
+ const pauseBtn = document.getElementById('pause-btn');
246
+ const stopBtn = document.getElementById('stop-btn');
247
+ const startTimeInput = document.getElementById('start-time');
248
+ const endTimeInput = document.getElementById('end-time');
249
+ const loopCheckbox = document.getElementById('loop');
250
+ const playbackRateSelect = document.getElementById('playback-rate');
251
+ const globalVolumeSlider = document.getElementById('global-volume');
252
+ const globalVolumeValue = document.getElementById('global-volume-value');
253
+ const volumeSliders = document.querySelectorAll('.volume-slider');
254
+ const volumeValues = document.querySelectorAll('.volume-value');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
 
256
+ // 音声オブジェクトを作成
257
+ const audioContext = new (window.AudioContext || window.webkitAudioContext)();
258
+ const audioBuffers = {};
259
+ const audioSources = {};
260
+ const gainNodes = {};
 
 
 
 
 
 
 
 
 
 
 
 
 
261
 
262
+ // 音声ファイル名の配列
263
+ const audioFiles = ['p', 'a', 't', 's', 'k'];
 
 
 
 
 
 
264
 
265
+ // 初期化
266
+ let videoDuration = 0;
267
+ let isPlaying = false;
 
268
 
269
+ // 動画のメタデータが読み込まれたら
270
+ video.addEventListener('loadedmetadata', function() {
271
+ videoDuration = video.duration;
272
+ endTimeInput.value = videoDuration.toFixed(1);
273
+ endTimeInput.max = videoDuration;
274
+ startTimeInput.max = videoDuration - 0.1;
 
 
 
 
 
 
275
  });
276
 
277
+ // 音声ファイルを読み込む
278
+ function loadAudioFiles() {
279
+ audioFiles.forEach(file => {
280
+ fetch(`${file}.mp3`)
281
+ .then(response => response.arrayBuffer())
282
+ .then(arrayBuffer => audioContext.decodeAudioData(arrayBuffer))
283
+ .then(audioBuffer => {
284
+ audioBuffers[file] = audioBuffer;
285
+ gainNodes[file] = audioContext.createGain();
286
+ gainNodes[file].gain.value = 1;
287
+ })
288
+ .catch(error => console.error(`Error loading ${file}.mp3:`, error));
289
+ });
290
  }
291
 
292
+ // 再生関数
293
+ function playMedia() {
294
+ if (isPlaying) return;
 
 
295
 
296
+ const startTime = parseFloat(startTimeInput.value) || 0;
297
+ let endTime = parseFloat(endTimeInput.value) || videoDuration;
298
+ const loop = loopCheckbox.checked;
299
+ const playbackRate = parseFloat(playbackRateSelect.value);
300
+ const globalVolume = parseFloat(globalVolumeSlider.value);
301
+
302
+ // 終了時間が動画の長さを超えないように
303
+ endTime = Math.min(endTime, videoDuration);
304
+
305
+ // 動画を設定
306
+ video.currentTime = startTime;
307
+ video.playbackRate = playbackRate;
308
+ video.muted = true;
309
+
310
+ // 音声を再生
311
+ audioFiles.forEach(file => {
312
+ if (audioBuffers[file]) {
313
+ // 既存のソースがあれば停止
314
+ if (audioSources[file]) {
315
+ audioSources[file].stop();
316
+ }
317
+
318
+ const source = audioContext.createBufferSource();
319
+ source.buffer = audioBuffers[file];
320
+
321
+ // ボリュームスライダーの値を取得
322
+ const volumeSlider = document.querySelector(`.volume-slider[data-audio="${file}"]`);
323
+ const volume = parseFloat(volumeSlider.value) * globalVolume;
324
+
325
+ // ゲインノードを設定
326
+ gainNodes[file].gain.value = volume;
327
+
328
+ // 接続
329
+ source.connect(gainNodes[file]);
330
+ gainNodes[file].connect(audioContext.destination);
331
+
332
+ // 再生
333
+ source.start(0, startTime, endTime - startTime);
334
+
335
+ // ループ設定
336
+ source.loop = loop;
337
+ if (loop) {
338
+ source.loopStart = startTime;
339
+ source.loopEnd = endTime;
340
+ }
341
+
342
+ audioSources[file] = source;
343
+ }
344
+ });
345
+
346
+ // 動画を再生
347
+ video.play();
348
+ isPlaying = true;
349
+
350
+ // 終了時間に達したら停止
351
+ video.ontimeupdate = function() {
352
+ if (video.currentTime >= endTime) {
353
+ if (!loop) {
354
+ stopMedia();
355
+ } else {
356
+ video.currentTime = startTime;
357
+ }
358
+ }
359
+ };
360
  }
361
 
362
+ // 一時停止関数
363
+ function pauseMedia() {
364
+ if (!isPlaying) return;
365
+
366
+ video.pause();
367
+ audioFiles.forEach(file => {
368
+ if (audioSources[file]) {
369
+ audioSources[file].stop();
370
+ audioSources[file] = null;
371
+ }
372
+ });
373
+
374
+ isPlaying = false;
375
  }
376
 
377
+ // 停止関数
378
+ function stopMedia() {
379
+ pauseMedia();
380
+ video.currentTime = parseFloat(startTimeInput.value) || 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
381
  }
382
 
383
+ // イベントリスナーを設定
384
+ playBtn.addEventListener('click', playMedia);
385
+ pauseBtn.addEventListener('click', pauseMedia);
386
+ stopBtn.addEventListener('click', stopMedia);
 
 
387
 
388
+ // ボリュームスライダーのイベント
389
+ volumeSliders.forEach((slider, index) => {
390
+ slider.addEventListener('input', function() {
391
+ const value = parseFloat(this.value);
392
+ volumeValues[index].textContent = value.toFixed(2);
393
+
394
+ if (isPlaying && audioSources[this.dataset.audio] && gainNodes[this.dataset.audio]) {
395
+ const globalVolume = parseFloat(globalVolumeSlider.value);
396
+ gainNodes[this.dataset.audio].gain.value = value * globalVolume;
397
+ }
398
+ });
399
+ });
400
 
401
+ // 全体音量スライダーのイベント
402
+ globalVolumeSlider.addEventListener('input', function() {
403
+ const value = parseFloat(this.value);
404
+ globalVolumeValue.textContent = value.toFixed(1);
405
+
406
+ if (isPlaying) {
407
+ audioFiles.forEach(file => {
408
+ if (gainNodes[file]) {
409
+ const volumeSlider = document.querySelector(`.volume-slider[data-audio="${file}"]`);
410
+ const volume = parseFloat(volumeSlider.value) * value;
411
+ gainNodes[file].gain.value = volume;
412
+ }
413
+ });
 
 
 
 
 
 
 
 
 
 
414
  }
415
  });
 
 
 
 
 
 
 
416
 
417
+ // 初期化
418
+ loadAudioFiles();
 
 
 
 
419
  });
420
  </script>
421
  </body>