feat(audio): volume в проекте, DnD, параллельные плейлисты
Громкость треков сцены/игры сохраняется в проекте; порядок через DnD; несколько плейлистов сразу; автоимя из файла; стоп превью при запуске сессии. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -107,15 +107,30 @@ function clampAudioGain(v: number): number {
|
||||
return Math.max(0, Math.min(1, v));
|
||||
}
|
||||
|
||||
function readAudioGain(gains: Map<string, number>, assetId: string): number {
|
||||
return gains.get(assetId) ?? 1;
|
||||
function readAudioGain(gains: Map<string, number>, assetId: string, projectDefault = 1): number {
|
||||
return gains.get(assetId) ?? clampAudioGain(projectDefault);
|
||||
}
|
||||
|
||||
/** Применяет пользовательскую громкость; `factor` — для fade in/out (0…1). */
|
||||
function applyAudioGain(el: HTMLAudioElement, gains: Map<string, number>, assetId: string, factor = 1): void {
|
||||
function applyAudioGain(
|
||||
el: HTMLAudioElement,
|
||||
gains: Map<string, number>,
|
||||
assetId: string,
|
||||
factor = 1,
|
||||
projectDefault = 1,
|
||||
): void {
|
||||
if (!gains.has(assetId)) gains.set(assetId, clampAudioGain(projectDefault));
|
||||
el.volume = clampAudioGain(readAudioGain(gains, assetId) * factor);
|
||||
}
|
||||
|
||||
function seedAudioGainFromRef(
|
||||
gains: Map<string, number>,
|
||||
assetId: string,
|
||||
projectVolume: number | undefined,
|
||||
): void {
|
||||
if (!gains.has(assetId)) gains.set(assetId, clampAudioGain(projectVolume ?? 1));
|
||||
}
|
||||
|
||||
/** Файл из `app/renderer/public/molniya.mp3` — рядом с `control.html` в dev и в dist. */
|
||||
function lightningEffectSoundUrl(): string {
|
||||
return new URL('molniya.mp3', window.location.href).href;
|
||||
@@ -615,6 +630,7 @@ export function ControlApp() {
|
||||
void (async () => {
|
||||
const loaded: { ref: (typeof sceneAudioRefs)[number]; el: HTMLAudioElement }[] = [];
|
||||
for (const item of sceneAudioRefs) {
|
||||
seedAudioGainFromRef(sceneAudioGainRef.current, item.assetId, item.volume);
|
||||
const r = await api.invoke(ipcChannels.project.assetFileUrl, { assetId: item.assetId });
|
||||
if (sceneAudioLoadRunRef.current !== runId) return;
|
||||
if (!r.url) continue;
|
||||
@@ -622,7 +638,7 @@ export function ControlApp() {
|
||||
el.loop = item.loop;
|
||||
el.preload = 'auto';
|
||||
if (item.autoplay) el.volume = 0;
|
||||
else applyAudioGain(el, sceneAudioGainRef.current, item.assetId);
|
||||
else applyAudioGain(el, sceneAudioGainRef.current, item.assetId, 1, item.volume);
|
||||
sceneAudioMetaRef.current.set(item.assetId, { lastPlayError: null });
|
||||
el.addEventListener('play', () => setSceneAudioStateTick((x) => x + 1));
|
||||
el.addEventListener('pause', () => setSceneAudioStateTick((x) => x + 1));
|
||||
@@ -725,6 +741,7 @@ export function ControlApp() {
|
||||
void (async () => {
|
||||
const loaded: { ref: (typeof campaignAudioRefs)[number]; el: HTMLAudioElement }[] = [];
|
||||
for (const item of campaignAudioRefs) {
|
||||
seedAudioGainFromRef(campaignAudioGainRef.current, item.assetId, item.volume);
|
||||
const r = await api.invoke(ipcChannels.project.assetFileUrl, { assetId: item.assetId });
|
||||
if (campaignAudioLoadRunRef.current !== runId) return;
|
||||
if (!r.url) continue;
|
||||
@@ -732,7 +749,7 @@ export function ControlApp() {
|
||||
el.loop = item.loop;
|
||||
el.preload = 'auto';
|
||||
if (item.autoplay) el.volume = 0;
|
||||
else applyAudioGain(el, campaignAudioGainRef.current, item.assetId);
|
||||
else applyAudioGain(el, campaignAudioGainRef.current, item.assetId, 1, item.volume);
|
||||
campaignAudioMetaRef.current.set(item.assetId, { lastPlayError: null });
|
||||
el.addEventListener('play', () => setCampaignAudioStateTick((x) => x + 1));
|
||||
el.addEventListener('pause', () => setCampaignAudioStateTick((x) => x + 1));
|
||||
@@ -3111,7 +3128,7 @@ export function ControlApp() {
|
||||
statusLabel={st.label}
|
||||
{...(st.detail ? { statusDetail: st.detail } : {})}
|
||||
audioEl={el}
|
||||
initialGain={readAudioGain(sceneAudioGainRef.current, ref.assetId)}
|
||||
initialGain={readAudioGain(sceneAudioGainRef.current, ref.assetId, ref.volume)}
|
||||
gainMap={sceneAudioGainRef.current}
|
||||
playTitle={t('control.transportPlay')}
|
||||
playLabel={t('control.transportPlay')}
|
||||
@@ -3189,7 +3206,7 @@ export function ControlApp() {
|
||||
}
|
||||
: {})}
|
||||
audioEl={el}
|
||||
initialGain={readAudioGain(campaignAudioGainRef.current, ref.assetId)}
|
||||
initialGain={readAudioGain(campaignAudioGainRef.current, ref.assetId, ref.volume)}
|
||||
gainMap={campaignAudioGainRef.current}
|
||||
playTitle={
|
||||
!allowCampaignAudio ? t('control.pauseCampaignTitle') : t('control.transportPlay')
|
||||
|
||||
Reference in New Issue
Block a user