chart-preview 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.changeset/config.json +1 -1
- package/.github/workflows/release.yml +1 -1
- package/CHANGELOG.md +7 -0
- package/RELEASING.md +43 -0
- package/dev/main.ts +10 -3
- package/dist/index.js +58 -7
- package/dist/index.mjs +58 -7
- package/package.json +1 -1
- package/src/ChartPreview.ts +86 -7
package/.changeset/config.json
CHANGED
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# chart-preview
|
|
2
|
+
|
|
3
|
+
## 1.0.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 0bbccfe: Enhance audio management by adding fallback audio length and improving start delay handling. Update AudioManager to use fallback length if no audio files are provided, and ensure end event timeout is managed correctly during playback and seeking. Refactor start delay calculation for clarity in main.ts.
|
package/RELEASING.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Releasing
|
|
2
|
+
|
|
3
|
+
This project uses [Changesets](https://github.com/changesets/changesets) for versioning and publishing.
|
|
4
|
+
|
|
5
|
+
## How to Release
|
|
6
|
+
|
|
7
|
+
### 1. Create a Changeset
|
|
8
|
+
|
|
9
|
+
After making changes you want to release:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx changeset
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
- Select the version bump: `patch` (bug fix), `minor` (new feature), or `major` (breaking change)
|
|
16
|
+
- Write a short summary of the changes
|
|
17
|
+
|
|
18
|
+
This creates a file in `.changeset/` — commit it with your changes.
|
|
19
|
+
|
|
20
|
+
### 2. Push to `main`
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
git add .
|
|
24
|
+
git commit -m "Your commit message"
|
|
25
|
+
git push
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### 3. Merge the Version PR
|
|
29
|
+
|
|
30
|
+
GitHub Actions will create a **"Version Packages"** PR that:
|
|
31
|
+
|
|
32
|
+
- Bumps the version in `package.json`
|
|
33
|
+
- Updates `CHANGELOG.md`
|
|
34
|
+
|
|
35
|
+
Merge this PR to publish to npm automatically.
|
|
36
|
+
|
|
37
|
+
## Version Bump Guide
|
|
38
|
+
|
|
39
|
+
| Type | When to Use | Example |
|
|
40
|
+
| ------- | ----------------------------------- | ------------- |
|
|
41
|
+
| `patch` | Bug fixes, minor tweaks | 1.0.0 → 1.0.1 |
|
|
42
|
+
| `minor` | New features (backwards compatible) | 1.0.0 → 1.1.0 |
|
|
43
|
+
| `major` | Breaking changes | 1.0.0 → 2.0.0 |
|
package/dev/main.ts
CHANGED
|
@@ -471,9 +471,16 @@ loadBtn.addEventListener("click", async () => {
|
|
|
471
471
|
const audioLengthMs = currentScannedChart.song_length || lastNote + 5000;
|
|
472
472
|
|
|
473
473
|
// Get start delay from chart metadata
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
474
|
+
// delay takes priority over chart_offset if both are present
|
|
475
|
+
// Both use the same sign convention:
|
|
476
|
+
// - Positive value = audio starts earlier (audio is ahead of chart time)
|
|
477
|
+
// - Negative value = audio starts later (audio is delayed)
|
|
478
|
+
// We negate to convert to startDelayMs (internal representation):
|
|
479
|
+
// - startDelayMs < 0 = audio is ahead (at chart time 0, audio is already playing)
|
|
480
|
+
// - startDelayMs > 0 = audio is delayed (audio starts after chart time 0)
|
|
481
|
+
const startDelayMs = currentScannedChart.delay
|
|
482
|
+
? -currentScannedChart.delay
|
|
483
|
+
: -(currentScannedChart.chart_offset || 0);
|
|
477
484
|
|
|
478
485
|
// Calculate initial seek position from preview_start_time if available
|
|
479
486
|
const initialSeekPercent = currentScannedChart.preview_start_time
|
package/dist/index.js
CHANGED
|
@@ -148,7 +148,11 @@ var _ChartPreview = class {
|
|
|
148
148
|
chartPreview.progressIntervalMs = config.progressIntervalMs ?? DEFAULT_PROGRESS_INTERVAL_MS;
|
|
149
149
|
chartPreview.camera = new ChartCamera(config.container);
|
|
150
150
|
chartPreview.renderer = new ChartRenderer(config.container);
|
|
151
|
-
chartPreview.audioManager = await (isAudioContextSupported() ? AudioManager.create(
|
|
151
|
+
chartPreview.audioManager = await (isAudioContextSupported() ? AudioManager.create(
|
|
152
|
+
config.audioFiles,
|
|
153
|
+
config.startDelayMs,
|
|
154
|
+
config.audioLengthMs
|
|
155
|
+
) : SilentAudioManager.create(config.startDelayMs, config.audioLengthMs));
|
|
152
156
|
chartPreview.audioManager.on(
|
|
153
157
|
"end",
|
|
154
158
|
() => chartPreview.eventEmitter.emit("end")
|
|
@@ -326,9 +330,10 @@ var ChartRenderer = class extends THREE.WebGLRenderer {
|
|
|
326
330
|
}
|
|
327
331
|
};
|
|
328
332
|
var AudioManager = class {
|
|
329
|
-
constructor(audioFiles, startDelayMs) {
|
|
333
|
+
constructor(audioFiles, startDelayMs, fallbackAudioLengthMs) {
|
|
330
334
|
this.audioFiles = audioFiles;
|
|
331
335
|
this.startDelayMs = startDelayMs;
|
|
336
|
+
this.fallbackAudioLengthMs = fallbackAudioLengthMs;
|
|
332
337
|
this.eventEmitter = new import_eventemitter3.EventEmitter();
|
|
333
338
|
this.gainNode = null;
|
|
334
339
|
this.audioSources = [];
|
|
@@ -337,6 +342,7 @@ var AudioManager = class {
|
|
|
337
342
|
this.lastAudioCtxCurrentTime = 0;
|
|
338
343
|
// Necessary because audioCtx.currentTime doesn't reset to 0 on seek
|
|
339
344
|
this.audioLengthMs = 0;
|
|
345
|
+
this.endEventTimeout = null;
|
|
340
346
|
const ctx = getSharedAudioContext();
|
|
341
347
|
if (!ctx) {
|
|
342
348
|
throw new Error("AudioContext is not supported in this environment");
|
|
@@ -347,9 +353,14 @@ var AudioManager = class {
|
|
|
347
353
|
/**
|
|
348
354
|
* @param audioFiles The `Uint8Array[]` of the audio files to be played.
|
|
349
355
|
* @param startDelayMs The amount of time to delay the start of the audio. (can be negative)
|
|
356
|
+
* @param fallbackAudioLengthMs The fallback audio length to use if no audio files are provided.
|
|
350
357
|
*/
|
|
351
|
-
static async create(audioFiles, startDelayMs) {
|
|
352
|
-
const audioManager = new AudioManager(
|
|
358
|
+
static async create(audioFiles, startDelayMs, fallbackAudioLengthMs) {
|
|
359
|
+
const audioManager = new AudioManager(
|
|
360
|
+
audioFiles,
|
|
361
|
+
startDelayMs,
|
|
362
|
+
fallbackAudioLengthMs
|
|
363
|
+
);
|
|
353
364
|
await audioManager.initAudio();
|
|
354
365
|
return audioManager;
|
|
355
366
|
}
|
|
@@ -381,7 +392,13 @@ var AudioManager = class {
|
|
|
381
392
|
}
|
|
382
393
|
/** Nonnegative number of milliseconds representing when the audio ends (and when the chart preview ends). */
|
|
383
394
|
get chartEndTimeMs() {
|
|
384
|
-
|
|
395
|
+
const theoreticalEndTime = this.startDelayMs + this.audioLengthMs;
|
|
396
|
+
return Math.max(
|
|
397
|
+
theoreticalEndTime,
|
|
398
|
+
this.audioLengthMs,
|
|
399
|
+
this.fallbackAudioLengthMs,
|
|
400
|
+
0
|
|
401
|
+
);
|
|
385
402
|
}
|
|
386
403
|
async play() {
|
|
387
404
|
if (this.gainNode === null) {
|
|
@@ -390,14 +407,30 @@ var AudioManager = class {
|
|
|
390
407
|
if (this.audioCtx.state === "suspended") {
|
|
391
408
|
await this.audioCtx.resume();
|
|
392
409
|
}
|
|
410
|
+
if (this.audioSources.length === 0 && !this.endEventTimeout) {
|
|
411
|
+
const remainingMs = this.chartEndTimeMs - this.chartCurrentTimeMs;
|
|
412
|
+
if (remainingMs > 0) {
|
|
413
|
+
this.endEventTimeout = setTimeout(() => {
|
|
414
|
+
this.eventEmitter.emit("end");
|
|
415
|
+
}, remainingMs);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
393
418
|
}
|
|
394
419
|
async pause() {
|
|
395
420
|
if (this.audioCtx.state === "running") {
|
|
396
421
|
await this.audioCtx.suspend();
|
|
397
422
|
}
|
|
423
|
+
if (this.endEventTimeout) {
|
|
424
|
+
clearTimeout(this.endEventTimeout);
|
|
425
|
+
this.endEventTimeout = null;
|
|
426
|
+
}
|
|
398
427
|
}
|
|
399
428
|
closeAudio() {
|
|
400
429
|
this.eventEmitter.removeAllListeners();
|
|
430
|
+
if (this.endEventTimeout) {
|
|
431
|
+
clearTimeout(this.endEventTimeout);
|
|
432
|
+
this.endEventTimeout = null;
|
|
433
|
+
}
|
|
401
434
|
for (const source of this.audioSources) {
|
|
402
435
|
source.onended = null;
|
|
403
436
|
try {
|
|
@@ -427,12 +460,25 @@ var AudioManager = class {
|
|
|
427
460
|
(file) => this.audioCtx.decodeAudioData(file.slice(0).buffer)
|
|
428
461
|
)
|
|
429
462
|
);
|
|
430
|
-
|
|
463
|
+
const calculatedLengthMs = audioBuffers.length > 0 ? Math.max(...audioBuffers.map((b) => b.duration)) * 1e3 : 0;
|
|
464
|
+
this.audioLengthMs = calculatedLengthMs > 0 ? calculatedLengthMs : this.fallbackAudioLengthMs;
|
|
431
465
|
this.gainNode = this.audioCtx.createGain();
|
|
432
466
|
this.gainNode.gain.value = this._volume;
|
|
433
467
|
this.gainNode.connect(this.audioCtx.destination);
|
|
434
468
|
let endedCount = 0;
|
|
435
469
|
const audioStartOffsetSeconds = (this.lastSeekChartTimeMs - this.startDelayMs) / 1e3;
|
|
470
|
+
if (audioBuffers.length === 0) {
|
|
471
|
+
if (this.endEventTimeout) {
|
|
472
|
+
clearTimeout(this.endEventTimeout);
|
|
473
|
+
this.endEventTimeout = null;
|
|
474
|
+
}
|
|
475
|
+
const remainingMs = this.chartEndTimeMs - this.lastSeekChartTimeMs;
|
|
476
|
+
if (remainingMs > 0) {
|
|
477
|
+
this.endEventTimeout = setTimeout(() => {
|
|
478
|
+
this.eventEmitter.emit("end");
|
|
479
|
+
}, remainingMs);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
436
482
|
for (const audioBuffer of audioBuffers) {
|
|
437
483
|
const source = this.audioCtx.createBufferSource();
|
|
438
484
|
source.buffer = audioBuffer;
|
|
@@ -459,6 +505,10 @@ var AudioManager = class {
|
|
|
459
505
|
if (this.audioCtx.state === "running") {
|
|
460
506
|
await this.audioCtx.suspend();
|
|
461
507
|
}
|
|
508
|
+
if (this.endEventTimeout) {
|
|
509
|
+
clearTimeout(this.endEventTimeout);
|
|
510
|
+
this.endEventTimeout = null;
|
|
511
|
+
}
|
|
462
512
|
for (const source of this.audioSources) {
|
|
463
513
|
source.onended = null;
|
|
464
514
|
try {
|
|
@@ -517,7 +567,8 @@ var SilentAudioManager = class {
|
|
|
517
567
|
}
|
|
518
568
|
/** Nonnegative number of milliseconds representing when the audio ends (and when the chart preview ends). */
|
|
519
569
|
get chartEndTimeMs() {
|
|
520
|
-
|
|
570
|
+
const theoreticalEndTime = this.startDelayMs + this.audioLengthMs;
|
|
571
|
+
return Math.max(theoreticalEndTime, this.audioLengthMs, 0);
|
|
521
572
|
}
|
|
522
573
|
async play() {
|
|
523
574
|
if (this.lastResumeChartTimeMs >= this.chartEndTimeMs - 2) {
|
package/dist/index.mjs
CHANGED
|
@@ -109,7 +109,11 @@ var _ChartPreview = class {
|
|
|
109
109
|
chartPreview.progressIntervalMs = config.progressIntervalMs ?? DEFAULT_PROGRESS_INTERVAL_MS;
|
|
110
110
|
chartPreview.camera = new ChartCamera(config.container);
|
|
111
111
|
chartPreview.renderer = new ChartRenderer(config.container);
|
|
112
|
-
chartPreview.audioManager = await (isAudioContextSupported() ? AudioManager.create(
|
|
112
|
+
chartPreview.audioManager = await (isAudioContextSupported() ? AudioManager.create(
|
|
113
|
+
config.audioFiles,
|
|
114
|
+
config.startDelayMs,
|
|
115
|
+
config.audioLengthMs
|
|
116
|
+
) : SilentAudioManager.create(config.startDelayMs, config.audioLengthMs));
|
|
113
117
|
chartPreview.audioManager.on(
|
|
114
118
|
"end",
|
|
115
119
|
() => chartPreview.eventEmitter.emit("end")
|
|
@@ -287,9 +291,10 @@ var ChartRenderer = class extends THREE.WebGLRenderer {
|
|
|
287
291
|
}
|
|
288
292
|
};
|
|
289
293
|
var AudioManager = class {
|
|
290
|
-
constructor(audioFiles, startDelayMs) {
|
|
294
|
+
constructor(audioFiles, startDelayMs, fallbackAudioLengthMs) {
|
|
291
295
|
this.audioFiles = audioFiles;
|
|
292
296
|
this.startDelayMs = startDelayMs;
|
|
297
|
+
this.fallbackAudioLengthMs = fallbackAudioLengthMs;
|
|
293
298
|
this.eventEmitter = new EventEmitter();
|
|
294
299
|
this.gainNode = null;
|
|
295
300
|
this.audioSources = [];
|
|
@@ -298,6 +303,7 @@ var AudioManager = class {
|
|
|
298
303
|
this.lastAudioCtxCurrentTime = 0;
|
|
299
304
|
// Necessary because audioCtx.currentTime doesn't reset to 0 on seek
|
|
300
305
|
this.audioLengthMs = 0;
|
|
306
|
+
this.endEventTimeout = null;
|
|
301
307
|
const ctx = getSharedAudioContext();
|
|
302
308
|
if (!ctx) {
|
|
303
309
|
throw new Error("AudioContext is not supported in this environment");
|
|
@@ -308,9 +314,14 @@ var AudioManager = class {
|
|
|
308
314
|
/**
|
|
309
315
|
* @param audioFiles The `Uint8Array[]` of the audio files to be played.
|
|
310
316
|
* @param startDelayMs The amount of time to delay the start of the audio. (can be negative)
|
|
317
|
+
* @param fallbackAudioLengthMs The fallback audio length to use if no audio files are provided.
|
|
311
318
|
*/
|
|
312
|
-
static async create(audioFiles, startDelayMs) {
|
|
313
|
-
const audioManager = new AudioManager(
|
|
319
|
+
static async create(audioFiles, startDelayMs, fallbackAudioLengthMs) {
|
|
320
|
+
const audioManager = new AudioManager(
|
|
321
|
+
audioFiles,
|
|
322
|
+
startDelayMs,
|
|
323
|
+
fallbackAudioLengthMs
|
|
324
|
+
);
|
|
314
325
|
await audioManager.initAudio();
|
|
315
326
|
return audioManager;
|
|
316
327
|
}
|
|
@@ -342,7 +353,13 @@ var AudioManager = class {
|
|
|
342
353
|
}
|
|
343
354
|
/** Nonnegative number of milliseconds representing when the audio ends (and when the chart preview ends). */
|
|
344
355
|
get chartEndTimeMs() {
|
|
345
|
-
|
|
356
|
+
const theoreticalEndTime = this.startDelayMs + this.audioLengthMs;
|
|
357
|
+
return Math.max(
|
|
358
|
+
theoreticalEndTime,
|
|
359
|
+
this.audioLengthMs,
|
|
360
|
+
this.fallbackAudioLengthMs,
|
|
361
|
+
0
|
|
362
|
+
);
|
|
346
363
|
}
|
|
347
364
|
async play() {
|
|
348
365
|
if (this.gainNode === null) {
|
|
@@ -351,14 +368,30 @@ var AudioManager = class {
|
|
|
351
368
|
if (this.audioCtx.state === "suspended") {
|
|
352
369
|
await this.audioCtx.resume();
|
|
353
370
|
}
|
|
371
|
+
if (this.audioSources.length === 0 && !this.endEventTimeout) {
|
|
372
|
+
const remainingMs = this.chartEndTimeMs - this.chartCurrentTimeMs;
|
|
373
|
+
if (remainingMs > 0) {
|
|
374
|
+
this.endEventTimeout = setTimeout(() => {
|
|
375
|
+
this.eventEmitter.emit("end");
|
|
376
|
+
}, remainingMs);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
354
379
|
}
|
|
355
380
|
async pause() {
|
|
356
381
|
if (this.audioCtx.state === "running") {
|
|
357
382
|
await this.audioCtx.suspend();
|
|
358
383
|
}
|
|
384
|
+
if (this.endEventTimeout) {
|
|
385
|
+
clearTimeout(this.endEventTimeout);
|
|
386
|
+
this.endEventTimeout = null;
|
|
387
|
+
}
|
|
359
388
|
}
|
|
360
389
|
closeAudio() {
|
|
361
390
|
this.eventEmitter.removeAllListeners();
|
|
391
|
+
if (this.endEventTimeout) {
|
|
392
|
+
clearTimeout(this.endEventTimeout);
|
|
393
|
+
this.endEventTimeout = null;
|
|
394
|
+
}
|
|
362
395
|
for (const source of this.audioSources) {
|
|
363
396
|
source.onended = null;
|
|
364
397
|
try {
|
|
@@ -388,12 +421,25 @@ var AudioManager = class {
|
|
|
388
421
|
(file) => this.audioCtx.decodeAudioData(file.slice(0).buffer)
|
|
389
422
|
)
|
|
390
423
|
);
|
|
391
|
-
|
|
424
|
+
const calculatedLengthMs = audioBuffers.length > 0 ? Math.max(...audioBuffers.map((b) => b.duration)) * 1e3 : 0;
|
|
425
|
+
this.audioLengthMs = calculatedLengthMs > 0 ? calculatedLengthMs : this.fallbackAudioLengthMs;
|
|
392
426
|
this.gainNode = this.audioCtx.createGain();
|
|
393
427
|
this.gainNode.gain.value = this._volume;
|
|
394
428
|
this.gainNode.connect(this.audioCtx.destination);
|
|
395
429
|
let endedCount = 0;
|
|
396
430
|
const audioStartOffsetSeconds = (this.lastSeekChartTimeMs - this.startDelayMs) / 1e3;
|
|
431
|
+
if (audioBuffers.length === 0) {
|
|
432
|
+
if (this.endEventTimeout) {
|
|
433
|
+
clearTimeout(this.endEventTimeout);
|
|
434
|
+
this.endEventTimeout = null;
|
|
435
|
+
}
|
|
436
|
+
const remainingMs = this.chartEndTimeMs - this.lastSeekChartTimeMs;
|
|
437
|
+
if (remainingMs > 0) {
|
|
438
|
+
this.endEventTimeout = setTimeout(() => {
|
|
439
|
+
this.eventEmitter.emit("end");
|
|
440
|
+
}, remainingMs);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
397
443
|
for (const audioBuffer of audioBuffers) {
|
|
398
444
|
const source = this.audioCtx.createBufferSource();
|
|
399
445
|
source.buffer = audioBuffer;
|
|
@@ -420,6 +466,10 @@ var AudioManager = class {
|
|
|
420
466
|
if (this.audioCtx.state === "running") {
|
|
421
467
|
await this.audioCtx.suspend();
|
|
422
468
|
}
|
|
469
|
+
if (this.endEventTimeout) {
|
|
470
|
+
clearTimeout(this.endEventTimeout);
|
|
471
|
+
this.endEventTimeout = null;
|
|
472
|
+
}
|
|
423
473
|
for (const source of this.audioSources) {
|
|
424
474
|
source.onended = null;
|
|
425
475
|
try {
|
|
@@ -478,7 +528,8 @@ var SilentAudioManager = class {
|
|
|
478
528
|
}
|
|
479
529
|
/** Nonnegative number of milliseconds representing when the audio ends (and when the chart preview ends). */
|
|
480
530
|
get chartEndTimeMs() {
|
|
481
|
-
|
|
531
|
+
const theoreticalEndTime = this.startDelayMs + this.audioLengthMs;
|
|
532
|
+
return Math.max(theoreticalEndTime, this.audioLengthMs, 0);
|
|
482
533
|
}
|
|
483
534
|
async play() {
|
|
484
535
|
if (this.lastResumeChartTimeMs >= this.chartEndTimeMs - 2) {
|
package/package.json
CHANGED
package/src/ChartPreview.ts
CHANGED
|
@@ -167,7 +167,11 @@ export class ChartPreview {
|
|
|
167
167
|
chartPreview.camera = new ChartCamera(config.container);
|
|
168
168
|
chartPreview.renderer = new ChartRenderer(config.container);
|
|
169
169
|
chartPreview.audioManager = await (isAudioContextSupported()
|
|
170
|
-
? AudioManager.create(
|
|
170
|
+
? AudioManager.create(
|
|
171
|
+
config.audioFiles,
|
|
172
|
+
config.startDelayMs,
|
|
173
|
+
config.audioLengthMs
|
|
174
|
+
)
|
|
171
175
|
: SilentAudioManager.create(config.startDelayMs, config.audioLengthMs));
|
|
172
176
|
chartPreview.audioManager.on("end", () =>
|
|
173
177
|
chartPreview.eventEmitter.emit("end")
|
|
@@ -389,10 +393,12 @@ class AudioManager {
|
|
|
389
393
|
private lastSeekChartTimeMs = 0;
|
|
390
394
|
private lastAudioCtxCurrentTime = 0; // Necessary because audioCtx.currentTime doesn't reset to 0 on seek
|
|
391
395
|
private audioLengthMs: number = 0;
|
|
396
|
+
private endEventTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
392
397
|
|
|
393
398
|
private constructor(
|
|
394
399
|
private audioFiles: Uint8Array[],
|
|
395
|
-
private startDelayMs: number
|
|
400
|
+
private startDelayMs: number,
|
|
401
|
+
private fallbackAudioLengthMs: number
|
|
396
402
|
) {
|
|
397
403
|
const ctx = getSharedAudioContext();
|
|
398
404
|
if (!ctx) {
|
|
@@ -405,9 +411,18 @@ class AudioManager {
|
|
|
405
411
|
/**
|
|
406
412
|
* @param audioFiles The `Uint8Array[]` of the audio files to be played.
|
|
407
413
|
* @param startDelayMs The amount of time to delay the start of the audio. (can be negative)
|
|
414
|
+
* @param fallbackAudioLengthMs The fallback audio length to use if no audio files are provided.
|
|
408
415
|
*/
|
|
409
|
-
static async create(
|
|
410
|
-
|
|
416
|
+
static async create(
|
|
417
|
+
audioFiles: Uint8Array[],
|
|
418
|
+
startDelayMs: number,
|
|
419
|
+
fallbackAudioLengthMs: number
|
|
420
|
+
) {
|
|
421
|
+
const audioManager = new AudioManager(
|
|
422
|
+
audioFiles,
|
|
423
|
+
startDelayMs,
|
|
424
|
+
fallbackAudioLengthMs
|
|
425
|
+
);
|
|
411
426
|
await audioManager.initAudio();
|
|
412
427
|
return audioManager;
|
|
413
428
|
}
|
|
@@ -456,7 +471,16 @@ class AudioManager {
|
|
|
456
471
|
|
|
457
472
|
/** Nonnegative number of milliseconds representing when the audio ends (and when the chart preview ends). */
|
|
458
473
|
get chartEndTimeMs() {
|
|
459
|
-
|
|
474
|
+
// Calculate the theoretical end time based on start delay + audio length
|
|
475
|
+
const theoreticalEndTime = this.startDelayMs + this.audioLengthMs;
|
|
476
|
+
// Ensure we always have a positive end time - use audio length as minimum if startDelay is extremely negative
|
|
477
|
+
// This handles edge cases where chart metadata has invalid/extreme delay values
|
|
478
|
+
return Math.max(
|
|
479
|
+
theoreticalEndTime,
|
|
480
|
+
this.audioLengthMs,
|
|
481
|
+
this.fallbackAudioLengthMs,
|
|
482
|
+
0
|
|
483
|
+
);
|
|
460
484
|
}
|
|
461
485
|
|
|
462
486
|
async play() {
|
|
@@ -468,15 +492,34 @@ class AudioManager {
|
|
|
468
492
|
if (this.audioCtx.state === "suspended") {
|
|
469
493
|
await this.audioCtx.resume();
|
|
470
494
|
}
|
|
495
|
+
// Restart end event timeout for charts without audio
|
|
496
|
+
if (this.audioSources.length === 0 && !this.endEventTimeout) {
|
|
497
|
+
const remainingMs = this.chartEndTimeMs - this.chartCurrentTimeMs;
|
|
498
|
+
if (remainingMs > 0) {
|
|
499
|
+
this.endEventTimeout = setTimeout(() => {
|
|
500
|
+
this.eventEmitter.emit("end");
|
|
501
|
+
}, remainingMs);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
471
504
|
}
|
|
472
505
|
async pause() {
|
|
473
506
|
if (this.audioCtx.state === "running") {
|
|
474
507
|
await this.audioCtx.suspend();
|
|
475
508
|
}
|
|
509
|
+
// Clear end event timeout when pausing
|
|
510
|
+
if (this.endEventTimeout) {
|
|
511
|
+
clearTimeout(this.endEventTimeout);
|
|
512
|
+
this.endEventTimeout = null;
|
|
513
|
+
}
|
|
476
514
|
}
|
|
477
515
|
|
|
478
516
|
closeAudio() {
|
|
479
517
|
this.eventEmitter.removeAllListeners();
|
|
518
|
+
// Clear end event timeout
|
|
519
|
+
if (this.endEventTimeout) {
|
|
520
|
+
clearTimeout(this.endEventTimeout);
|
|
521
|
+
this.endEventTimeout = null;
|
|
522
|
+
}
|
|
480
523
|
// Stop all audio sources
|
|
481
524
|
for (const source of this.audioSources) {
|
|
482
525
|
// Remove onended handler before stopping to prevent false "end" events
|
|
@@ -515,8 +558,14 @@ class AudioManager {
|
|
|
515
558
|
this.audioCtx.decodeAudioData(file.slice(0).buffer)
|
|
516
559
|
)
|
|
517
560
|
);
|
|
561
|
+
|
|
562
|
+
// Use fallback if no audio files or if calculated duration is invalid
|
|
563
|
+
const calculatedLengthMs =
|
|
564
|
+
audioBuffers.length > 0
|
|
565
|
+
? Math.max(...audioBuffers.map((b) => b.duration)) * 1000
|
|
566
|
+
: 0;
|
|
518
567
|
this.audioLengthMs =
|
|
519
|
-
|
|
568
|
+
calculatedLengthMs > 0 ? calculatedLengthMs : this.fallbackAudioLengthMs;
|
|
520
569
|
|
|
521
570
|
this.gainNode = this.audioCtx.createGain();
|
|
522
571
|
this.gainNode.gain.value = this._volume;
|
|
@@ -525,6 +574,23 @@ class AudioManager {
|
|
|
525
574
|
let endedCount = 0;
|
|
526
575
|
const audioStartOffsetSeconds =
|
|
527
576
|
(this.lastSeekChartTimeMs - this.startDelayMs) / 1000;
|
|
577
|
+
|
|
578
|
+
// If no audio files, use a timeout to emit "end" event like SilentAudioManager does
|
|
579
|
+
if (audioBuffers.length === 0) {
|
|
580
|
+
// Clear any existing timeout
|
|
581
|
+
if (this.endEventTimeout) {
|
|
582
|
+
clearTimeout(this.endEventTimeout);
|
|
583
|
+
this.endEventTimeout = null;
|
|
584
|
+
}
|
|
585
|
+
// Schedule end event based on remaining time
|
|
586
|
+
const remainingMs = this.chartEndTimeMs - this.lastSeekChartTimeMs;
|
|
587
|
+
if (remainingMs > 0) {
|
|
588
|
+
this.endEventTimeout = setTimeout(() => {
|
|
589
|
+
this.eventEmitter.emit("end");
|
|
590
|
+
}, remainingMs);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
528
594
|
for (const audioBuffer of audioBuffers) {
|
|
529
595
|
const source = this.audioCtx.createBufferSource();
|
|
530
596
|
source.buffer = audioBuffer;
|
|
@@ -537,6 +603,10 @@ class AudioManager {
|
|
|
537
603
|
source.connect(this.gainNode!);
|
|
538
604
|
// When using a shared AudioContext, we need to schedule relative to the current time,
|
|
539
605
|
// not absolute time 0, since the context may have been running for a while
|
|
606
|
+
//
|
|
607
|
+
// audioStartOffsetSeconds calculation:
|
|
608
|
+
// - Positive value: Start audio from this position (audio is "behind" chart time)
|
|
609
|
+
// - Negative value: Delay audio start by |value| seconds (audio hasn't started yet at chart time 0)
|
|
540
610
|
const delaySeconds = Math.abs(Math.min(audioStartOffsetSeconds, 0));
|
|
541
611
|
const when = this.audioCtx.currentTime + delaySeconds;
|
|
542
612
|
const offset = Math.max(audioStartOffsetSeconds, 0);
|
|
@@ -557,6 +627,12 @@ class AudioManager {
|
|
|
557
627
|
await this.audioCtx.suspend();
|
|
558
628
|
}
|
|
559
629
|
|
|
630
|
+
// Clear end event timeout when seeking
|
|
631
|
+
if (this.endEventTimeout) {
|
|
632
|
+
clearTimeout(this.endEventTimeout);
|
|
633
|
+
this.endEventTimeout = null;
|
|
634
|
+
}
|
|
635
|
+
|
|
560
636
|
// Stop existing audio sources
|
|
561
637
|
for (const source of this.audioSources) {
|
|
562
638
|
// Remove onended handler before stopping to prevent false "end" events
|
|
@@ -635,7 +711,10 @@ class SilentAudioManager {
|
|
|
635
711
|
}
|
|
636
712
|
/** Nonnegative number of milliseconds representing when the audio ends (and when the chart preview ends). */
|
|
637
713
|
get chartEndTimeMs() {
|
|
638
|
-
|
|
714
|
+
// Calculate the theoretical end time based on start delay + audio length
|
|
715
|
+
const theoreticalEndTime = this.startDelayMs + this.audioLengthMs;
|
|
716
|
+
// Ensure we always have a positive end time - use audio length as minimum if startDelay is extremely negative
|
|
717
|
+
return Math.max(theoreticalEndTime, this.audioLengthMs, 0);
|
|
639
718
|
}
|
|
640
719
|
|
|
641
720
|
async play() {
|