Repository navigation
Expand file tree
/
Copy pathsongStructureDynamicBoundaries.ts
More file actions
94 lines (80 loc) · 2.91 KB
/
Copy pathsongStructureDynamicBoundaries.ts
File metadata and controls
94 lines (80 loc) · 2.91 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
import { toFixedNumber, type BuildSongStructureInput } from './songStructureCommon'
import { createSongBeatGridRuntime, type SongBeatGridRuntime } from './songBeatGridMap'
const DYNAMIC_BOUNDARY_MIN_GAP_SEC = 2
const DYNAMIC_PHRASE_BOUNDARY_PRIORITY = 1
const DYNAMIC_CLIP_BOUNDARY_PRIORITY = 2
type DynamicBoundaryCandidate = {
sec: number
priority: number
}
type DynamicPhraseBoundaries = {
runtime: SongBeatGridRuntime
boundaries: number[]
}
const normalizeBoundarySec = (sec: number) => toFixedNumber(Math.max(0, sec), 4)
const isInnerBoundarySec = (sec: number, durationSec: number) =>
sec > DYNAMIC_BOUNDARY_MIN_GAP_SEC && sec < durationSec - DYNAMIC_BOUNDARY_MIN_GAP_SEC
const pushCandidate = (
candidates: DynamicBoundaryCandidate[],
secInput: number,
priority: number,
durationSec: number
) => {
if (!Number.isFinite(secInput) || !isInnerBoundarySec(secInput, durationSec)) return
candidates.push({
sec: normalizeBoundarySec(secInput),
priority
})
}
const buildBoundariesFromCandidates = (
candidates: readonly DynamicBoundaryCandidate[],
durationSec: number
) => {
const selected: DynamicBoundaryCandidate[] = []
for (const candidate of [...candidates].sort(
(left, right) => left.sec - right.sec || right.priority - left.priority
)) {
const last = selected[selected.length - 1]
if (!last || candidate.sec - last.sec >= DYNAMIC_BOUNDARY_MIN_GAP_SEC) {
selected.push(candidate)
continue
}
if (candidate.priority > last.priority) {
selected[selected.length - 1] = candidate
}
}
const boundaries = [0, ...selected.map((candidate) => candidate.sec)]
const lastBoundary = boundaries[boundaries.length - 1] ?? 0
if (durationSec - lastBoundary >= DYNAMIC_BOUNDARY_MIN_GAP_SEC) {
boundaries.push(toFixedNumber(durationSec, 4))
} else {
boundaries[boundaries.length - 1] = toFixedNumber(durationSec, 4)
}
return boundaries
}
export const buildDynamicPhraseBoundaries = (
input: BuildSongStructureInput,
durationSec: number
): DynamicPhraseBoundaries | null => {
const runtime = createSongBeatGridRuntime(input.beatGridMap, durationSec)
if (!runtime) return null
const rawBarLines = runtime.lines.filter(
(line) => line.level === 'bar' && line.sec > 0 && line.sec < durationSec
)
if (rawBarLines.length < 1) return null
const barLines = rawBarLines.filter((line) => isInnerBoundarySec(line.sec, durationSec))
const candidates: DynamicBoundaryCandidate[] = []
for (const clip of runtime.clips) {
if (clip.index > 0) {
pushCandidate(candidates, clip.startSec, DYNAMIC_CLIP_BOUNDARY_PRIORITY, durationSec)
}
const clipBarLines = barLines.filter((line) => line.clipIndex === clip.index)
for (const line of clipBarLines) {
pushCandidate(candidates, line.sec, DYNAMIC_PHRASE_BOUNDARY_PRIORITY, durationSec)
}
}
return {
runtime,
boundaries: buildBoundariesFromCandidates(candidates, durationSec)
}
}