mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-21 22:15:40 +00:00
feat(autodj): configurable overlap cap (auto or 2–30 s) (#1173)
This commit is contained in:
@@ -21,6 +21,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
* **Crossfade / AutoDJ:** both sides resample to the chosen rate; the output stream reopens when needed and the outgoing track rebuilds from cache so mixed 88.2 ↔ 44.1 kHz transitions no longer tear mid-fade.
|
* **Crossfade / AutoDJ:** both sides resample to the chosen rate; the output stream reopens when needed and the outgoing track rebuilds from cache so mixed 88.2 ↔ 44.1 kHz transitions no longer tear mid-fade.
|
||||||
* **Gapless:** the next track chains at the blend rate and the current track realigns when the stream Hz differs, instead of falling back to a hard cut.
|
* **Gapless:** the next track chains at the blend rate and the current track realigns when the stream Hz differs, instead of falling back to a hard cut.
|
||||||
|
|
||||||
|
### AutoDJ — configurable overlap cap
|
||||||
|
|
||||||
|
**By [@cucadmuh](https://github.com/cucadmuh), PR [#1173](https://github.com/Psychotoxical/psysonic/pull/1173)**
|
||||||
|
|
||||||
|
* **Settings → Audio → Track transitions → AutoDJ:** choose **Auto** (content-driven overlap, up to 12 s) or **Limit** (slider 2–30 s, default 15 s when enabled) to cap how long AutoDJ may overlap tracks.
|
||||||
|
* The cap applies to end-of-track planning, JS auto-advance, smooth skip, and Orbit transition sync; the audio engine accepts dynamic overlap overrides up to 30 s.
|
||||||
|
|
||||||
### Theme store — version numbers and an animated/static filter
|
### Theme store — version numbers and an animated/static filter
|
||||||
|
|
||||||
**By [@Psychotoxical](https://github.com/Psychotoxical), PR [#1104](https://github.com/Psychotoxical/psysonic/pull/1104)**
|
**By [@Psychotoxical](https://github.com/Psychotoxical), PR [#1104](https://github.com/Psychotoxical/psysonic/pull/1104)**
|
||||||
|
|||||||
@@ -247,9 +247,12 @@ pub async fn audio_play(
|
|||||||
state.crossfade_enabled.load(Ordering::Relaxed) && (!manual || manual_blend);
|
state.crossfade_enabled.load(Ordering::Relaxed) && (!manual || manual_blend);
|
||||||
// Per-transition override (dynamic crossfade) caps the fade for this swap;
|
// Per-transition override (dynamic crossfade) caps the fade for this swap;
|
||||||
// otherwise fall back to the global crossfade length. Both clamped the same.
|
// otherwise fall back to the global crossfade length. Both clamped the same.
|
||||||
let crossfade_secs_val = crossfade_secs_override
|
let crossfade_secs_val = if let Some(override_secs) = crossfade_secs_override {
|
||||||
.unwrap_or_else(|| f32::from_bits(state.crossfade_secs.load(Ordering::Relaxed)))
|
override_secs.clamp(0.5, 30.0)
|
||||||
.clamp(0.5, 12.0);
|
} else {
|
||||||
|
f32::from_bits(state.crossfade_secs.load(Ordering::Relaxed))
|
||||||
|
.clamp(0.5, 12.0)
|
||||||
|
};
|
||||||
|
|
||||||
// Measure how much audio Track A actually has left right now.
|
// Measure how much audio Track A actually has left right now.
|
||||||
// By the time audio_play is called, near_end_ticks (2×500ms) + IPC latency
|
// By the time audio_play is called, near_end_ticks (2×500ms) + IPC latency
|
||||||
|
|||||||
@@ -129,6 +129,9 @@ export interface OrbitTransitionSettings {
|
|||||||
crossfadeTrimSilence: boolean;
|
crossfadeTrimSilence: boolean;
|
||||||
autodjSmoothSkip: boolean;
|
autodjSmoothSkip: boolean;
|
||||||
gaplessEnabled: boolean;
|
gaplessEnabled: boolean;
|
||||||
|
/** Optional — absent on sessions hosted by builds before overlap-cap sync. */
|
||||||
|
autodjOverlapCapMode?: 'auto' | 'limit';
|
||||||
|
autodjOverlapCapSec?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OrbitSettings {
|
export interface OrbitSettings {
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ import React from 'react';
|
|||||||
import type { TFunction } from 'i18next';
|
import type { TFunction } from 'i18next';
|
||||||
import { useAuthStore } from '../../../store/authStore';
|
import { useAuthStore } from '../../../store/authStore';
|
||||||
import { useOrbitStore } from '../../../store/orbitStore';
|
import { useOrbitStore } from '../../../store/orbitStore';
|
||||||
|
import {
|
||||||
|
AUTODJ_OVERLAP_CAP_MAX_SEC,
|
||||||
|
AUTODJ_OVERLAP_CAP_MIN_SEC,
|
||||||
|
} from '../../../utils/playback/autodjOverlapCap';
|
||||||
import {
|
import {
|
||||||
getTransitionMode,
|
getTransitionMode,
|
||||||
setTransitionMode,
|
setTransitionMode,
|
||||||
@@ -21,8 +25,7 @@ interface Props {
|
|||||||
* transition-mode helper.
|
* transition-mode helper.
|
||||||
*
|
*
|
||||||
* Classic crossfade exposes the seconds slider; AutoDJ is content-driven and
|
* Classic crossfade exposes the seconds slider; AutoDJ is content-driven and
|
||||||
* has no duration to configure (just a short explainer + the smooth-skip
|
* exposes an optional overlap cap (auto vs manual limit).
|
||||||
* toggle).
|
|
||||||
*
|
*
|
||||||
* Rendered as its own top-level "Track transitions" category in the Audio tab,
|
* Rendered as its own top-level "Track transitions" category in the Audio tab,
|
||||||
* so the boxed `SettingsGroup` is title-less — the `SettingsSubSection` header
|
* so the boxed `SettingsGroup` is title-less — the `SettingsSubSection` header
|
||||||
@@ -89,6 +92,50 @@ export function TrackTransitionsBlock({ t }: Props) {
|
|||||||
<div style={{ paddingLeft: '1rem', fontSize: 12, color: 'var(--text-muted)', marginTop: '0.7rem' }}>
|
<div style={{ paddingLeft: '1rem', fontSize: 12, color: 'var(--text-muted)', marginTop: '0.7rem' }}>
|
||||||
{t('settings.autoDjDesc')}
|
{t('settings.autoDjDesc')}
|
||||||
</div>
|
</div>
|
||||||
|
<div style={{ paddingLeft: '1rem', marginTop: '0.9rem' }}>
|
||||||
|
<p className="settings-row-label" style={{ marginBottom: '0.45rem' }}>
|
||||||
|
{t('settings.autodjOverlapCapTitle')}
|
||||||
|
</p>
|
||||||
|
<p className="settings-row-desc" style={{ marginBottom: '0.6rem' }}>
|
||||||
|
{t('settings.autodjOverlapCapDesc')}
|
||||||
|
</p>
|
||||||
|
<div className="settings-segmented" style={{ marginBottom: auth.autodjOverlapCapMode === 'limit' ? '0.65rem' : 0 }}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`btn ${auth.autodjOverlapCapMode === 'auto' ? 'btn-primary' : 'btn-ghost'}`}
|
||||||
|
disabled={hostControlled}
|
||||||
|
onClick={() => auth.setAutodjOverlapCapMode('auto')}
|
||||||
|
>
|
||||||
|
{t('settings.autodjOverlapCapAuto')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`btn ${auth.autodjOverlapCapMode === 'limit' ? 'btn-primary' : 'btn-ghost'}`}
|
||||||
|
disabled={hostControlled}
|
||||||
|
onClick={() => auth.setAutodjOverlapCapMode('limit')}
|
||||||
|
>
|
||||||
|
{t('settings.autodjOverlapCapLimit')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{auth.autodjOverlapCapMode === 'limit' && (
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', flexWrap: 'wrap' }}>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min={AUTODJ_OVERLAP_CAP_MIN_SEC}
|
||||||
|
max={AUTODJ_OVERLAP_CAP_MAX_SEC}
|
||||||
|
step={1}
|
||||||
|
value={auth.autodjOverlapCapSec}
|
||||||
|
disabled={hostControlled}
|
||||||
|
onChange={e => auth.setAutodjOverlapCapSec(parseInt(e.target.value, 10))}
|
||||||
|
style={{ flex: 1, minWidth: 80, maxWidth: 200 }}
|
||||||
|
id="autodj-overlap-cap-slider"
|
||||||
|
/>
|
||||||
|
<span style={{ fontSize: 13, color: 'var(--text-secondary)', minWidth: 48 }}>
|
||||||
|
{t('settings.autodjOverlapCapSecs', { n: auth.autodjOverlapCapSec })}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div style={{ paddingLeft: '1rem', marginTop: '0.7rem' }}>
|
<div style={{ paddingLeft: '1rem', marginTop: '0.7rem' }}>
|
||||||
<SettingsToggle
|
<SettingsToggle
|
||||||
label={t('settings.autodjSmoothSkip')}
|
label={t('settings.autodjSmoothSkip')}
|
||||||
|
|||||||
@@ -175,6 +175,7 @@ const CONTRIBUTOR_ENTRIES = [
|
|||||||
'Custom HTTP headers for reverse-proxy gates — per-server header editor, TS/Rust registry sync, full playback/sync/cover path coverage, log redaction (PR #1156)',
|
'Custom HTTP headers for reverse-proxy gates — per-server header editor, TS/Rust registry sync, full playback/sync/cover path coverage, log redaction (PR #1156)',
|
||||||
'CI: ESLint strict workflow and path-aware ci-ok merge gate (PR #1170)',
|
'CI: ESLint strict workflow and path-aware ci-ok merge gate (PR #1170)',
|
||||||
'Hi-Res transition blend rate — configurable 44.1/88.2/96 kHz resampling for crossfade, AutoDJ, and gapless when adjacent tracks differ in sample rate (PR #1171)',
|
'Hi-Res transition blend rate — configurable 44.1/88.2/96 kHz resampling for crossfade, AutoDJ, and gapless when adjacent tracks differ in sample rate (PR #1171)',
|
||||||
|
'AutoDJ overlap cap — Auto (12 s content cap) or Limit (2–30 s slider, default 15 s) in track-transition settings; Orbit sync + engine override up to 30 s (PR #1173)',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -608,6 +608,11 @@ export const settings = {
|
|||||||
autoDjDesc: 'Keine feste Dauer — AutoDJ richtet sich nach dem tatsächlichen Klang und überlappt echte Aus- und Einblendungen statt einer festen Sekundenzahl. Für zuverlässige Ergebnisse den Hot-Playback-Cache aktivieren.',
|
autoDjDesc: 'Keine feste Dauer — AutoDJ richtet sich nach dem tatsächlichen Klang und überlappt echte Aus- und Einblendungen statt einer festen Sekundenzahl. Für zuverlässige Ergebnisse den Hot-Playback-Cache aktivieren.',
|
||||||
autodjSmoothSkip: 'Weicher Skip',
|
autodjSmoothSkip: 'Weicher Skip',
|
||||||
autodjSmoothSkipDesc: 'Beim Wechsel zum nächsten oder vorherigen Titel während der Wiedergabe blendet der aktuelle Song kurz aus, statt abrupt abgeschnitten zu werden.',
|
autodjSmoothSkipDesc: 'Beim Wechsel zum nächsten oder vorherigen Titel während der Wiedergabe blendet der aktuelle Song kurz aus, statt abrupt abgeschnitten zu werden.',
|
||||||
|
autodjOverlapCapTitle: 'Überlappungslimit',
|
||||||
|
autodjOverlapCapDesc: 'Begrenzt, wie lange AutoDJ Titel überlappen darf. Auto folgt der Inhaltsanalyse (bis 12 s). Limit setzt Ihr eigenes Maximum.',
|
||||||
|
autodjOverlapCapAuto: 'Auto',
|
||||||
|
autodjOverlapCapLimit: 'Limit',
|
||||||
|
autodjOverlapCapSecs: '{{n}} s',
|
||||||
gapless: 'Nahtlose Wiedergabe',
|
gapless: 'Nahtlose Wiedergabe',
|
||||||
transitionsTitle: 'Übergänge zwischen Tracks',
|
transitionsTitle: 'Übergänge zwischen Tracks',
|
||||||
transitionsDesc: 'Wie aufeinanderfolgende Tracks ineinander übergehen. Es kann immer nur ein Modus aktiv sein.',
|
transitionsDesc: 'Wie aufeinanderfolgende Tracks ineinander übergehen. Es kann immer nur ein Modus aktiv sein.',
|
||||||
|
|||||||
@@ -675,6 +675,11 @@ export const settings = {
|
|||||||
autoDjDesc: 'No fixed duration — AutoDJ follows the actual audio, blending real fade-outs and intros instead of a set number of seconds. For reliable results, enable the Hot playback cache.',
|
autoDjDesc: 'No fixed duration — AutoDJ follows the actual audio, blending real fade-outs and intros instead of a set number of seconds. For reliable results, enable the Hot playback cache.',
|
||||||
autodjSmoothSkip: 'Smooth skip',
|
autodjSmoothSkip: 'Smooth skip',
|
||||||
autodjSmoothSkipDesc: 'When you skip to the next or previous track while playing, blend into it with the same AutoDJ rules (overlap length, intro trim) from your current position instead of an abrupt cut. Works best when the next track is already buffered.',
|
autodjSmoothSkipDesc: 'When you skip to the next or previous track while playing, blend into it with the same AutoDJ rules (overlap length, intro trim) from your current position instead of an abrupt cut. Works best when the next track is already buffered.',
|
||||||
|
autodjOverlapCapTitle: 'Overlap limit',
|
||||||
|
autodjOverlapCapDesc: 'Caps how long AutoDJ may overlap tracks. Auto follows content analysis (up to 12 s). Limit sets your own maximum.',
|
||||||
|
autodjOverlapCapAuto: 'Auto',
|
||||||
|
autodjOverlapCapLimit: 'Limit',
|
||||||
|
autodjOverlapCapSecs: '{{n}} s',
|
||||||
gapless: 'Gapless Playback',
|
gapless: 'Gapless Playback',
|
||||||
transitionsTitle: 'Track transitions',
|
transitionsTitle: 'Track transitions',
|
||||||
transitionsDesc: 'How consecutive tracks blend together. Only one mode can be active at a time.',
|
transitionsDesc: 'How consecutive tracks blend together. Only one mode can be active at a time.',
|
||||||
|
|||||||
@@ -607,6 +607,11 @@ export const settings = {
|
|||||||
autoDjDesc: 'Sin duración fija: AutoDJ se adapta al audio real y se solapa con los fundidos e introducciones reales en lugar de un número fijo de segundos. Para resultados fiables, activa la Caché de reproducción activa.',
|
autoDjDesc: 'Sin duración fija: AutoDJ se adapta al audio real y se solapa con los fundidos e introducciones reales en lugar de un número fijo de segundos. Para resultados fiables, activa la Caché de reproducción activa.',
|
||||||
autodjSmoothSkip: 'Salto suave',
|
autodjSmoothSkip: 'Salto suave',
|
||||||
autodjSmoothSkipDesc: 'Al pasar a la siguiente o anterior pista mientras suena una canción, esta se desvanece brevemente en lugar de cortarse de golpe.',
|
autodjSmoothSkipDesc: 'Al pasar a la siguiente o anterior pista mientras suena una canción, esta se desvanece brevemente en lugar de cortarse de golpe.',
|
||||||
|
autodjOverlapCapTitle: 'Límite de solapamiento',
|
||||||
|
autodjOverlapCapDesc: 'Limita cuánto pueden solaparse las pistas con AutoDJ. Auto sigue el análisis del contenido (hasta 12 s). Límite fija su propio máximo.',
|
||||||
|
autodjOverlapCapAuto: 'Auto',
|
||||||
|
autodjOverlapCapLimit: 'Límite',
|
||||||
|
autodjOverlapCapSecs: '{{n}} s',
|
||||||
gapless: 'Reproducción Gapless',
|
gapless: 'Reproducción Gapless',
|
||||||
transitionsTitle: 'Transiciones entre pistas',
|
transitionsTitle: 'Transiciones entre pistas',
|
||||||
transitionsDesc: 'Cómo se enlazan las pistas consecutivas. Solo un modo puede estar activo a la vez.',
|
transitionsDesc: 'Cómo se enlazan las pistas consecutivas. Solo un modo puede estar activo a la vez.',
|
||||||
|
|||||||
@@ -595,6 +595,11 @@ export const settings = {
|
|||||||
autoDjDesc: 'Sans durée fixe : AutoDJ suit l’audio réel et se superpose aux vrais fondus et intros plutôt qu’à un nombre fixe de secondes. Pour un résultat fiable, activez le Cache de lecture à chaud.',
|
autoDjDesc: 'Sans durée fixe : AutoDJ suit l’audio réel et se superpose aux vrais fondus et intros plutôt qu’à un nombre fixe de secondes. Pour un résultat fiable, activez le Cache de lecture à chaud.',
|
||||||
autodjSmoothSkip: 'Passage en fondu',
|
autodjSmoothSkip: 'Passage en fondu',
|
||||||
autodjSmoothSkipDesc: 'Lors d’un passage à la piste suivante ou précédente pendant la lecture, la piste en cours s’estompe brièvement au lieu d’être coupée net.',
|
autodjSmoothSkipDesc: 'Lors d’un passage à la piste suivante ou précédente pendant la lecture, la piste en cours s’estompe brièvement au lieu d’être coupée net.',
|
||||||
|
autodjOverlapCapTitle: 'Limite de chevauchement',
|
||||||
|
autodjOverlapCapDesc: 'Limite la durée de chevauchement AutoDJ. Auto suit l’analyse du contenu (jusqu’à 12 s). Limite fixe votre propre maximum.',
|
||||||
|
autodjOverlapCapAuto: 'Auto',
|
||||||
|
autodjOverlapCapLimit: 'Limite',
|
||||||
|
autodjOverlapCapSecs: '{{n}} s',
|
||||||
gapless: 'Lecture sans blanc',
|
gapless: 'Lecture sans blanc',
|
||||||
transitionsTitle: 'Transitions entre pistes',
|
transitionsTitle: 'Transitions entre pistes',
|
||||||
transitionsDesc: 'Comment les pistes consécutives s’enchaînent. Un seul mode peut être actif à la fois.',
|
transitionsDesc: 'Comment les pistes consécutives s’enchaînent. Un seul mode peut être actif à la fois.',
|
||||||
|
|||||||
@@ -675,6 +675,11 @@ export const settings = {
|
|||||||
autoDjDesc: 'Nincs fix időtartam — az AutoDJ a tényleges hangot követi, valós fade-outokat és intrókat keverve egy beállított másodpercszám helyett. A megbízható eredményekért engedélyezd az aktív lejátszási gyorsítótárat.',
|
autoDjDesc: 'Nincs fix időtartam — az AutoDJ a tényleges hangot követi, valós fade-outokat és intrókat keverve egy beállított másodpercszám helyett. A megbízható eredményekért engedélyezd az aktív lejátszási gyorsítótárat.',
|
||||||
autodjSmoothSkip: 'Sima átugrás',
|
autodjSmoothSkip: 'Sima átugrás',
|
||||||
autodjSmoothSkipDesc: 'Amikor lejátszás közben a következő vagy előző számra ugrasz, ugyanazokkal az AutoDJ-szabályokkal (átfedéshossz, intró-vágás) tűnik át bele a jelenlegi pozíciódból, éles vágás helyett. Akkor működik a legjobban, ha a következő szám már pufferelt.',
|
autodjSmoothSkipDesc: 'Amikor lejátszás közben a következő vagy előző számra ugrasz, ugyanazokkal az AutoDJ-szabályokkal (átfedéshossz, intró-vágás) tűnik át bele a jelenlegi pozíciódból, éles vágás helyett. Akkor működik a legjobban, ha a következő szám már pufferelt.',
|
||||||
|
autodjOverlapCapTitle: 'Átfedési limit',
|
||||||
|
autodjOverlapCapDesc: 'Korlátozza, meddig fedhetik egymást a számok AutoDJ mellett. Auto a tartalomelemzést követi (max. 12 s). Limit saját felső határt ad.',
|
||||||
|
autodjOverlapCapAuto: 'Auto',
|
||||||
|
autodjOverlapCapLimit: 'Limit',
|
||||||
|
autodjOverlapCapSecs: '{{n}} mp',
|
||||||
gapless: 'Szünetmentes lejátszás',
|
gapless: 'Szünetmentes lejátszás',
|
||||||
transitionsTitle: 'Számátmenetek',
|
transitionsTitle: 'Számátmenetek',
|
||||||
transitionsDesc: 'Hogyan tűnik át egymásba az egymást követő számok. Egyszerre csak egy mód lehet aktív.',
|
transitionsDesc: 'Hogyan tűnik át egymásba az egymást követő számok. Egyszerre csak egy mód lehet aktív.',
|
||||||
|
|||||||
@@ -669,6 +669,11 @@ export const settings = {
|
|||||||
autoDjDesc: '固定時間ではありません。AutoDJ は実際の音声に追従し、秒数固定ではなく本物のフェードアウトとイントロをブレンドします。安定した結果にはホット再生キャッシュを有効にしてください。',
|
autoDjDesc: '固定時間ではありません。AutoDJ は実際の音声に追従し、秒数固定ではなく本物のフェードアウトとイントロをブレンドします。安定した結果にはホット再生キャッシュを有効にしてください。',
|
||||||
autodjSmoothSkip: 'スムーズスキップ',
|
autodjSmoothSkip: 'スムーズスキップ',
|
||||||
autodjSmoothSkipDesc: '再生中に次または前のトラックへスキップすると、突然切り替えず、現在位置から同じ AutoDJ ルール (重なり長、イントロトリム) でブレンドします。次のトラックがすでにバッファ済みだと最も安定します。',
|
autodjSmoothSkipDesc: '再生中に次または前のトラックへスキップすると、突然切り替えず、現在位置から同じ AutoDJ ルール (重なり長、イントロトリム) でブレンドします。次のトラックがすでにバッファ済みだと最も安定します。',
|
||||||
|
autodjOverlapCapTitle: '重なり上限',
|
||||||
|
autodjOverlapCapDesc: 'AutoDJ の重なり時間の上限です。自動はコンテンツ分析に従います (最大 12 秒)。上限は自分で設定します。',
|
||||||
|
autodjOverlapCapAuto: '自動',
|
||||||
|
autodjOverlapCapLimit: '上限',
|
||||||
|
autodjOverlapCapSecs: '{{n}} 秒',
|
||||||
gapless: 'ギャップレス再生',
|
gapless: 'ギャップレス再生',
|
||||||
transitionsTitle: 'トラック遷移',
|
transitionsTitle: 'トラック遷移',
|
||||||
transitionsDesc: '連続するトラックのつなぎ方です。一度に有効にできるモードは 1 つだけです。',
|
transitionsDesc: '連続するトラックのつなぎ方です。一度に有効にできるモードは 1 つだけです。',
|
||||||
|
|||||||
@@ -594,6 +594,11 @@ export const settings = {
|
|||||||
autoDjDesc: 'Ingen fast varighet — AutoDJ følger selve lyden og overlapper ekte inn-/uttoninger i stedet for et fast antall sekunder. For pålitelige resultater bør du aktivere Varm avspillingsbuffer.',
|
autoDjDesc: 'Ingen fast varighet — AutoDJ følger selve lyden og overlapper ekte inn-/uttoninger i stedet for et fast antall sekunder. For pålitelige resultater bør du aktivere Varm avspillingsbuffer.',
|
||||||
autodjSmoothSkip: 'Myk hopp',
|
autodjSmoothSkip: 'Myk hopp',
|
||||||
autodjSmoothSkipDesc: 'Når du hopper til neste eller forrige spor under avspilling, toner det pågående sporet kort ut i stedet for å kuttes brått.',
|
autodjSmoothSkipDesc: 'Når du hopper til neste eller forrige spor under avspilling, toner det pågående sporet kort ut i stedet for å kuttes brått.',
|
||||||
|
autodjOverlapCapTitle: 'Overlappgrense',
|
||||||
|
autodjOverlapCapDesc: 'Begrenser hvor lenge AutoDJ kan overlappe spor. Auto følger innholdsanalyse (opptil 12 s). Grense setter ditt eget maksimum.',
|
||||||
|
autodjOverlapCapAuto: 'Auto',
|
||||||
|
autodjOverlapCapLimit: 'Grense',
|
||||||
|
autodjOverlapCapSecs: '{{n}} s',
|
||||||
gapless: 'Gapless avspilling',
|
gapless: 'Gapless avspilling',
|
||||||
transitionsTitle: 'Overganger mellom spor',
|
transitionsTitle: 'Overganger mellom spor',
|
||||||
transitionsDesc: 'Hvordan etterfølgende spor går over i hverandre. Bare én modus kan være aktiv om gangen.',
|
transitionsDesc: 'Hvordan etterfølgende spor går over i hverandre. Bare én modus kan være aktiv om gangen.',
|
||||||
|
|||||||
@@ -595,6 +595,11 @@ export const settings = {
|
|||||||
autoDjDesc: 'Geen vaste duur — AutoDJ volgt de werkelijke audio en overlapt echte fades en intro’s in plaats van een vast aantal seconden. Schakel voor betrouwbare resultaten de Warme afspeelcache in.',
|
autoDjDesc: 'Geen vaste duur — AutoDJ volgt de werkelijke audio en overlapt echte fades en intro’s in plaats van een vast aantal seconden. Schakel voor betrouwbare resultaten de Warme afspeelcache in.',
|
||||||
autodjSmoothSkip: 'Zacht overslaan',
|
autodjSmoothSkip: 'Zacht overslaan',
|
||||||
autodjSmoothSkipDesc: 'Bij overslaan naar het volgende of vorige nummer tijdens het afspelen fadet het huidige nummer kort uit in plaats van abrupt te stoppen.',
|
autodjSmoothSkipDesc: 'Bij overslaan naar het volgende of vorige nummer tijdens het afspelen fadet het huidige nummer kort uit in plaats van abrupt te stoppen.',
|
||||||
|
autodjOverlapCapTitle: 'Overlaplimiet',
|
||||||
|
autodjOverlapCapDesc: 'Beperkt hoe lang AutoDJ nummers mag overlappen. Auto volgt inhoudsanalyse (tot 12 s). Limiet stelt uw eigen maximum in.',
|
||||||
|
autodjOverlapCapAuto: 'Auto',
|
||||||
|
autodjOverlapCapLimit: 'Limiet',
|
||||||
|
autodjOverlapCapSecs: '{{n}} s',
|
||||||
gapless: 'Naadloos afspelen',
|
gapless: 'Naadloos afspelen',
|
||||||
transitionsTitle: 'Overgangen tussen nummers',
|
transitionsTitle: 'Overgangen tussen nummers',
|
||||||
transitionsDesc: 'Hoe opeenvolgende nummers in elkaar overvloeien. Er kan maar één modus tegelijk actief zijn.',
|
transitionsDesc: 'Hoe opeenvolgende nummers in elkaar overvloeien. Er kan maar één modus tegelijk actief zijn.',
|
||||||
|
|||||||
@@ -610,6 +610,11 @@ export const settings = {
|
|||||||
autoDjDesc: 'Fără durată fixă — AutoDJ urmează sunetul real și se suprapune peste estompările și introducerile reale, în loc de un număr fix de secunde. Pentru rezultate fiabile, activează Cache-ul hot playback.',
|
autoDjDesc: 'Fără durată fixă — AutoDJ urmează sunetul real și se suprapune peste estompările și introducerile reale, în loc de un număr fix de secunde. Pentru rezultate fiabile, activează Cache-ul hot playback.',
|
||||||
autodjSmoothSkip: 'Salt lin',
|
autodjSmoothSkip: 'Salt lin',
|
||||||
autodjSmoothSkipDesc: 'La trecerea la piesa următoare sau anterioară în timpul redării, piesa curentă se estompează scurt în loc să fie tăiată brusc.',
|
autodjSmoothSkipDesc: 'La trecerea la piesa următoare sau anterioară în timpul redării, piesa curentă se estompează scurt în loc să fie tăiată brusc.',
|
||||||
|
autodjOverlapCapTitle: 'Limită suprapunere',
|
||||||
|
autodjOverlapCapDesc: 'Limitează cât pot suprapune piesele cu AutoDJ. Auto urmează analiza conținutului (până la 12 s). Limită setează maximul dvs.',
|
||||||
|
autodjOverlapCapAuto: 'Auto',
|
||||||
|
autodjOverlapCapLimit: 'Limită',
|
||||||
|
autodjOverlapCapSecs: '{{n}} s',
|
||||||
gapless: 'Playback Gapless',
|
gapless: 'Playback Gapless',
|
||||||
transitionsTitle: 'Tranziții între piese',
|
transitionsTitle: 'Tranziții între piese',
|
||||||
transitionsDesc: 'Cum se îmbină piesele consecutive. Doar un mod poate fi activ la un moment dat.',
|
transitionsDesc: 'Cum se îmbină piesele consecutive. Doar un mod poate fi activ la un moment dat.',
|
||||||
|
|||||||
@@ -695,6 +695,11 @@ export const settings = {
|
|||||||
autoDjDesc: 'Без фиксированной длительности — AutoDJ подстраивается под само звучание, накладываясь на реальные затухания и вступления, а не на заданное число секунд. Для надёжной работы включите «Горячий кэш воспроизведения».',
|
autoDjDesc: 'Без фиксированной длительности — AutoDJ подстраивается под само звучание, накладываясь на реальные затухания и вступления, а не на заданное число секунд. Для надёжной работы включите «Горячий кэш воспроизведения».',
|
||||||
autodjSmoothSkip: 'Плавный пропуск',
|
autodjSmoothSkip: 'Плавный пропуск',
|
||||||
autodjSmoothSkipDesc: 'При переходе на следующий или предыдущий трек во время воспроизведения склеивает его по тем же правилам AutoDJ (длина наложения, обрезка вступления) с текущей позиции, а не резким обрывом. Надёжнее, когда следующий трек уже в буфере.',
|
autodjSmoothSkipDesc: 'При переходе на следующий или предыдущий трек во время воспроизведения склеивает его по тем же правилам AutoDJ (длина наложения, обрезка вступления) с текущей позиции, а не резким обрывом. Надёжнее, когда следующий трек уже в буфере.',
|
||||||
|
autodjOverlapCapTitle: 'Лимит наложения',
|
||||||
|
autodjOverlapCapDesc: 'Ограничивает максимальную длительность наложения AutoDJ. Авто — по анализу контента (до 12 с). Лимит — ваш верхний предел.',
|
||||||
|
autodjOverlapCapAuto: 'Авто',
|
||||||
|
autodjOverlapCapLimit: 'Лимит',
|
||||||
|
autodjOverlapCapSecs: '{{n}} с',
|
||||||
gapless: 'Без пауз между треками',
|
gapless: 'Без пауз между треками',
|
||||||
transitionsTitle: 'Переходы между треками',
|
transitionsTitle: 'Переходы между треками',
|
||||||
transitionsDesc: 'Как соединяются идущие подряд треки. Одновременно может быть активен только один режим.',
|
transitionsDesc: 'Как соединяются идущие подряд треки. Одновременно может быть активен только один режим.',
|
||||||
|
|||||||
@@ -594,6 +594,11 @@ export const settings = {
|
|||||||
autoDjDesc: '没有固定时长——AutoDJ 跟随实际音频,叠加在真实的淡入淡出上,而不是固定的秒数。为获得稳定效果,请启用「热播放缓存」。',
|
autoDjDesc: '没有固定时长——AutoDJ 跟随实际音频,叠加在真实的淡入淡出上,而不是固定的秒数。为获得稳定效果,请启用「热播放缓存」。',
|
||||||
autodjSmoothSkip: '平滑跳过',
|
autodjSmoothSkip: '平滑跳过',
|
||||||
autodjSmoothSkipDesc: '播放中跳到下一首或上一首时,当前歌曲会短暂淡出,而不是突然切断。',
|
autodjSmoothSkipDesc: '播放中跳到下一首或上一首时,当前歌曲会短暂淡出,而不是突然切断。',
|
||||||
|
autodjOverlapCapTitle: '重叠上限',
|
||||||
|
autodjOverlapCapDesc: '限制 AutoDJ 轨道重叠的最长时间。自动模式按内容分析(最多 12 秒)。上限由您自行设定。',
|
||||||
|
autodjOverlapCapAuto: '自动',
|
||||||
|
autodjOverlapCapLimit: '上限',
|
||||||
|
autodjOverlapCapSecs: '{{n}} 秒',
|
||||||
gapless: '无缝播放',
|
gapless: '无缝播放',
|
||||||
transitionsTitle: '曲目间过渡',
|
transitionsTitle: '曲目间过渡',
|
||||||
transitionsDesc: '连续曲目之间如何衔接。同一时间只能启用一种模式。',
|
transitionsDesc: '连续曲目之间如何衔接。同一时间只能启用一种模式。',
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ import {
|
|||||||
} from './seekTargetState';
|
} from './seekTargetState';
|
||||||
import { refreshWaveformForTrack } from './waveformRefresh';
|
import { refreshWaveformForTrack } from './waveformRefresh';
|
||||||
import { analyzeBoundary, computeWaveformSilence } from '../utils/waveform/waveformSilence';
|
import { analyzeBoundary, computeWaveformSilence } from '../utils/waveform/waveformSilence';
|
||||||
|
import { autodjMaxOverlapCapSec } from '../utils/playback/autodjOverlapCap';
|
||||||
import {
|
import {
|
||||||
autodjJsTriggerAtSec,
|
autodjJsTriggerAtSec,
|
||||||
clampCrossfadeSecs,
|
clampCrossfadeSecs,
|
||||||
@@ -307,7 +308,8 @@ export function handleAudioProgress(
|
|||||||
}
|
}
|
||||||
if (shouldJsDriveAutodjTransition(curTrailSilenceSec, contentOverlap, cf, aRidesOwnFade)) {
|
if (shouldJsDriveAutodjTransition(curTrailSilenceSec, contentOverlap, cf, aRidesOwnFade)) {
|
||||||
autodjSuppressWant = true;
|
autodjSuppressWant = true;
|
||||||
const { overlapSec, outgoingFadeSec } = computeAutodjJsOverlap(contentOverlap, aRidesOwnFade);
|
const maxCapSec = autodjMaxOverlapCapSec(useAuthStore.getState());
|
||||||
|
const { overlapSec, outgoingFadeSec } = computeAutodjJsOverlap(contentOverlap, aRidesOwnFade, maxCapSec);
|
||||||
const triggerAt = autodjJsTriggerAtSec(dur, curTrailSilenceSec, overlapSec);
|
const triggerAt = autodjJsTriggerAtSec(dur, curTrailSilenceSec, overlapSec);
|
||||||
const gen = getPlayGeneration();
|
const gen = getPlayGeneration();
|
||||||
// Readiness gate: only advance when B's audio is actually available (RAM
|
// Readiness gate: only advance when B's audio is actually available (RAM
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import { clampStoredLoudnessPreAnalysisAttenuationRefDb } from '../utils/audio/loudnessPreAnalysisSlider';
|
import { clampStoredLoudnessPreAnalysisAttenuationRefDb } from '../utils/audio/loudnessPreAnalysisSlider';
|
||||||
|
import {
|
||||||
|
sanitizeAutodjOverlapCapMode,
|
||||||
|
sanitizeAutodjOverlapCapSec,
|
||||||
|
} from '../utils/playback/autodjOverlapCap';
|
||||||
import { DEFAULT_LOUDNESS_PRE_ANALYSIS_ATTENUATION_DB } from './authStoreDefaults';
|
import { DEFAULT_LOUDNESS_PRE_ANALYSIS_ATTENUATION_DB } from './authStoreDefaults';
|
||||||
import { usePlayerStore } from './playerStore';
|
import { usePlayerStore } from './playerStore';
|
||||||
import type { AuthState } from './authStoreTypes';
|
import type { AuthState } from './authStoreTypes';
|
||||||
@@ -29,6 +33,8 @@ export function createAudioSettingsActions(set: SetState): Pick<
|
|||||||
| 'setCrossfadeSecs'
|
| 'setCrossfadeSecs'
|
||||||
| 'setCrossfadeTrimSilence'
|
| 'setCrossfadeTrimSilence'
|
||||||
| 'setAutodjSmoothSkip'
|
| 'setAutodjSmoothSkip'
|
||||||
|
| 'setAutodjOverlapCapMode'
|
||||||
|
| 'setAutodjOverlapCapSec'
|
||||||
| 'setGaplessEnabled'
|
| 'setGaplessEnabled'
|
||||||
| 'setEnableHiRes'
|
| 'setEnableHiRes'
|
||||||
| 'setHiResCrossfadeResampleHz'
|
| 'setHiResCrossfadeResampleHz'
|
||||||
@@ -72,6 +78,8 @@ export function createAudioSettingsActions(set: SetState): Pick<
|
|||||||
setCrossfadeSecs: (v) => set({ crossfadeSecs: v }),
|
setCrossfadeSecs: (v) => set({ crossfadeSecs: v }),
|
||||||
setCrossfadeTrimSilence: (v) => set({ crossfadeTrimSilence: v }),
|
setCrossfadeTrimSilence: (v) => set({ crossfadeTrimSilence: v }),
|
||||||
setAutodjSmoothSkip: (v) => set({ autodjSmoothSkip: v }),
|
setAutodjSmoothSkip: (v) => set({ autodjSmoothSkip: v }),
|
||||||
|
setAutodjOverlapCapMode: (v) => set({ autodjOverlapCapMode: sanitizeAutodjOverlapCapMode(v) }),
|
||||||
|
setAutodjOverlapCapSec: (v) => set({ autodjOverlapCapSec: sanitizeAutodjOverlapCapSec(v) }),
|
||||||
setGaplessEnabled: (v) => set({ gaplessEnabled: v }),
|
setGaplessEnabled: (v) => set({ gaplessEnabled: v }),
|
||||||
setEnableHiRes: (v) => set({ enableHiRes: v }),
|
setEnableHiRes: (v) => set({ enableHiRes: v }),
|
||||||
setHiResCrossfadeResampleHz: (v) => set({ hiResCrossfadeResampleHz: v }),
|
setHiResCrossfadeResampleHz: (v) => set({ hiResCrossfadeResampleHz: v }),
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ export const useAuthStore = create<AuthState>()(
|
|||||||
crossfadeSecs: 3,
|
crossfadeSecs: 3,
|
||||||
crossfadeTrimSilence: false,
|
crossfadeTrimSilence: false,
|
||||||
autodjSmoothSkip: true,
|
autodjSmoothSkip: true,
|
||||||
|
autodjOverlapCapMode: 'auto',
|
||||||
|
autodjOverlapCapSec: 15,
|
||||||
gaplessEnabled: false,
|
gaplessEnabled: false,
|
||||||
trackPreviewsEnabled: true,
|
trackPreviewsEnabled: true,
|
||||||
trackPreviewLocations: { ...DEFAULT_TRACK_PREVIEW_LOCATIONS },
|
trackPreviewLocations: { ...DEFAULT_TRACK_PREVIEW_LOCATIONS },
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { IS_LINUX } from '../utils/platform';
|
import { IS_LINUX } from '../utils/platform';
|
||||||
import { sanitizeHiResCrossfadeResampleHz } from '../utils/audio/hiResCrossfadeResample';
|
import { sanitizeHiResCrossfadeResampleHz } from '../utils/audio/hiResCrossfadeResample';
|
||||||
|
import {
|
||||||
|
sanitizeAutodjOverlapCapMode,
|
||||||
|
sanitizeAutodjOverlapCapSec,
|
||||||
|
} from '../utils/playback/autodjOverlapCap';
|
||||||
import {
|
import {
|
||||||
LOUDNESS_PRE_ANALYSIS_REF_TARGET_LUFS,
|
LOUDNESS_PRE_ANALYSIS_REF_TARGET_LUFS,
|
||||||
clampStoredLoudnessPreAnalysisAttenuationRefDb,
|
clampStoredLoudnessPreAnalysisAttenuationRefDb,
|
||||||
@@ -248,6 +252,12 @@ export function computeAuthStoreRehydration(state: AuthState): Partial<AuthState
|
|||||||
hiResCrossfadeResampleHz: sanitizeHiResCrossfadeResampleHz(
|
hiResCrossfadeResampleHz: sanitizeHiResCrossfadeResampleHz(
|
||||||
(state as { hiResCrossfadeResampleHz?: unknown }).hiResCrossfadeResampleHz,
|
(state as { hiResCrossfadeResampleHz?: unknown }).hiResCrossfadeResampleHz,
|
||||||
),
|
),
|
||||||
|
autodjOverlapCapMode: sanitizeAutodjOverlapCapMode(
|
||||||
|
(state as { autodjOverlapCapMode?: unknown }).autodjOverlapCapMode,
|
||||||
|
),
|
||||||
|
autodjOverlapCapSec: sanitizeAutodjOverlapCapSec(
|
||||||
|
(state as { autodjOverlapCapSec?: unknown }).autodjOverlapCapSec,
|
||||||
|
),
|
||||||
...lyricsSourcesMigrated,
|
...lyricsSourcesMigrated,
|
||||||
...youLyPlusMigrated,
|
...youLyPlusMigrated,
|
||||||
...wheelSmoothOneTime,
|
...wheelSmoothOneTime,
|
||||||
|
|||||||
@@ -152,6 +152,12 @@ export interface AuthState {
|
|||||||
* playing (avoids an abrupt cut). Default on for new installs.
|
* playing (avoids an abrupt cut). Default on for new installs.
|
||||||
*/
|
*/
|
||||||
autodjSmoothSkip: boolean;
|
autodjSmoothSkip: boolean;
|
||||||
|
/**
|
||||||
|
* AutoDJ: upper bound for content-driven overlap. `auto` keeps the built-in
|
||||||
|
* 12 s cap; `limit` uses `autodjOverlapCapSec` (2–30 s, default 15).
|
||||||
|
*/
|
||||||
|
autodjOverlapCapMode: 'auto' | 'limit';
|
||||||
|
autodjOverlapCapSec: number;
|
||||||
gaplessEnabled: boolean;
|
gaplessEnabled: boolean;
|
||||||
/** Show inline Play+Preview buttons in tracklists. Default on per Q3. Master kill switch — when off, all locations are off. */
|
/** Show inline Play+Preview buttons in tracklists. Default on per Q3. Master kill switch — when off, all locations are off. */
|
||||||
trackPreviewsEnabled: boolean;
|
trackPreviewsEnabled: boolean;
|
||||||
@@ -375,6 +381,8 @@ export interface AuthState {
|
|||||||
setCrossfadeSecs: (v: number) => void;
|
setCrossfadeSecs: (v: number) => void;
|
||||||
setCrossfadeTrimSilence: (v: boolean) => void;
|
setCrossfadeTrimSilence: (v: boolean) => void;
|
||||||
setAutodjSmoothSkip: (v: boolean) => void;
|
setAutodjSmoothSkip: (v: boolean) => void;
|
||||||
|
setAutodjOverlapCapMode: (v: 'auto' | 'limit') => void;
|
||||||
|
setAutodjOverlapCapSec: (v: number) => void;
|
||||||
setGaplessEnabled: (v: boolean) => void;
|
setGaplessEnabled: (v: boolean) => void;
|
||||||
setTrackPreviewsEnabled: (v: boolean) => void;
|
setTrackPreviewsEnabled: (v: boolean) => void;
|
||||||
setTrackPreviewLocation: (location: TrackPreviewLocation, enabled: boolean) => void;
|
setTrackPreviewLocation: (location: TrackPreviewLocation, enabled: boolean) => void;
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
import { useAuthStore } from './authStore';
|
||||||
|
import { autodjMaxOverlapCapSec } from '../utils/playback/autodjOverlapCap';
|
||||||
import { computeWaveformSilence, planCrossfadeTransition } from '../utils/waveform/waveformSilence';
|
import { computeWaveformSilence, planCrossfadeTransition } from '../utils/waveform/waveformSilence';
|
||||||
import { findLocalPlaybackUrl } from '../utils/offline/offlineLibraryHelpers';
|
import { findLocalPlaybackUrl } from '../utils/offline/offlineLibraryHelpers';
|
||||||
import { playbackCacheKeyForRef } from '../utils/playback/playbackServer';
|
import { playbackCacheKeyForRef } from '../utils/playback/playbackServer';
|
||||||
import { resolvePlaybackUrl } from '../utils/playback/resolvePlaybackUrl';
|
import { resolvePlaybackUrl } from '../utils/playback/resolvePlaybackUrl';
|
||||||
import { resolveQueueTrack } from '../utils/library/queueTrackView';
|
import { resolveQueueTrack } from '../utils/library/queueTrackView';
|
||||||
import type { Track } from './playerStoreTypes';
|
import type { Track } from './playerStoreTypes';
|
||||||
import { useAuthStore } from './authStore';
|
|
||||||
import {
|
import {
|
||||||
hasPlannedCrossfade,
|
hasPlannedCrossfade,
|
||||||
markPlannedCrossfade,
|
markPlannedCrossfade,
|
||||||
@@ -185,7 +186,8 @@ export function maybeCrossfadeBytePreload(currentTime: number, dur: number): voi
|
|||||||
.then(nextBins => {
|
.then(nextBins => {
|
||||||
// Overlap is derived purely from the audio (fade-out / buildup); the
|
// Overlap is derived purely from the audio (fade-out / buildup); the
|
||||||
// user's crossfadeSecs is intentionally not a factor in this mode.
|
// user's crossfadeSecs is intentionally not a factor in this mode.
|
||||||
const plan = planCrossfadeTransition(curBins, dur, nextBins, planDuration);
|
const maxOverlapSec = autodjMaxOverlapCapSec(useAuthStore.getState());
|
||||||
|
const plan = planCrossfadeTransition(curBins, dur, nextBins, planDuration, { maxOverlapSec });
|
||||||
setCrossfadeTransition(planTrackId, plan);
|
setCrossfadeTransition(planTrackId, plan);
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ const { store, setState } = vi.hoisted(() => {
|
|||||||
crossfadeTrimSilence: false,
|
crossfadeTrimSilence: false,
|
||||||
autodjSmoothSkip: true,
|
autodjSmoothSkip: true,
|
||||||
gaplessEnabled: false,
|
gaplessEnabled: false,
|
||||||
|
autodjOverlapCapMode: 'auto',
|
||||||
|
autodjOverlapCapSec: 15,
|
||||||
} as Record<string, unknown>,
|
} as Record<string, unknown>,
|
||||||
};
|
};
|
||||||
const setState = vi.fn((patch: Record<string, unknown>) => {
|
const setState = vi.fn((patch: Record<string, unknown>) => {
|
||||||
@@ -36,6 +38,8 @@ const GUEST_OWN: OrbitTransitionSettings = {
|
|||||||
crossfadeTrimSilence: false,
|
crossfadeTrimSilence: false,
|
||||||
autodjSmoothSkip: true,
|
autodjSmoothSkip: true,
|
||||||
gaplessEnabled: false,
|
gaplessEnabled: false,
|
||||||
|
autodjOverlapCapMode: 'auto',
|
||||||
|
autodjOverlapCapSec: 15,
|
||||||
};
|
};
|
||||||
const HOST: OrbitTransitionSettings = {
|
const HOST: OrbitTransitionSettings = {
|
||||||
crossfadeEnabled: true,
|
crossfadeEnabled: true,
|
||||||
@@ -43,6 +47,8 @@ const HOST: OrbitTransitionSettings = {
|
|||||||
crossfadeTrimSilence: true,
|
crossfadeTrimSilence: true,
|
||||||
autodjSmoothSkip: false,
|
autodjSmoothSkip: false,
|
||||||
gaplessEnabled: false,
|
gaplessEnabled: false,
|
||||||
|
autodjOverlapCapMode: 'limit',
|
||||||
|
autodjOverlapCapSec: 20,
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -52,7 +58,7 @@ beforeEach(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('read/apply transition settings', () => {
|
describe('read/apply transition settings', () => {
|
||||||
it('reads the five transition fields from the store', () => {
|
it('reads the transition fields from the store', () => {
|
||||||
expect(readOrbitTransitionSettings()).toEqual(GUEST_OWN);
|
expect(readOrbitTransitionSettings()).toEqual(GUEST_OWN);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { useAuthStore } from '../../store/authStore';
|
import { useAuthStore } from '../../store/authStore';
|
||||||
import type { OrbitTransitionSettings } from '../../api/orbit';
|
import type { OrbitTransitionSettings } from '../../api/orbit';
|
||||||
|
import {
|
||||||
|
sanitizeAutodjOverlapCapMode,
|
||||||
|
sanitizeAutodjOverlapCapSec,
|
||||||
|
} from '../playback/autodjOverlapCap';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bridge between the local playback-transition settings (in `authStore`) and
|
* Bridge between the local playback-transition settings (in `authStore`) and
|
||||||
@@ -29,13 +33,24 @@ export function readOrbitTransitionSettings(): OrbitTransitionSettings {
|
|||||||
crossfadeTrimSilence: s.crossfadeTrimSilence,
|
crossfadeTrimSilence: s.crossfadeTrimSilence,
|
||||||
autodjSmoothSkip: s.autodjSmoothSkip,
|
autodjSmoothSkip: s.autodjSmoothSkip,
|
||||||
gaplessEnabled: s.gaplessEnabled,
|
gaplessEnabled: s.gaplessEnabled,
|
||||||
|
autodjOverlapCapMode: s.autodjOverlapCapMode,
|
||||||
|
autodjOverlapCapSec: s.autodjOverlapCapSec,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** True when the local settings already equal `t` (nothing to apply). */
|
/** True when the local settings already equal `t` (nothing to apply). */
|
||||||
function alreadyInSync(t: OrbitTransitionSettings): boolean {
|
function alreadyInSync(t: OrbitTransitionSettings): boolean {
|
||||||
const s = useAuthStore.getState() as unknown as OrbitTransitionSettings;
|
const s = useAuthStore.getState();
|
||||||
return FIELDS.every(f => s[f] === t[f]);
|
if (!FIELDS.every(f => s[f] === t[f])) return false;
|
||||||
|
if (t.autodjOverlapCapMode !== undefined
|
||||||
|
&& s.autodjOverlapCapMode !== sanitizeAutodjOverlapCapMode(t.autodjOverlapCapMode)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (t.autodjOverlapCapSec !== undefined
|
||||||
|
&& s.autodjOverlapCapSec !== sanitizeAutodjOverlapCapSec(t.autodjOverlapCapSec)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -51,6 +66,12 @@ export function applyOrbitTransitionSettings(t: OrbitTransitionSettings): void {
|
|||||||
crossfadeTrimSilence: t.crossfadeTrimSilence,
|
crossfadeTrimSilence: t.crossfadeTrimSilence,
|
||||||
autodjSmoothSkip: t.autodjSmoothSkip,
|
autodjSmoothSkip: t.autodjSmoothSkip,
|
||||||
gaplessEnabled: t.gaplessEnabled,
|
gaplessEnabled: t.gaplessEnabled,
|
||||||
|
...(t.autodjOverlapCapMode !== undefined
|
||||||
|
? { autodjOverlapCapMode: sanitizeAutodjOverlapCapMode(t.autodjOverlapCapMode) }
|
||||||
|
: {}),
|
||||||
|
...(t.autodjOverlapCapSec !== undefined
|
||||||
|
? { autodjOverlapCapSec: sanitizeAutodjOverlapCapSec(t.autodjOverlapCapSec) }
|
||||||
|
: {}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { STANDARD_BLEND_SEC } from '../waveform/waveformSilence';
|
import { DYNAMIC_OVERLAP_HARD_CAP_SEC, STANDARD_BLEND_SEC } from '../waveform/waveformSilence';
|
||||||
|
|
||||||
/** Clamp engine crossfade setting to the same bounds used in progress handling. */
|
/** Clamp engine crossfade setting to the same bounds used in progress handling. */
|
||||||
export function clampCrossfadeSecs(crossfadeSecs: number): number {
|
export function clampCrossfadeSecs(crossfadeSecs: number): number {
|
||||||
@@ -26,8 +26,9 @@ export function shouldJsDriveAutodjTransition(
|
|||||||
export function computeAutodjJsOverlap(
|
export function computeAutodjJsOverlap(
|
||||||
contentOverlap: number,
|
contentOverlap: number,
|
||||||
aRidesOwnFade: boolean,
|
aRidesOwnFade: boolean,
|
||||||
|
maxCapSec = DYNAMIC_OVERLAP_HARD_CAP_SEC,
|
||||||
): { overlapSec: number; outgoingFadeSec: number } {
|
): { overlapSec: number; outgoingFadeSec: number } {
|
||||||
let overlap = Math.max(0.5, Math.min(12, contentOverlap || 0.5));
|
let overlap = Math.max(0.5, Math.min(maxCapSec, contentOverlap || 0.5));
|
||||||
if (!aRidesOwnFade && overlap < STANDARD_BLEND_SEC) overlap = STANDARD_BLEND_SEC;
|
if (!aRidesOwnFade && overlap < STANDARD_BLEND_SEC) overlap = STANDARD_BLEND_SEC;
|
||||||
const outgoingFadeSec = aRidesOwnFade ? 0 : overlap;
|
const outgoingFadeSec = aRidesOwnFade ? 0 : overlap;
|
||||||
return { overlapSec: overlap, outgoingFadeSec };
|
return { overlapSec: overlap, outgoingFadeSec };
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useAuthStore } from '../../store/authStore';
|
import { useAuthStore } from '../../store/authStore';
|
||||||
|
import { autodjMaxOverlapCapSec } from './autodjOverlapCap';
|
||||||
import {
|
import {
|
||||||
analyzeBoundary,
|
analyzeBoundary,
|
||||||
planCrossfadeTransition,
|
planCrossfadeTransition,
|
||||||
@@ -58,15 +59,19 @@ export function computeAutodjManualBlendPlan(
|
|||||||
const bDur = Number.isFinite(bDurationSec) && bDurationSec > 0 ? bDurationSec : 0;
|
const bDur = Number.isFinite(bDurationSec) && bDurationSec > 0 ? bDurationSec : 0;
|
||||||
if (aDur <= 0 || bDur <= 0) return null;
|
if (aDur <= 0 || bDur <= 0) return null;
|
||||||
|
|
||||||
const base = planCrossfadeTransition(aBins, aDur, bBins, bDur);
|
const base = planCrossfadeTransition(aBins, aDur, bBins, bDur, {
|
||||||
|
maxOverlapSec: autodjMaxOverlapCapSec(useAuthStore.getState()),
|
||||||
|
});
|
||||||
if (!(base.overlapSec > 0)) return null;
|
if (!(base.overlapSec > 0)) return null;
|
||||||
|
|
||||||
|
const maxCapSec = autodjMaxOverlapCapSec(useAuthStore.getState());
|
||||||
|
|
||||||
const aShape = analyzeBoundary(aBins, aDur);
|
const aShape = analyzeBoundary(aBins, aDur);
|
||||||
const bShape = analyzeBoundary(bBins, bDur);
|
const bShape = analyzeBoundary(bBins, bDur);
|
||||||
const aRemaining = aShape.contentEndSec - Math.max(0, skipFromTimeSec);
|
const aRemaining = aShape.contentEndSec - Math.max(0, skipFromTimeSec);
|
||||||
if (aRemaining < MIN_A_REMAINING_SEC) return null;
|
if (aRemaining < MIN_A_REMAINING_SEC) return null;
|
||||||
|
|
||||||
let overlap = Math.max(0.5, Math.min(12, base.overlapSec, aRemaining));
|
let overlap = Math.max(0.5, Math.min(maxCapSec, base.overlapSec, aRemaining));
|
||||||
const bPlayable = Math.max(0, bShape.contentEndSec - base.bStartSec);
|
const bPlayable = Math.max(0, bShape.contentEndSec - base.bStartSec);
|
||||||
if (bPlayable > 0) overlap = Math.min(overlap, bPlayable * 0.9);
|
if (bPlayable > 0) overlap = Math.min(overlap, bPlayable * 0.9);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import {
|
||||||
|
autodjMaxOverlapCapSec,
|
||||||
|
DEFAULT_AUTODJ_OVERLAP_CAP_SEC,
|
||||||
|
sanitizeAutodjOverlapCapSec,
|
||||||
|
} from './autodjOverlapCap';
|
||||||
|
|
||||||
|
describe('autodjOverlapCap', () => {
|
||||||
|
it('sanitizes cap seconds to 2–30', () => {
|
||||||
|
expect(sanitizeAutodjOverlapCapSec(1)).toBe(2);
|
||||||
|
expect(sanitizeAutodjOverlapCapSec(40)).toBe(30);
|
||||||
|
expect(sanitizeAutodjOverlapCapSec(DEFAULT_AUTODJ_OVERLAP_CAP_SEC)).toBe(15);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses 12 s in auto mode', () => {
|
||||||
|
expect(autodjMaxOverlapCapSec({ autodjOverlapCapMode: 'auto', autodjOverlapCapSec: 20 })).toBe(12);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses configured cap in limit mode', () => {
|
||||||
|
expect(autodjMaxOverlapCapSec({ autodjOverlapCapMode: 'limit', autodjOverlapCapSec: 20 })).toBe(20);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import type { AuthState } from '../../store/authStoreTypes';
|
||||||
|
import { DYNAMIC_OVERLAP_HARD_CAP_SEC } from '../waveform/waveformSilence';
|
||||||
|
|
||||||
|
export type AutodjOverlapCapMode = 'auto' | 'limit';
|
||||||
|
|
||||||
|
export const AUTODJ_OVERLAP_CAP_MIN_SEC = 2;
|
||||||
|
export const AUTODJ_OVERLAP_CAP_MAX_SEC = 30;
|
||||||
|
export const DEFAULT_AUTODJ_OVERLAP_CAP_SEC = 15;
|
||||||
|
|
||||||
|
export function sanitizeAutodjOverlapCapSec(value: unknown): number {
|
||||||
|
const n = typeof value === 'number' ? value : Number(value);
|
||||||
|
if (!Number.isFinite(n)) return DEFAULT_AUTODJ_OVERLAP_CAP_SEC;
|
||||||
|
return Math.min(AUTODJ_OVERLAP_CAP_MAX_SEC, Math.max(AUTODJ_OVERLAP_CAP_MIN_SEC, n));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sanitizeAutodjOverlapCapMode(value: unknown): AutodjOverlapCapMode {
|
||||||
|
return value === 'limit' ? 'limit' : 'auto';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Upper bound (seconds) for content-driven AutoDJ overlap calculations. */
|
||||||
|
export function autodjMaxOverlapCapSec(
|
||||||
|
auth: Pick<AuthState, 'autodjOverlapCapMode' | 'autodjOverlapCapSec'>,
|
||||||
|
): number {
|
||||||
|
if (auth.autodjOverlapCapMode === 'auto') {
|
||||||
|
return DYNAMIC_OVERLAP_HARD_CAP_SEC;
|
||||||
|
}
|
||||||
|
return sanitizeAutodjOverlapCapSec(auth.autodjOverlapCapSec);
|
||||||
|
}
|
||||||
@@ -157,7 +157,8 @@ export function analyzeBoundary(
|
|||||||
|
|
||||||
/** Engine fade min/max — the override is clamped to the same range on the Rust side. */
|
/** Engine fade min/max — the override is clamped to the same range on the Rust side. */
|
||||||
const DYNAMIC_OVERLAP_MIN_SEC = 0.5;
|
const DYNAMIC_OVERLAP_MIN_SEC = 0.5;
|
||||||
const DYNAMIC_OVERLAP_HARD_CAP_SEC = 12;
|
export const DYNAMIC_OVERLAP_HARD_CAP_SEC = 12;
|
||||||
|
export const DYNAMIC_OVERLAP_ABSOLUTE_MAX_SEC = 30;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Standard pleasant blend used when *both* edges are known but neither fades —
|
* Standard pleasant blend used when *both* edges are known but neither fades —
|
||||||
@@ -227,7 +228,8 @@ export function planCrossfadeTransition(
|
|||||||
opts: CrossfadePlanOptions = {},
|
opts: CrossfadePlanOptions = {},
|
||||||
): CrossfadeTransitionPlan {
|
): CrossfadeTransitionPlan {
|
||||||
const min = Math.max(0.1, opts.minOverlapSec ?? DYNAMIC_OVERLAP_MIN_SEC);
|
const min = Math.max(0.1, opts.minOverlapSec ?? DYNAMIC_OVERLAP_MIN_SEC);
|
||||||
const cap = Math.min(DYNAMIC_OVERLAP_HARD_CAP_SEC, Math.max(min, opts.maxOverlapSec ?? DYNAMIC_OVERLAP_HARD_CAP_SEC));
|
const capUpper = opts.maxOverlapSec ?? DYNAMIC_OVERLAP_HARD_CAP_SEC;
|
||||||
|
const cap = Math.max(min, Math.min(capUpper, DYNAMIC_OVERLAP_ABSOLUTE_MAX_SEC));
|
||||||
|
|
||||||
const aShape = analyzeBoundary(aBins, aDurationSec, opts);
|
const aShape = analyzeBoundary(aBins, aDurationSec, opts);
|
||||||
const bShape = analyzeBoundary(bBins, bDurationSec, opts);
|
const bShape = analyzeBoundary(bBins, bDurationSec, opts);
|
||||||
|
|||||||
Reference in New Issue
Block a user