egaki 0.7.2 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -0
- package/dist/cli/bpm-detect.d.ts +7 -0
- package/dist/cli/bpm-detect.d.ts.map +1 -0
- package/dist/cli/bpm-detect.js +123 -0
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +115 -0
- package/dist/cli/loudness-detect.d.ts +13 -0
- package/dist/cli/loudness-detect.d.ts.map +1 -0
- package/dist/cli/loudness-detect.js +96 -0
- package/dist/vite/angled-screen-shader.d.ts +45 -0
- package/dist/vite/angled-screen-shader.d.ts.map +1 -0
- package/dist/vite/angled-screen-shader.js +573 -0
- package/dist/vite/angled-screen.d.ts +12 -3
- package/dist/vite/angled-screen.d.ts.map +1 -1
- package/dist/vite/angled-screen.js +12 -3
- package/dist/vite/code-block.d.ts.map +1 -1
- package/dist/vite/code-block.js +26 -5
- package/dist/vite/layout-editor.js +1 -1
- package/dist/vite/mdx-video.d.ts +4 -2
- package/dist/vite/mdx-video.d.ts.map +1 -1
- package/dist/vite/mdx-video.js +4 -2
- package/dist/vite/mdx-video.test.js +1 -79
- package/dist/vite/player-page.d.ts.map +1 -1
- package/dist/vite/player-page.js +6 -94
- package/dist/vite/render-client.d.ts +27 -0
- package/dist/vite/render-client.d.ts.map +1 -1
- package/dist/vite/render-client.js +47 -51
- package/dist/vite/sdk.d.ts.map +1 -1
- package/dist/vite/sdk.js +4 -3
- package/dist/vite/shader-renderer.d.ts +4 -0
- package/dist/vite/shader-renderer.d.ts.map +1 -1
- package/dist/vite/shader-renderer.js +3 -3
- package/dist/vite/store-hooks.d.ts +2 -0
- package/dist/vite/store-hooks.d.ts.map +1 -1
- package/dist/vite/store-hooks.js +8 -0
- package/dist/vite/store.d.ts +5 -0
- package/dist/vite/store.d.ts.map +1 -1
- package/dist/vite/store.js +5 -0
- package/dist/vite/tweakpane-hook.d.ts +12 -3
- package/dist/vite/tweakpane-hook.d.ts.map +1 -1
- package/dist/vite/tweakpane-hook.js +128 -20
- package/dist/vite/vite-plugin.d.ts +0 -6
- package/dist/vite/vite-plugin.d.ts.map +1 -1
- package/dist/vite/vite-plugin.js +1 -92
- package/package.json +9 -6
- package/skills/egaki/SKILL.md +26 -224
- package/src/cli/bpm-detect.ts +165 -0
- package/src/cli/cli.ts +149 -0
- package/src/cli/loudness-detect.ts +129 -0
- package/src/vite/angled-screen-shader.tsx +706 -0
- package/src/vite/angled-screen.tsx +13 -4
- package/src/vite/code-block.tsx +28 -6
- package/src/vite/layout-editor.tsx +1 -1
- package/src/vite/mdx-video.test.tsx +1 -94
- package/src/vite/mdx-video.tsx +4 -1
- package/src/vite/player-page.tsx +28 -117
- package/src/vite/render-client.ts +52 -51
- package/src/vite/sdk.ts +4 -3
- package/src/vite/shader-renderer.tsx +3 -3
- package/src/vite/store-hooks.ts +11 -0
- package/src/vite/store.ts +10 -0
- package/src/vite/tweakpane-hook.tsx +173 -28
- package/src/vite/vite-plugin.ts +1 -91
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// LUFS (Loudness Units Full Scale) measurement for audio files.
|
|
2
|
+
// Ported from meltica. Uses node-web-audio-api (native NAPI Rust binding)
|
|
3
|
+
// as an optional dependency. If missing, returns an Error with install instructions.
|
|
4
|
+
//
|
|
5
|
+
// Implements a simplified LUFS measurement inspired by EBU R128 / ITU-R BS.1770:
|
|
6
|
+
// K-weighting via biquad filters (approximate, not exact BS.1770 coefficients),
|
|
7
|
+
// 400ms blocks with 100ms hop, two-pass gating (absolute -70 LUFS gate, then
|
|
8
|
+
// relative -10 LU gate). Energy averaging is done in the linear domain (mean
|
|
9
|
+
// square) before converting to LUFS, which is the correct approach.
|
|
10
|
+
//
|
|
11
|
+
// The K-weighting filters are simplified (highpass 60Hz + highshelf 1500Hz)
|
|
12
|
+
// rather than the exact BS.1770 pre-filter + RLB coefficients, so results
|
|
13
|
+
// may differ slightly from reference implementations.
|
|
14
|
+
|
|
15
|
+
interface LoudnessResult {
|
|
16
|
+
/** Integrated loudness in LUFS (two-pass gated average). */
|
|
17
|
+
integrated: number
|
|
18
|
+
/** Maximum momentary loudness (single 400ms block). */
|
|
19
|
+
max: number
|
|
20
|
+
/** Minimum momentary loudness above the absolute gate. */
|
|
21
|
+
min: number
|
|
22
|
+
/** Loudness range (max - min) in LU. */
|
|
23
|
+
range: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function lufsFromMeanSquare(ms: number): number {
|
|
27
|
+
return -0.691 + 10 * Math.log10(ms)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function detectLoudness(buffer: ArrayBuffer): Promise<Error | LoudnessResult> {
|
|
31
|
+
let webAudioMod: any
|
|
32
|
+
try {
|
|
33
|
+
webAudioMod = await import('node-web-audio-api')
|
|
34
|
+
} catch {
|
|
35
|
+
return new Error(
|
|
36
|
+
'node-web-audio-api is required for loudness measurement. Install it:\n\n' +
|
|
37
|
+
' pnpm add node-web-audio-api\n',
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const context = new webAudioMod.AudioContext()
|
|
42
|
+
try {
|
|
43
|
+
const audioBuffer: AudioBuffer = await context.decodeAudioData(buffer)
|
|
44
|
+
|
|
45
|
+
const offlineContext = new webAudioMod.OfflineAudioContext(
|
|
46
|
+
audioBuffer.numberOfChannels,
|
|
47
|
+
audioBuffer.length,
|
|
48
|
+
audioBuffer.sampleRate,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
const source = offlineContext.createBufferSource()
|
|
52
|
+
source.buffer = audioBuffer
|
|
53
|
+
|
|
54
|
+
// Simplified K-weighting: high-pass at 60Hz + high-shelf boost at 1500Hz
|
|
55
|
+
const highPass = offlineContext.createBiquadFilter()
|
|
56
|
+
highPass.type = 'highpass'
|
|
57
|
+
highPass.frequency.value = 60
|
|
58
|
+
highPass.Q.value = 0.7
|
|
59
|
+
|
|
60
|
+
const highShelf = offlineContext.createBiquadFilter()
|
|
61
|
+
highShelf.type = 'highshelf'
|
|
62
|
+
highShelf.frequency.value = 1500
|
|
63
|
+
highShelf.gain.value = 4
|
|
64
|
+
|
|
65
|
+
source.connect(highPass)
|
|
66
|
+
highPass.connect(highShelf)
|
|
67
|
+
highShelf.connect(offlineContext.destination)
|
|
68
|
+
|
|
69
|
+
source.start(0)
|
|
70
|
+
const rendered: AudioBuffer = await offlineContext.startRendering()
|
|
71
|
+
|
|
72
|
+
// 400ms blocks, 100ms hop
|
|
73
|
+
const blockSize = Math.floor(0.4 * rendered.sampleRate)
|
|
74
|
+
const hopSize = Math.floor(0.1 * rendered.sampleRate)
|
|
75
|
+
|
|
76
|
+
if (rendered.length < blockSize) {
|
|
77
|
+
return new Error('Audio must be at least 400ms for loudness measurement.')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Collect mean square energy per block
|
|
81
|
+
const blocks: number[] = []
|
|
82
|
+
|
|
83
|
+
for (let i = 0; i <= rendered.length - blockSize; i += hopSize) {
|
|
84
|
+
let sumSquared = 0
|
|
85
|
+
for (let ch = 0; ch < rendered.numberOfChannels; ch++) {
|
|
86
|
+
const data = rendered.getChannelData(ch)
|
|
87
|
+
for (let j = 0; j < blockSize; j++) {
|
|
88
|
+
sumSquared += data[i + j]! * data[i + j]!
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
blocks.push(sumSquared / (blockSize * rendered.numberOfChannels))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Two-pass gating per EBU R128:
|
|
95
|
+
// Pass 1: absolute gate at -70 LUFS
|
|
96
|
+
const absoluteGate = -70
|
|
97
|
+
const absoluteGated = blocks.filter((ms) => lufsFromMeanSquare(ms) > absoluteGate)
|
|
98
|
+
|
|
99
|
+
if (absoluteGated.length === 0) {
|
|
100
|
+
return new Error('Audio is silent or below the -70 LUFS gate threshold.')
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Pass 2: relative gate at -10 LU below the absolute-gated integrated loudness
|
|
104
|
+
const preliminaryMs = absoluteGated.reduce((s, v) => s + v, 0) / absoluteGated.length
|
|
105
|
+
const relativeGate = lufsFromMeanSquare(preliminaryMs) - 10
|
|
106
|
+
const relativeGated = absoluteGated.filter((ms) => lufsFromMeanSquare(ms) > relativeGate)
|
|
107
|
+
|
|
108
|
+
if (relativeGated.length === 0) {
|
|
109
|
+
return new Error('Audio is too quiet for reliable loudness measurement.')
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Integrated loudness: average energy in linear domain, then convert to LUFS
|
|
113
|
+
const integratedMs = relativeGated.reduce((s, v) => s + v, 0) / relativeGated.length
|
|
114
|
+
const integrated = lufsFromMeanSquare(integratedMs)
|
|
115
|
+
|
|
116
|
+
// Momentary loudness values for range calculation (from absolute-gated blocks)
|
|
117
|
+
const momentaryLufs = absoluteGated.map(lufsFromMeanSquare)
|
|
118
|
+
const max = Math.max(...momentaryLufs)
|
|
119
|
+
const min = Math.min(...momentaryLufs)
|
|
120
|
+
const range = max - min
|
|
121
|
+
|
|
122
|
+
return { integrated, max, min, range }
|
|
123
|
+
} catch (err) {
|
|
124
|
+
if (err instanceof Error) return err
|
|
125
|
+
return new Error(String(err))
|
|
126
|
+
} finally {
|
|
127
|
+
await context.close().catch(() => {})
|
|
128
|
+
}
|
|
129
|
+
}
|