diff --git a/CHANGELOG.md b/CHANGELOG.md index c660e5b..c8a4c0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,22 @@ All notable changes to this package. Versions follow [Semantic Versioning](https://semver.org/). +## [0.1.8](https://github.com/empty-sekai/ournotes-player/compare/v0.1.7..v0.1.8) — 2026-10-16 + +### Features +- report fixes for script problems, and pace each line _(story-author)_ + + +### Bug Fixes +- cull per-object lights against the layer of each draw _(story)_ +- refuse nested DOTween Sequences _(story)_ +- play a cue missing from its sheet as silence _(story)_ + + +### Documentation +- list every export of the story and UI entry points + + ## [0.1.7](https://github.com/empty-sekai/ournotes-player/compare/v0.1.6..v0.1.7) — 2026-10-14 ### Features diff --git a/package-lock.json b/package-lock.json index 9a35155..33a900e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ournotes-player", - "version": "0.1.7", + "version": "0.1.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ournotes-player", - "version": "0.1.7", + "version": "0.1.8", "license": "AGPL-3.0-only", "devDependencies": { "@eslint/js": "10.0.1", diff --git a/package.json b/package.json index 4aab9e9..078bb57 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ournotes-player", - "version": "0.1.7", + "version": "0.1.8", "description": "Browser player for BanG Dream! Our Notes: live charts, story episodes and Live2D models with the game's own shaders (WebGL2 + WebAudio, custom elements)", "keywords": [ "bang-dream", diff --git a/release-notes/v0.1.8.txt b/release-notes/v0.1.8.txt new file mode 100644 index 0000000..48f4ed0 --- /dev/null +++ b/release-notes/v0.1.8.txt @@ -0,0 +1,8 @@ +ournotes-player 0.1.8 plays a story line whose cue is missing from its cue sheet as silence, and fixes story lighting and DOTween handling. + +- A story sound whose cue the cue sheet does not hold no longer stops the episode at preload; the line runs on without that sound. +- Per-object lights are culled against the layer of each draw. +- Nested DOTween Sequences are refused. +- The experimental story author reports fixes for script problems and paces each line. + +Validation: type checks, reproducible builds and package checks pass; the unit tests pass. diff --git a/src/engine/audio.js b/src/engine/audio.js index e29f66d..b1a3680 100644 --- a/src/engine/audio.js +++ b/src/engine/audio.js @@ -64,6 +64,9 @@ export class SoundCategoryVolumes { } }; +// the meta of a cue the sheet lacks: no file, no samples +export const MISSING_CUE = Object.freeze({ file: null, sampleRate: 48000, channels: 1, samples: 0, missing: true }); + export class Audio { constructor(resolve, loop, { context = null, assets = null } = {}) { this.ownsContext = !context; @@ -219,7 +222,14 @@ export class Audio { const c = this.cueOf(id), key = `${c.sheet}/${c.cue}`; if (this.buffers.has(key)) continue; const meta = this.assets.json(`audio/${c.sheet}/cues.json`)[c.cue]; - if (!meta) throw new Error(`cue ${c.cue} not decoded in ${c.sheet}`); + if (!meta) { + // a sound row whose cue the sheet does not hold (a line listed with a voice that was never shipped): it + // plays as an empty sound, so the line runs on and the rest of the episode is not held back + console.warn(`cue ${c.cue} is not in cue sheet ${c.sheet}; it plays as silence`); + const rate = this.ctx.sampleRate; + this.buffers.set(key, { buf: this.ctx.createBuffer(1, 1, rate), meta: { ...MISSING_CUE, sampleRate: rate } }); + continue; + } // decoded at the context's rate; the loop points below are converted to seconds with the cue's own rate const buf = await this.ctx.decodeAudioData(this.assets.arrayBuffer(`audio/${c.sheet}/${meta.file}`)); this.buffers.set(key, { buf, meta }); diff --git a/src/story/silent-audio.js b/src/story/silent-audio.js index c79eaac..773333c 100644 --- a/src/story/silent-audio.js +++ b/src/story/silent-audio.js @@ -1,4 +1,4 @@ -import { SOUND_CATEGORY, SoundCategoryVolumes } from "../engine/audio.js"; +import { MISSING_CUE, SOUND_CATEGORY, SoundCategoryVolumes } from "../engine/audio.js"; // SilentAudio: the SoundManager of a story session without Web Audio (Node, or a page that plays the story muted). // It keeps the sounds' timing on the player loop's clock and plays nothing: a cue lasts its length in cues.json @@ -24,8 +24,7 @@ export class SilentAudio { cueOf(soundId) { const c = this.resolve(soundId); - const meta = this.assets.json(`audio/${c.sheet}/cues.json`)[c.cue]; - if (!meta) throw new Error(`cue ${c.cue} not in ${c.sheet}`); + const meta = this.assets.json(`audio/${c.sheet}/cues.json`)[c.cue] || MISSING_CUE; // a cue the sheet lacks is silence return { ...c, meta }; } diff --git a/tests/unit/engine-audio.test.mjs b/tests/unit/engine-audio.test.mjs index 835604f..2671127 100644 --- a/tests/unit/engine-audio.test.mjs +++ b/tests/unit/engine-audio.test.mjs @@ -178,3 +178,19 @@ test("timeStretch keeps the length ratio and the pitch of a sine", () => { assert.ok(Math.abs(rate - 2 * f) < 0.05 * 2 * f, `speed ${speed}: ${rate} crossings/s`); } }); + +test("a cue the sheet lacks plays as an empty sound and ends at once", async () => { + const ctx = fakeContext(); + const row = { _isRandomPitch: false }; + const cue = { sheet: "S", cue: "gone", category: 2, row }; + const assets = { json: () => ({}), arrayBuffer: () => { throw new Error("no file to read"); } }; + const a = new Audio(() => cue, { time: 0 }, { context: ctx, assets }); + const warn = console.warn; + console.warn = () => {}; + try { await a.preload([1]); } finally { console.warn = warn; } + const started = []; + const id = a.play(1, { onPlayStart: (info) => started.push(info) }); + assert.ok(id > 0); + assert.equal(started.length, 1); + assert.equal(ctx.sources.length, 1); +});