Repository navigation
Expand file tree
/
Copy pathsongStructureCommon.ts
More file actions
135 lines (114 loc) · 4.47 KB
/
Copy pathsongStructureCommon.ts
File metadata and controls
135 lines (114 loc) · 4.47 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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
import type { UnifiedDisplayWaveformDetailData } from './unifiedDisplayWaveform'
import type { SongBeatGridMap } from './songBeatGridMap'
import type { SongStructureFeatureData } from './songStructureFeatureData'
export const CURRENT_SONG_STRUCTURE_ALGORITHM_VERSION = 22
// 仅持久化结构格式不兼容时升级;普通算法调参只升级 algorithmVersion。
export const CURRENT_SONG_STRUCTURE_FORMAT_VERSION = 1
export type SongStructureSectionKind = 'intro' | 'groove' | 'breakdown' | 'build' | 'drop' | 'outro'
export type SongStructureAnalysisSource = 'algorithmic'
export type SongStructureSection = {
startSec: number
endSec: number
startBar: number
endBar: number
phraseIndex: number
kind: SongStructureSectionKind
confidence: number
energy: number
low: number
high: number
novelty: number
}
export type SongStructureAnalysis = {
formatVersion: number
algorithmVersion: number
source?: SongStructureAnalysisSource
durationSec: number
bpm: number
firstBeatMs: number
barBeatOffset: number
beatGridSignature?: string
phraseBars: number
sections: SongStructureSection[]
}
export type BuildSongStructureInput = {
waveformData: UnifiedDisplayWaveformDetailData | null | undefined
structureFeatureData?: SongStructureFeatureData | null
bpm: unknown
firstBeatMs: unknown
barBeatOffset: unknown
beatGridMap?: SongBeatGridMap | null
}
export type SongStructureGrid = {
bpm?: unknown
firstBeatMs?: unknown
barBeatOffset?: unknown
beatGridMap?: unknown
}
export const BYTE_MAX = 255
export const BEATS_PER_BAR = 4
export const PHRASE_BARS = 8
export const PHRASE_BEATS = BEATS_PER_BAR * PHRASE_BARS
export const MAX_SECTIONS = 64
export const clamp = (value: number, min: number, max: number) =>
Math.max(min, Math.min(max, value))
export const clamp01 = (value: number) => (Number.isFinite(value) ? clamp(value, 0, 1) : 0)
export const ramp = (value: number, min: number, max: number) => {
if (max <= min) return value >= max ? 1 : 0
return clamp01((value - min) / (max - min))
}
export const toFixedNumber = (value: number, digits: number) => Number(value.toFixed(digits))
export const resolveSongStructureTimelineFirstBeatMs = (
analyzedFirstBeatMs: unknown,
cachedFirstBeatMs: unknown,
timeBasisOffsetMs: unknown
): number | undefined => {
const analyzed = Number(analyzedFirstBeatMs)
if (Number.isFinite(analyzed)) {
const offset = Number(timeBasisOffsetMs)
return toFixedNumber(analyzed + (Number.isFinite(offset) ? Math.max(0, offset) : 0), 3)
}
const cached = Number(cachedFirstBeatMs)
return Number.isFinite(cached) ? toFixedNumber(cached, 3) : undefined
}
export const resolveBassPresence = (value: { energy: number; low: number }) =>
clamp01(Math.sqrt(clamp01(value.energy) * clamp01(value.low)))
const normalizeBpm = (value: unknown): number | undefined => {
const numeric = Number(value)
if (!Number.isFinite(numeric) || numeric <= 0) return undefined
return toFixedNumber(numeric, 6)
}
const normalizeFirstBeatMs = (value: unknown): number | undefined => {
const numeric = Number(value)
if (!Number.isFinite(numeric)) return undefined
return toFixedNumber(numeric, 3)
}
export const normalizeBeatOffset = (
value: unknown,
interval = PHRASE_BEATS
): number | undefined => {
const numeric = Number(value)
if (!Number.isFinite(numeric)) return undefined
const rounded = Math.round(numeric)
return ((rounded % interval) + interval) % interval
}
export const normalizeStructureGrid = (value: SongStructureGrid | null | undefined) => {
const bpm = normalizeBpm(value?.bpm)
const firstBeatMs = normalizeFirstBeatMs(value?.firstBeatMs)
const barBeatOffset = normalizeBeatOffset(value?.barBeatOffset)
if (bpm === undefined || firstBeatMs === undefined || barBeatOffset === undefined) return null
return { bpm, firstBeatMs, barBeatOffset }
}
export const isSameGridValue = (left: number | undefined, right: number, epsilon: number) =>
left !== undefined && Math.abs(left - right) <= epsilon
export const readByteRatio = (values: Uint8Array | undefined, index: number) => {
if (!values?.length) return 0
const safeIndex = clamp(Math.floor(index), 0, values.length - 1)
return clamp(values[safeIndex] ?? 0, 0, BYTE_MAX) / BYTE_MAX
}
export const percentile = (values: readonly number[], ratio: number) => {
if (!values.length) return 0
const sorted = [...values].sort((a, b) => a - b)
const index = clamp(Math.round((sorted.length - 1) * clamp01(ratio)), 0, sorted.length - 1)
return sorted[index] ?? 0
}