feat: add adaptive soundtrack mixer

This commit is contained in:
2026-09-04 15:57:16 +03:00
parent 6fce20a228
commit 4c7d46ba0e
5 changed files with 466 additions and 1 deletions

View File

@@ -1,11 +1,24 @@
import { describe, expect, it } from 'vitest';
import {
AFTERGLOW_LOOP_END_SAMPLE,
AFTERGLOW_LOOP_START_SAMPLE,
AFTERGLOW_TRANSITION_SECONDS,
DRIVE_LIFT_LOOP_SAMPLES,
DRIVE_LIFT_LOOP_SECONDS,
MAIN_DURATION_SAMPLES,
MAIN_DURATION_SECONDS,
MAIN_PHRASE_BARS,
MUSIC_BAR_SAMPLES,
MUSIC_BAR_SECONDS,
MUSIC_BEAT_SAMPLES,
MUSIC_BEAT_SECONDS,
INTRO_IN_END_SECONDS,
SOUNDTRACK_SAMPLE_RATE,
mainPhraseOffsetSeconds,
nextBeatTime,
nextSafeBarTime,
phaseAlignedLoopOffset,
resolveMainSchedule,
resolveInitialPlayback,
} from '@/audio/soundtrack-transport';
@@ -50,4 +63,44 @@ describe('soundtrack transport decisions', () => {
);
expect(earliestIntroExit).toBeCloseTo(anchorTime + MUSIC_BAR_SECONDS * 2, 8);
});
it('pins the short adaptive regions to whole samples', () => {
expect(MUSIC_BEAT_SAMPLES).toBe(33_488);
expect(MUSIC_BAR_SAMPLES).toBe(133_953);
expect(DRIVE_LIFT_LOOP_SAMPLES).toBe(535_814);
expect(AFTERGLOW_LOOP_START_SAMPLE).toBe(133_953);
expect(AFTERGLOW_LOOP_END_SAMPLE).toBe(1_205_581);
expect(MAIN_DURATION_SAMPLES).toBe(8_573_023);
});
it('derives one immutable main anchor and the final-bar afterglow handoff', () => {
expect(resolveMainSchedule({
startTime: 10,
offsetSeconds: mainPhraseOffsetSeconds(6),
bufferDuration: MAIN_DURATION_SECONDS,
})).toEqual({
sourceStartTime: 10,
sourceOffsetSeconds: mainPhraseOffsetSeconds(6),
musicalAnchorTime: 10 - mainPhraseOffsetSeconds(6),
sourceEndTime: 10 + MAIN_DURATION_SECONDS - mainPhraseOffsetSeconds(6),
afterglowStartTime: 10 + MAIN_DURATION_SECONDS - mainPhraseOffsetSeconds(6) - AFTERGLOW_TRANSITION_SECONDS,
});
});
it('aligns a late loop source and sustained entry to the master beat', () => {
expect(phaseAlignedLoopOffset(13, 10, DRIVE_LIFT_LOOP_SECONDS))
.toBeCloseTo(3, 8);
expect(nextBeatTime(11.45, 10)).toBeCloseTo(
10 + 3 * MUSIC_BEAT_SECONDS,
8,
);
});
it('keeps one-sample-rounded beat and bar boundaries on the current boundary', () => {
const oneSample = 1 / SOUNDTRACK_SAMPLE_RATE;
expect(nextBeatTime(MUSIC_BEAT_SECONDS + oneSample, 0))
.toBeCloseTo(MUSIC_BEAT_SECONDS, 8);
expect(nextSafeBarTime(MUSIC_BAR_SECONDS + oneSample, 0, 0))
.toBeCloseTo(MUSIC_BAR_SECONDS, 8);
});
});