sound library
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
const vm = require('node:vm');
|
||||
const test = require('node:test');
|
||||
|
||||
function harness() {
|
||||
const storage = new Map(), blobs = [], revoked = [], undo = [];
|
||||
const source = fs.readFileSync(path.join(__dirname, '..', 'AnimationDirector.js'), 'utf8')
|
||||
.replace(' Plugin.register(PLUGIN_ID, {', `
|
||||
globalThis.api = { loadSoundLibrary, resolveSoundAudio, mapAnimationSounds, setSoundPreviewMode,
|
||||
applyLibrarySound, compileConjoinedStage, showSoundLibrary, SOUND_SOURCE_KEY, SOUND_PREVIEW_PROPERTY };
|
||||
Plugin.register(PLUGIN_ID, {`);
|
||||
class EffectAnimator {
|
||||
constructor(animation) { this.animation = animation; this.sound = []; }
|
||||
createKeyframe(data, time) {
|
||||
const frame = { channel: 'sound', time, animator: this, select() {},
|
||||
extend(data) { this.data_points = data.data_points.map((point) => ({ ...point, getUndoCopy() { return { ...this }; } })); } };
|
||||
frame.extend(data); this.sound.push(frame); return frame;
|
||||
}
|
||||
select() {}
|
||||
}
|
||||
const context = {
|
||||
Plugin: { register() {} }, Buffer, requireNativeModule: require, Blob,
|
||||
URL: { createObjectURL(blob) { blobs.push(blob); return `blob:test-${blobs.length}`; }, revokeObjectURL(url) { revoked.push(url); } },
|
||||
localStorage: { getItem(key) { return storage.get(key); }, setItem(key, value) { storage.set(key, value); } },
|
||||
Animation: { selected: { animators: {} }, all: [] }, EffectAnimator, Timeline: { time: 2, selected: [] },
|
||||
Undo: { initEdit(aspects) { undo.push(aspects.keyframes.slice()); }, finishEdit() {} },
|
||||
Dialog: class { constructor(options) { context.dialog = options; } show() {} delete() {} },
|
||||
};
|
||||
vm.runInNewContext(source, context);
|
||||
return { ...context.api, context, storage, blobs, revoked, undo };
|
||||
}
|
||||
|
||||
function assets(t, json, files = {}) {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ad-sounds-'));
|
||||
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
||||
const write = (name, content) => { const file = path.join(root, name); fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, content); };
|
||||
write('assets/needsofnature/sounds.json', JSON.stringify(json));
|
||||
for (const [name, content] of Object.entries(files)) write(`assets/needsofnature/sounds/${name}.ogg`, content);
|
||||
return root;
|
||||
}
|
||||
|
||||
test('sound library reads the real NoN sound registry, lazily maps semantic cues and caches audio', () => {
|
||||
const h = harness();
|
||||
h.storage.set(h.SOUND_SOURCE_KEY, path.resolve(__dirname, '../../NON/src/main/resources'));
|
||||
const library = h.loadSoundLibrary();
|
||||
assert(library.events.has('needsofnature:spray'));
|
||||
assert(library.events.size > 60);
|
||||
assert.equal(h.blobs.length, 0);
|
||||
const audio = h.resolveSoundAudio('reactiveimpact');
|
||||
assert.equal(audio, h.resolveSoundAudio('needsofnature:impactdry01'));
|
||||
assert.equal(h.blobs.length, 1);
|
||||
assert.equal(h.resolveSoundAudio('reactiveimpact_silent'), null);
|
||||
assert.equal(h.resolveSoundAudio('not_a_sound'), null);
|
||||
h.setSoundPreviewMode('peak');
|
||||
assert.equal(h.resolveSoundAudio('reactiveimpact'), h.resolveSoundAudio('shot_in01'));
|
||||
h.loadSoundLibrary();
|
||||
assert(h.revoked.includes(audio));
|
||||
});
|
||||
|
||||
test('event references and random pools resolve safely; cyclic or missing events stay unresolved', (t) => {
|
||||
const h = harness();
|
||||
h.storage.set(h.SOUND_SOURCE_KEY, assets(t, {
|
||||
one: { sounds: ['needsofnature:one'] },
|
||||
nested: { sounds: [{ type: 'event', name: 'needsofnature:one' }] },
|
||||
loop: { sounds: [{ type: 'event', name: 'needsofnature:loop' }] },
|
||||
unsafe: { sounds: ['needsofnature:../outside'] },
|
||||
}, { one: 'OggS test' }));
|
||||
h.loadSoundLibrary();
|
||||
assert.equal(h.resolveSoundAudio('nested'), h.resolveSoundAudio('one'));
|
||||
assert.equal(h.resolveSoundAudio('random(needsofnature:missing, needsofnature:one)'), h.resolveSoundAudio('one'));
|
||||
assert.equal(h.resolveSoundAudio('loop'), null);
|
||||
assert.equal(h.resolveSoundAudio('unsafe'), null);
|
||||
});
|
||||
|
||||
test('pack sound priority honors replace without overriding higher-priority entries', (t) => {
|
||||
const h = harness();
|
||||
const high = assets(t, { one: { replace: true, sounds: ['needsofnature:high'] } }, { high: 'high' });
|
||||
const low = assets(t, { one: { sounds: ['needsofnature:low'] } }, { low: 'low' });
|
||||
h.storage.set('animationdirector_pack_folders', JSON.stringify([high]));
|
||||
h.storage.set(h.SOUND_SOURCE_KEY, low);
|
||||
const library = h.loadSoundLibrary();
|
||||
assert.deepEqual(Array.from(library.events.get('needsofnature:one').entries), ['needsofnature:high']);
|
||||
assert(h.resolveSoundAudio('one'));
|
||||
});
|
||||
|
||||
test('mapping retains cue IDs and manual previews, updates managed files and handles missing/silent cues', (t) => {
|
||||
const h = harness();
|
||||
h.storage.set(h.SOUND_SOURCE_KEY, assets(t, { impactdry01: { sounds: ['needsofnature:dry'] },
|
||||
shot_in01: { sounds: ['needsofnature:peak'] } }, { dry: 'dry', peak: 'peak' }));
|
||||
h.loadSoundLibrary();
|
||||
const points = [{ effect: 'reactiveimpact' }, { effect: 'reactiveimpact', file: 'custom.ogg' },
|
||||
{ effect: 'unknown' }, { effect: 'reactiveimpact_silent' }];
|
||||
const animation = { animators: { effects: { sound: [{ data_points: points }] } } };
|
||||
h.context.Animation.all = [animation];
|
||||
h.mapAnimationSounds([animation]);
|
||||
const original = points[0].file;
|
||||
assert(original.startsWith('blob:'));
|
||||
assert.equal(points[0].effect, 'reactiveimpact');
|
||||
assert.equal(points[1].file, 'custom.ogg');
|
||||
assert.equal(points[2].file, '');
|
||||
assert.equal(points[3].file, '');
|
||||
h.setSoundPreviewMode('peak');
|
||||
assert.notEqual(points[0].file, original);
|
||||
points[0].effect = 'reactiveimpact_silent';
|
||||
h.mapAnimationSounds([animation]);
|
||||
assert.equal(points[0].file, '');
|
||||
});
|
||||
|
||||
test('insertion is undoable and preserves existing cues at the same time; Apply edits selection', (t) => {
|
||||
const h = harness();
|
||||
h.storage.set(h.SOUND_SOURCE_KEY, assets(t, { one: { sounds: ['needsofnature:one'] } }, { one: 'one' }));
|
||||
h.loadSoundLibrary();
|
||||
h.applyLibrarySound('one', true);
|
||||
const animation = h.context.Animation.selected, effects = animation.animators.effects;
|
||||
assert.equal(effects.sound.length, 1);
|
||||
assert.equal(effects.sound[0].time, 2);
|
||||
assert(effects.sound[0].data_points[0].file.startsWith('blob:'));
|
||||
h.applyLibrarySound('birth', true);
|
||||
assert.equal(effects.sound.length, 1);
|
||||
assert.equal(effects.sound[0].data_points.length, 2);
|
||||
assert.equal(effects.sound[0].data_points[0].effect, 'one');
|
||||
h.context.Timeline.selected = [effects.sound[0]];
|
||||
h.applyLibrarySound('reactiveimpact_silent');
|
||||
assert.equal(effects.sound[0].data_points[0].effect, 'reactiveimpact_silent');
|
||||
assert.equal(effects.sound[0].data_points[0].file, '');
|
||||
assert.equal(h.undo.length, 3);
|
||||
});
|
||||
|
||||
test('editor preview fields never appear in exported sound cues', () => {
|
||||
const h = harness();
|
||||
const clip = { sound_effects: { '0.0': [{ effect: 'reactiveimpact', file: 'blob:test',
|
||||
[h.SOUND_PREVIEW_PROPERTY]: 'blob:test', volume: 0.5 }, { effect: 'custom:sound' }] } };
|
||||
const result = h.compileConjoinedStage({ name: 'sample.p1', animation: { compileBedrockAnimation() { return clip; } } });
|
||||
assert.equal(result.clip.sound_effects['0.0'][0].effect, 'reactiveimpact');
|
||||
assert.equal(result.clip.sound_effects['0.0'][0].volume, 0.5);
|
||||
assert.equal(result.clip.sound_effects['0.0'][0].file, undefined);
|
||||
assert.equal(clip.sound_effects['0.0'][0].file, 'blob:test');
|
||||
});
|
||||
|
||||
test('library UI exposes search, semantic preview modes, audition and insertion controls', () => {
|
||||
const h = harness();
|
||||
h.showSoundLibrary();
|
||||
const ui = h.context.dialog.component;
|
||||
assert.match(ui.template, /Search sound IDs/);
|
||||
assert.match(ui.template, /Insert at Playhead/);
|
||||
assert.match(ui.template, /Reactive preview/);
|
||||
assert.equal(typeof ui.methods.preview, 'function');
|
||||
assert.equal(typeof ui.methods.reload, 'function');
|
||||
});
|
||||
Reference in New Issue
Block a user