tweaks and improvements
This commit is contained in:
+98
-15
@@ -26,12 +26,15 @@
|
||||
const propSources = new Map();
|
||||
let propPreviewSignature = null, propPreviewGeneration = 0;
|
||||
const SOUND_SOURCE_KEY = 'animationdirector_sound_source';
|
||||
const SOUND_SELECTION_BLACKLIST = new Set(['needsofnature:a', 'needsofnature:funkytown']);
|
||||
const SOUND_PREVIEW_PROPERTY = 'animationdirector_audio_preview';
|
||||
const SOUND_EVENTS = ['select_animation', 'load_project', 'select_project', 'update_keyframe_selection'];
|
||||
let soundLibraryAction, soundLibraryDialog, soundLibraryListener, soundPreviewProperty, soundAudition;
|
||||
let soundLibrary, soundLibraryGeneration = 0, soundPreviewMode = 'dry';
|
||||
let soundLibrary, soundLibraryGeneration = 0;
|
||||
const soundAudioUrls = new Map(), soundMappedPoints = new WeakMap();
|
||||
let soundAudioBytes = 0;
|
||||
const figuraSoundPatches = [], figuraSoundMedia = new Set();
|
||||
let figuraSoundCompatEnabled = true, figuraSoundPlayListener;
|
||||
const SYSTEM_TAG_OPTIONS = {
|
||||
'': 'None (normal animation)',
|
||||
manualpeak: 'Manual peak',
|
||||
@@ -5410,11 +5413,11 @@
|
||||
return soundLibrary;
|
||||
}
|
||||
|
||||
function soundCueId(cue, mode = soundPreviewMode) {
|
||||
function soundCueId(cue) {
|
||||
const text = String(cue || '').trim();
|
||||
const bare = text.replace(/^(?:needsofnature|minecraft):/, '');
|
||||
if (bare.toLowerCase() === 'reactiveimpact_silent') return null;
|
||||
const semantic = { reactiveimpact: { dry: 'impactdry01', wet: 'impactwet01', full: 'wet04', peak: 'shot_in01' }[mode] || 'impactdry01',
|
||||
const semantic = { reactiveimpact: 'impactdry01',
|
||||
impactdry: 'impactdry01', impactwet: 'impactwet01', birth: 'pop' };
|
||||
if (semantic[bare.toLowerCase()]) return `needsofnature:${semantic[bare.toLowerCase()]}`;
|
||||
const id = text.includes(':') ? text : `needsofnature:${text}`;
|
||||
@@ -5468,22 +5471,17 @@
|
||||
for (const keyframe of animation?.animators?.effects?.sound || []) {
|
||||
for (const point of keyframe.data_points || []) {
|
||||
if (point.file && point.file !== point[SOUND_PREVIEW_PROPERTY]) continue;
|
||||
const signature = `${soundLibraryGeneration}:${soundPreviewMode}:${point.effect}:${point.sound}:${point.file}`;
|
||||
const signature = `${soundLibraryGeneration}:${point.effect}:${point.sound}:${point.file}`;
|
||||
if (soundMappedPoints.get(point) === signature) continue;
|
||||
const audio = resolveSoundAudio(point.effect) || (point.sound ? resolveSoundAudio(point.sound) : null);
|
||||
point.file = audio || '';
|
||||
point[SOUND_PREVIEW_PROPERTY] = point.file;
|
||||
soundMappedPoints.set(point, `${soundLibraryGeneration}:${soundPreviewMode}:${point.effect}:${point.sound}:${point.file}`);
|
||||
soundMappedPoints.set(point, `${soundLibraryGeneration}:${point.effect}:${point.sound}:${point.file}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setSoundPreviewMode(mode) {
|
||||
soundPreviewMode = ['dry', 'wet', 'full', 'peak'].includes(mode) ? mode : 'dry';
|
||||
mapAnimationSounds(typeof Animation === 'undefined' ? [] : Animation.all);
|
||||
}
|
||||
|
||||
function applyLibrarySound(id, insert = false) {
|
||||
const animation = typeof Animation === 'undefined' ? null : Animation.selected;
|
||||
if (!animation) throw new Error('Select an animation first.');
|
||||
@@ -5521,13 +5519,14 @@
|
||||
soundLibraryDialog = new Dialog({ id: 'animationdirector_sound_library', title: 'Sound Library', width: 700,
|
||||
buttons: ['Close'], onConfirm: stopSoundAudition, onCancel: stopSoundAudition,
|
||||
component: {
|
||||
data() { return { query: '', category: 'all', selected: '', mode: soundPreviewMode, page: 0,
|
||||
data() { return { query: '', category: 'all', selected: '', page: 0,
|
||||
ids: [...soundLibrary.events.keys()].sort(), warnings: soundLibrary.warnings.join('\n'), error: '' }; },
|
||||
computed: {
|
||||
rows() {
|
||||
const cues = ['reactiveimpact', 'reactiveimpact_silent', 'impactdry', 'impactwet', 'birth'];
|
||||
return [...cues.map((id) => ({ id, category: 'Gameplay cues' })), ...this.ids.map((id) => ({ id, category: 'Sound events' }))]
|
||||
.filter((entry) => (this.category === 'all' || entry.category === this.category) && entry.id.toLowerCase().includes(this.query.toLowerCase()));
|
||||
.filter((entry) => !SOUND_SELECTION_BLACKLIST.has(entry.id)
|
||||
&& (this.category === 'all' || entry.category === this.category) && entry.id.toLowerCase().includes(this.query.toLowerCase()));
|
||||
},
|
||||
visibleRows() { return this.rows.slice(this.page * 100, (this.page + 1) * 100); },
|
||||
},
|
||||
@@ -5558,16 +5557,14 @@
|
||||
},
|
||||
stop: stopSoundAudition,
|
||||
apply(insert) { try { applyLibrarySound(this.selected, insert); this.error = ''; } catch (error) { this.error = error.message; } },
|
||||
modeChanged() { stopSoundAudition(); setSoundPreviewMode(this.mode); },
|
||||
},
|
||||
template: `<div style="padding:12px">
|
||||
<div class="bar flex" style="gap:6px"><button @click="jar">Select NoN JAR / ZIP</button><button @click="folder">Select Assets Folder</button><button @click="reload">Refresh</button></div>
|
||||
<p class="subtle">Uses the selected sound source and remembered Pack Folders. Previews never change exported cue IDs.</p>
|
||||
<div class="bar flex" style="gap:8px;margin:10px 0"><input v-model="query" placeholder="Search sound IDs" style="flex:1" aria-label="Search sound IDs"><select v-model="category"><option value="all">All sounds</option><option>Sound events</option><option>Gameplay cues</option></select></div>
|
||||
<label>Reactive preview <select v-model="mode" @change="modeChanged"><option value="dry">Dry</option><option value="wet">Wet</option><option value="full">Full</option><option value="peak">Peak</option></select></label>
|
||||
<p class="subtle">Gameplay cues and random events use representative audio, not a gameplay simulation.</p>
|
||||
<div style="height:260px;overflow-y:auto;margin:10px 0;border:1px solid var(--color-border)">
|
||||
<div v-for="row in visibleRows" :key="row.category+row.id" @click="selected=row.id;stop()" @dblclick="selected=row.id;preview()" :style="{background:selected===row.id?'var(--color-accent)':'',padding:'6px 10px',cursor:'pointer'}"><span>{{row.id}}</span><small style="float:right;opacity:.6">{{row.category}}</small></div>
|
||||
<div v-for="row in visibleRows" :key="row.category+row.id" @click="selected=row.id;preview()" :style="{background:selected===row.id?'var(--color-accent)':'',padding:'6px 10px',cursor:'pointer'}"><span>{{row.id}}</span><small style="float:right;opacity:.6">{{row.category}}</small></div>
|
||||
</div>
|
||||
<div class="bar flex" style="gap:6px"><button @click="page--" :disabled="page===0">Previous</button><span>{{rows.length}} results</span><button @click="page++" :disabled="(page+1)*100>=rows.length">Next</button></div>
|
||||
<div class="bar flex" style="gap:6px;margin-top:12px"><button @click="preview" :disabled="!selected">Play</button><button @click="stop">Stop</button><button @click="apply(false)" :disabled="!selected">Apply to Selected / Insert</button><button @click="apply(true)" :disabled="!selected">Insert at Playhead</button></div>
|
||||
@@ -5578,7 +5575,88 @@
|
||||
soundLibraryDialog.show();
|
||||
}
|
||||
|
||||
function isBrokenFiguraSoundWrapper(callback) {
|
||||
// Figura Format 0.1.5 discards in_loop. Do not patch native or corrected implementations.
|
||||
return typeof callback === 'function' && /^function\s*\(\s*\)\s*\{\s*if\s*\(\s*Format\s*===\s*format\s*\)\s*return\s*;?\s*displayFrame\.call\(\s*this\s*\)\s*;?\s*\}$/.test(Function.prototype.toString.call(callback));
|
||||
}
|
||||
|
||||
function removeFiguraSoundMedia(media) {
|
||||
for (const list of [Timeline.playing_sounds, Timeline.paused_sounds]) {
|
||||
const index = list?.indexOf(media) ?? -1;
|
||||
if (index >= 0) list.splice(index, 1);
|
||||
}
|
||||
figuraSoundMedia.delete(media);
|
||||
}
|
||||
|
||||
function playFiguraCompatSounds(animator, inLoop, states) {
|
||||
const time = animator.animation?.time;
|
||||
if (!Number.isFinite(time)) return;
|
||||
const previous = states.get(animator);
|
||||
states.set(animator, { time, includeStart: false });
|
||||
if (!inLoop || animator.muted?.sound || !Timeline.playing) return;
|
||||
const wrapped = previous && time < previous.time;
|
||||
const rangeStart = Timeline.custom_range?.[0] || 0;
|
||||
const rangeEnd = Math.min(animator.animation.length || Infinity, Timeline.custom_range?.[1] || Infinity);
|
||||
for (const frame of animator.sound || []) {
|
||||
const crossed = !previous ? frame.time === time : wrapped
|
||||
? (frame.time > previous.time && frame.time <= rangeEnd) || (frame.time >= rangeStart && frame.time <= time)
|
||||
: frame.time <= time && (frame.time > previous.time || (previous.includeStart && frame.time === previous.time));
|
||||
const file = frame.data_points?.[0]?.file;
|
||||
if (!crossed || !file) continue;
|
||||
const existing = Timeline.playing_sounds.filter((media) => media.keyframe_id === frame.uuid);
|
||||
// A sound resumed by Blockbench's startPreviousSounds must not be played twice.
|
||||
if (existing.length && !wrapped) continue;
|
||||
for (const media of existing) { media.pause(); removeFiguraSoundMedia(media); }
|
||||
try {
|
||||
const media = new Audio(file);
|
||||
media.keyframe_id = frame.uuid;
|
||||
media.playbackRate = Math.max(0.1, Math.min(4, (Timeline.playback_speed || 100) / 100));
|
||||
media.volume = Math.max(0, Math.min(1, typeof settings === 'undefined' ? 1 : (settings.volume?.value ?? 100) / 100));
|
||||
media.onended = () => removeFiguraSoundMedia(media);
|
||||
Timeline.playing_sounds.push(media); figuraSoundMedia.add(media);
|
||||
media.play().catch((error) => { removeFiguraSoundMedia(media); console.warn('AnimationDirector sound preview failed:', error); });
|
||||
} catch (error) { console.warn('AnimationDirector sound preview failed:', error); }
|
||||
}
|
||||
}
|
||||
|
||||
function installFiguraSoundCompat() {
|
||||
if (typeof EffectAnimator === 'undefined') return;
|
||||
const original = EffectAnimator.prototype.displayFrame;
|
||||
if (!isBrokenFiguraSoundWrapper(original)) return;
|
||||
const states = new WeakMap();
|
||||
const wrapper = function (...args) {
|
||||
const result = original.apply(this, args);
|
||||
if (figuraSoundCompatEnabled && typeof Format !== 'undefined' && Format.id === 'geckolib_model') {
|
||||
playFiguraCompatSounds(this, args[0], states);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
figuraSoundPatches.push({ original, wrapper, states });
|
||||
EffectAnimator.prototype.displayFrame = wrapper;
|
||||
}
|
||||
|
||||
function beginFiguraSoundPlayback() {
|
||||
installFiguraSoundCompat();
|
||||
if (typeof Animation === 'undefined') return;
|
||||
for (const animation of Animation.all) {
|
||||
const effects = animation.animators?.effects;
|
||||
if (effects) for (const patch of figuraSoundPatches) {
|
||||
patch.states.set(effects, { time: animation.time, includeStart: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removeFiguraSoundCompat() {
|
||||
figuraSoundCompatEnabled = false;
|
||||
for (const media of [...figuraSoundMedia]) { media.pause(); removeFiguraSoundMedia(media); }
|
||||
for (const patch of figuraSoundPatches.slice().reverse()) {
|
||||
if (EffectAnimator.prototype.displayFrame === patch.wrapper) EffectAnimator.prototype.displayFrame = patch.original;
|
||||
}
|
||||
figuraSoundPatches.length = 0;
|
||||
}
|
||||
|
||||
function refreshSoundLibraryPanel() {
|
||||
installFiguraSoundCompat();
|
||||
if (typeof document !== 'undefined') {
|
||||
const panel = document.getElementById('panel_keyframe');
|
||||
if (panel && !document.getElementById('animationdirector_sound_library_button')) {
|
||||
@@ -5616,6 +5694,9 @@ You can import or export complete AFW animation IDs as one file containing all s
|
||||
});
|
||||
soundLibraryListener = refreshSoundLibraryPanel;
|
||||
Blockbench.on(SOUND_EVENTS.join(' '), soundLibraryListener);
|
||||
figuraSoundCompatEnabled = true;
|
||||
figuraSoundPlayListener = beginFiguraSoundPlayback;
|
||||
Blockbench.on('timeline_play', figuraSoundPlayListener);
|
||||
refreshSoundLibraryPanel();
|
||||
if (typeof Property === 'function' && typeof ModelProject !== 'undefined' && ModelProject?.properties) {
|
||||
if (!ModelProject.properties[ACTOR_NAMES_PROPERTY]) {
|
||||
@@ -5820,6 +5901,8 @@ You can import or export complete AFW animation IDs as one file containing all s
|
||||
MenuBar.update();
|
||||
},
|
||||
onunload() {
|
||||
Blockbench.removeListener('timeline_play', figuraSoundPlayListener);
|
||||
removeFiguraSoundCompat();
|
||||
for (const event of SOUND_EVENTS) Blockbench.removeListener(event, soundLibraryListener);
|
||||
if (typeof document !== 'undefined') document.getElementById('animationdirector_sound_library_button')?.remove();
|
||||
soundLibraryAction?.delete(); soundLibraryDialog?.delete(); soundPreviewProperty?.delete();
|
||||
|
||||
Reference in New Issue
Block a user