Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
8 changes: 8 additions & 0 deletions release-notes/v0.1.8.txt
Original file line number Diff line number Diff line change
@@ -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.
12 changes: 11 additions & 1 deletion src/engine/audio.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 });
Expand Down
5 changes: 2 additions & 3 deletions src/story/silent-audio.js
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 };
}

Expand Down
16 changes: 16 additions & 0 deletions tests/unit/engine-audio.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Loading