soiz1 commited on
Commit
73384c6
·
1 Parent(s): 5c8193e

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +370 -469
index.html CHANGED
@@ -11,566 +11,467 @@
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
  font-size: 2.5em;
24
- text-shadow: 0 0 10px rgba(100, 255, 218, 0.3);
25
  }
26
-
27
- .container {
28
  display: flex;
29
- width: 100%;
30
- max-width: 1200px;
31
  gap: 20px;
 
32
  }
33
-
34
- .left-panel, .right-panel {
35
  flex: 1;
36
- background-color: #112240;
37
- border-radius: 10px;
38
- padding: 20px;
39
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  }
41
-
42
  .drop-box {
43
  border: 2px dashed #64ffda;
44
  border-radius: 8px;
45
  padding: 20px;
46
- min-height: 150px;
47
- margin-bottom: 20px;
 
48
  transition: all 0.3s;
49
- background-color: rgba(100, 255, 218, 0.05);
50
  }
51
-
52
  .drop-box.highlight {
53
  background-color: rgba(100, 255, 218, 0.1);
54
- border-color: #64ffda;
55
- box-shadow: 0 0 15px rgba(100, 255, 218, 0.2);
56
- }
57
-
58
- .sound-box {
59
- display: inline-block;
60
- background-color: #233554;
61
- color: #e6f1ff;
62
- padding: 8px 15px;
63
- margin: 5px;
64
- border-radius: 20px;
65
- border: 1px solid #64ffda;
66
- cursor: move;
67
- user-select: none;
68
- transition: all 0.2s;
69
  }
70
-
71
- .sound-box:hover {
72
- background-color: #1e2a47;
73
- transform: translateY(-2px);
74
- }
75
-
76
- .sound-box.selected {
77
- background-color: #64ffda;
78
- color: #0a192f;
79
- font-weight: bold;
80
- }
81
-
82
- .video-container {
83
- position: relative;
84
- width: 100%;
85
- margin-bottom: 20px;
86
- }
87
-
88
- video {
89
- width: 100%;
90
- border-radius: 8px;
91
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
92
- }
93
-
94
- .controls {
95
  display: flex;
96
- justify-content: center;
97
- gap: 15px;
98
- margin-bottom: 20px;
99
  }
100
-
101
- button {
102
- background-color: #233554;
103
- color: #e6f1ff;
104
- border: 1px solid #64ffda;
105
- border-radius: 5px;
106
- padding: 10px 20px;
107
  cursor: pointer;
108
- transition: all 0.3s;
109
- font-size: 1em;
110
- }
111
-
112
- button:hover {
113
- background-color: #64ffda;
114
- color: #0a192f;
115
- transform: translateY(-2px);
116
- box-shadow: 0 5px 15px rgba(100, 255, 218, 0.3);
117
  }
118
-
119
- .settings {
120
- background-color: #112240;
121
- border-radius: 10px;
122
  padding: 20px;
123
- margin-top: 20px;
124
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
125
- width: 100%;
126
- max-width: 1200px;
127
- }
128
-
129
- .settings h2 {
130
- color: #64ffda;
131
- margin-top: 0;
132
- border-bottom: 1px solid #233554;
133
- padding-bottom: 10px;
134
  }
135
-
136
- .setting-group {
137
- display: flex;
138
- flex-wrap: wrap;
139
- gap: 20px;
140
  margin-bottom: 15px;
141
  }
142
-
143
- .setting-item {
144
- flex: 1;
145
- min-width: 200px;
146
- }
147
-
148
  label {
149
  display: block;
150
  margin-bottom: 5px;
151
- color: #ccd6f6;
152
  }
153
-
154
  input[type="range"], input[type="number"] {
155
  width: 100%;
156
- background-color: #233554;
157
- border: 1px solid #1e2a47;
158
- border-radius: 5px;
159
- padding: 8px;
160
- color: #e6f1ff;
161
- }
162
-
163
- input[type="range"] {
164
- -webkit-appearance: none;
165
- height: 5px;
166
  background: #233554;
167
- border-radius: 5px;
 
 
 
168
  }
169
-
170
  input[type="range"]::-webkit-slider-thumb {
171
  -webkit-appearance: none;
172
- width: 15px;
173
- height: 15px;
174
  background: #64ffda;
175
  border-radius: 50%;
176
  cursor: pointer;
177
  }
178
-
179
- .status {
180
- margin-top: 20px;
181
- padding: 10px;
182
- background-color: rgba(100, 255, 218, 0.1);
183
- border-left: 3px solid #64ffda;
184
- border-radius: 0 5px 5px 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  }
186
-
187
  .tech-decoration {
 
 
 
 
 
 
 
188
  position: absolute;
189
- width: 100%;
190
- height: 100%;
191
- top: 0;
192
- left: 0;
193
- pointer-events: none;
194
- z-index: -1;
195
- opacity: 0.1;
196
- background:
197
- linear-gradient(90deg, #112240 1px, transparent 1px) 0 0 / 20px 20px,
198
- linear-gradient(#112240 1px, transparent 1px) 0 0 / 20px 20px;
199
  }
200
  </style>
201
  </head>
202
  <body>
203
- <div class="tech-decoration"></div>
204
- <h1>音声合成プレイヤー</h1>
205
-
206
  <div class="container">
207
- <div class="left-panel">
208
- <h2>音声アセット</h2>
209
- <div id="sound-assets">
210
- <div class="sound-box" draggable="true" data-sound="p.mp3">p.mp3</div>
211
- <div class="sound-box" draggable="true" data-sound="a.mp3">a.mp3</div>
212
- <div class="sound-box" draggable="true" data-sound="t.mp3">t.mp3</div>
213
- <div class="sound-box" draggable="true" data-sound="s.mp3">s.mp3</div>
214
- </div>
215
-
216
- <h2>ドロップボックス</h2>
217
- <div id="drop-box" class="drop-box">
218
- <p>音声ファイルをここにドラッグしてください</p>
219
- </div>
220
- </div>
221
 
222
- <div class="right-panel">
223
- <h2>プレビュー</h2>
224
  <div class="video-container">
 
225
  <video id="video" controls>
226
  <source src="v.mp4" type="video/mp4">
227
- お使いのブラウザはビデオタグをサポートしていません。
228
  </video>
229
  </div>
230
 
231
- <div class="controls">
232
- <button id="play-btn">再生</button>
233
- <button id="pause-btn">一時停止</button>
234
- <button id="stop-btn">停止</button>
235
- <button id="reset-btn">リセット</button>
236
- </div>
237
-
238
- <div class="status" id="status">
239
- 準備ができました。音声をドロップボックスに追加してください。
240
  </div>
241
  </div>
242
- </div>
243
-
244
- <div class="settings">
245
- <h2>設定</h2>
246
- <div class="setting-group">
247
- <div class="setting-item">
248
- <label for="start-time">再生開始秒数 (秒)</label>
249
- <input type="number" id="start-time" min="0" value="0" step="0.1">
 
250
  </div>
251
- <div class="setting-item">
252
- <label for="end-time">再生終了秒数 (秒)</label>
253
- <input type="number" id="end-time" min="0" step="0.1">
 
254
  </div>
255
- </div>
256
-
257
- <div class="setting-group">
258
- <div class="setting-item">
259
  <label for="volume">音量 (0-3)</label>
260
  <input type="range" id="volume" min="0" max="3" step="0.1" value="1">
261
- <span id="volume-value">1</span>
262
  </div>
263
- <div class="setting-item">
264
- <label for="playback-rate">再生速度 (0.5-2)</label>
265
- <input type="range" id="playback-rate" min="0.5" max="2" step="0.1" value="1">
266
- <span id="playback-rate-value">1</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
  </div>
268
- </div>
269
-
270
- <div class="setting-item">
271
- <label>
272
- <input type="checkbox" id="loop-checkbox">
273
- ループ再生
274
- </label>
275
  </div>
276
  </div>
277
-
278
  <script>
279
- document.addEventListener('DOMContentLoaded', function() {
280
- // 要素を取得
281
- const soundAssets = document.getElementById('sound-assets');
282
- const dropBox = document.getElementById('drop-box');
283
- const video = document.getElementById('video');
284
- const playBtn = document.getElementById('play-btn');
285
- const pauseBtn = document.getElementById('pause-btn');
286
- const stopBtn = document.getElementById('stop-btn');
287
- const resetBtn = document.getElementById('reset-btn');
288
- const statusDiv = document.getElementById('status');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
 
290
- // 設定要素
291
- const startTimeInput = document.getElementById('start-time');
292
- const endTimeInput = document.getElementById('end-time');
293
- const volumeInput = document.getElementById('volume');
294
- const volumeValue = document.getElementById('volume-value');
295
- const playbackRateInput = document.getElementById('playback-rate');
296
- const playbackRateValue = document.getElementById('playback-rate-value');
297
- const loopCheckbox = document.getElementById('loop-checkbox');
 
 
 
 
 
 
 
 
 
 
298
 
299
- // 音声コンテキストとノード
300
- let audioContext;
301
- let audioBuffers = {};
302
- let soundSources = [];
303
- let videoDuration = 0;
 
 
 
304
 
305
- // 初期化
306
- init();
 
 
307
 
308
- async function init() {
309
- try {
310
- // 動画の長さを取得
311
- video.addEventListener('loadedmetadata', function() {
312
- videoDuration = video.duration;
313
- endTimeInput.value = videoDuration.toFixed(1);
314
- endTimeInput.max = videoDuration;
315
- });
316
-
317
- // 音声コンテキストを初期化
318
- audioContext = new (window.AudioContext || window.webkitAudioContext)();
319
-
320
- // 既存の音声アセットを事前ロード
321
- const soundElements = soundAssets.querySelectorAll('.sound-box');
322
- for (const element of soundElements) {
323
- const soundFile = element.getAttribute('data-sound');
324
- await loadSound(soundFile);
325
- }
326
-
327
- // イベントリスナーを設定
328
- setupEventListeners();
329
-
330
- statusDiv.textContent = "準備ができました。音声をドロップボックスに追加してください。";
331
- } catch (error) {
332
- console.error("初期化エラー:", error);
333
- statusDiv.textContent = "初期化中にエラーが発生しました: " + error.message;
334
  }
335
- }
336
 
337
- function setupEventListeners() {
338
- // ドラッグ&ドロップイベント
339
- dropBox.addEventListener('dragover', function(e) {
340
- e.preventDefault();
341
- dropBox.classList.add('highlight');
342
- });
343
-
344
- dropBox.addEventListener('dragleave', function() {
345
- dropBox.classList.remove('highlight');
346
- });
347
-
348
- dropBox.addEventListener('drop', function(e) {
349
- e.preventDefault();
350
- dropBox.classList.remove('highlight');
351
-
352
- const soundFile = e.dataTransfer.getData('text/plain');
353
- if (soundFile && soundFile.endsWith('.mp3')) {
354
- addSoundToDropBox(soundFile);
355
- }
356
- });
357
-
358
- // 音声アセットのドラッグ開始
359
- soundAssets.querySelectorAll('.sound-box').forEach(box => {
360
- box.addEventListener('dragstart', function(e) {
361
- e.dataTransfer.setData('text/plain', this.getAttribute('data-sound'));
362
- });
363
- });
364
-
365
- // コントロールボタン
366
- playBtn.addEventListener('click', playAll);
367
- pauseBtn.addEventListener('click', pauseAll);
368
- stopBtn.addEventListener('click', stopAll);
369
- resetBtn.addEventListener('click', resetAll);
370
-
371
- // 設定変更イベント
372
- volumeInput.addEventListener('input', function() {
373
- volumeValue.textContent = this.value;
374
- });
375
-
376
- playbackRateInput.addEventListener('input', function() {
377
- playbackRateValue.textContent = this.value;
378
- video.playbackRate = this.value;
379
- });
380
-
381
- // 動画のループ処理
382
- video.addEventListener('timeupdate', function() {
383
- const startTime = parseFloat(startTimeInput.value) || 0;
384
- const endTime = parseFloat(endTimeInput.value) || videoDuration;
385
-
386
- if (loopCheckbox.checked && video.currentTime >= endTime) {
387
- video.currentTime = startTime;
388
- restartAudio(startTime);
389
- }
390
- });
391
  }
392
 
393
- async function loadSound(soundFile) {
394
- if (audioBuffers[soundFile]) return; // 既にロード済み
 
 
 
395
 
396
- try {
397
- const response = await fetch(soundFile);
398
- const arrayBuffer = await response.arrayBuffer();
399
- audioBuffers[soundFile] = await audioContext.decodeAudioData(arrayBuffer);
400
- } catch (error) {
401
- console.error(`音声ファイルのロードエラー (${soundFile}):`, error);
402
- throw error;
403
  }
 
 
 
 
404
  }
405
-
406
- function addSoundToDropBox(soundFile) {
407
- // 既に追加されているかチェック
408
- if (dropBox.querySelector(`.sound-box[data-sound="${soundFile}"]`)) {
409
- statusDiv.textContent = `"${soundFile}" は既に追加されています。`;
410
- return;
411
- }
412
-
413
- // 新しい音声ボックスを作成
414
- const soundBox = document.createElement('div');
415
- soundBox.className = 'sound-box';
416
- soundBox.setAttribute('data-sound', soundFile);
417
- soundBox.textContent = soundFile;
418
- soundBox.draggable = true;
419
-
420
- // ドラッグ開始イベント
421
- soundBox.addEventListener('dragstart', function(e) {
422
- e.dataTransfer.setData('text/plain', this.getAttribute('data-sound'));
423
- e.dataTransfer.effectAllowed = 'move';
424
- });
425
-
426
- // ドロップボックス内で���ドラッグオーバー
427
- soundBox.addEventListener('dragover', function(e) {
428
- e.preventDefault();
429
- e.dataTransfer.dropEffect = 'move';
430
- });
431
-
432
- // ドロップボックス内でのドロップ(並べ替え)
433
- soundBox.addEventListener('drop', function(e) {
434
- e.preventDefault();
435
- const draggedSound = e.dataTransfer.getData('text/plain');
436
- const draggedElement = dropBox.querySelector(`.sound-box[data-sound="${draggedSound}"]`);
437
-
438
- if (draggedElement && draggedElement !== this) {
439
- const dropY = e.clientY;
440
- const thisY = this.getBoundingClientRect().top;
441
-
442
- if (dropY < thisY) {
443
- dropBox.insertBefore(draggedElement, this);
444
- } else {
445
- dropBox.insertBefore(draggedElement, this.nextSibling);
446
- }
447
- }
448
- });
449
-
450
- // ダブルクリックで削除
451
- soundBox.addEventListener('dblclick', function() {
452
- this.remove();
453
- statusDiv.textContent = `"${soundFile}" を削除しました。`;
454
- });
455
-
456
- dropBox.appendChild(soundBox);
457
- statusDiv.textContent = `"${soundFile}" を追加しました。`;
458
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
459
 
460
- function playAll() {
461
- const startTime = parseFloat(startTimeInput.value) || 0;
462
- const endTime = parseFloat(endTimeInput.value) || videoDuration;
463
- const volume = parseFloat(volumeInput.value) || 1;
464
- const playbackRate = parseFloat(playbackRateInput.value) || 1;
465
-
466
- // 動画を再生
467
- video.currentTime = startTime;
468
- video.playbackRate = playbackRate;
469
- video.play().catch(e => console.error("動画再生エラー:", e));
470
-
471
- // 音声を再生
472
- stopAudio(); // 既存の音声を停止
473
-
474
- const soundBoxes = dropBox.querySelectorAll('.sound-box');
475
- if (soundBoxes.length === 0) {
476
- statusDiv.textContent = "再生中 (音声なし)";
477
- return;
478
- }
479
-
480
- let playTime = audioContext.currentTime;
481
-
482
- soundBoxes.forEach(box => {
483
- const soundFile = box.getAttribute('data-sound');
484
- const audioBuffer = audioBuffers[soundFile];
485
-
486
- if (audioBuffer) {
487
- const source = audioContext.createBufferSource();
488
- const gainNode = audioContext.createGain();
489
-
490
- source.buffer = audioBuffer;
491
- source.playbackRate.value = playbackRate;
492
- gainNode.gain.value = volume;
493
-
494
- source.connect(gainNode);
495
- gainNode.connect(audioContext.destination);
496
-
497
- source.start(playTime, startTime, endTime - startTime);
498
-
499
- soundSources.push({
500
- source: source,
501
- gain: gainNode
502
- });
503
- }
504
- });
505
-
506
- statusDiv.textContent = `再生中 (${startTime.toFixed(1)}秒~${endTime.toFixed(1)}秒)`;
507
  }
508
 
509
- function pauseAll() {
510
- video.pause();
511
- statusDiv.textContent = "一時停止中";
512
  }
513
 
514
- function stopAll() {
515
- video.pause();
516
- video.currentTime = parseFloat(startTimeInput.value) || 0;
517
- stopAudio();
518
- statusDiv.textContent = "停止中";
519
- }
520
 
521
- function resetAll() {
522
- video.pause();
523
- video.currentTime = 0;
524
- stopAudio();
525
- statusDiv.textContent = "リセットしました";
 
 
 
 
 
 
 
 
 
 
526
  }
527
 
528
- function stopAudio() {
529
- soundSources.forEach(source => {
530
- try {
531
- source.source.stop();
532
- } catch (e) {
533
- console.error("音声停止エラー:", e);
534
- }
535
- });
536
- soundSources = [];
 
 
 
 
 
 
 
 
 
537
  }
538
 
539
- function restartAudio(startTime) {
540
- if (soundSources.length === 0) return;
541
-
542
- stopAudio();
543
-
544
- const volume = parseFloat(volumeInput.value) || 1;
545
- const playbackRate = parseFloat(playbackRateInput.value) || 1;
546
- const endTime = parseFloat(endTimeInput.value) || videoDuration;
547
-
548
- let playTime = audioContext.currentTime;
549
-
550
- const soundBoxes = dropBox.querySelectorAll('.sound-box');
551
- soundBoxes.forEach(box => {
552
- const soundFile = box.getAttribute('data-sound');
553
- const audioBuffer = audioBuffers[soundFile];
554
-
555
- if (audioBuffer) {
556
- const source = audioContext.createBufferSource();
557
- const gainNode = audioContext.createGain();
558
-
559
- source.buffer = audioBuffer;
560
- source.playbackRate.value = playbackRate;
561
- gainNode.gain.value = volume;
562
-
563
- source.connect(gainNode);
564
- gainNode.connect(audioContext.destination);
565
-
566
- source.start(playTime, startTime, endTime - startTime);
567
-
568
- soundSources.push({
569
- source: source,
570
- gain: gainNode
571
- });
572
- }
573
- });
 
 
 
574
  }
575
  });
576
  </script>
 
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>