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.
Files changed (63) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/dist/cli/bpm-detect.d.ts +7 -0
  3. package/dist/cli/bpm-detect.d.ts.map +1 -0
  4. package/dist/cli/bpm-detect.js +123 -0
  5. package/dist/cli/cli.d.ts.map +1 -1
  6. package/dist/cli/cli.js +115 -0
  7. package/dist/cli/loudness-detect.d.ts +13 -0
  8. package/dist/cli/loudness-detect.d.ts.map +1 -0
  9. package/dist/cli/loudness-detect.js +96 -0
  10. package/dist/vite/angled-screen-shader.d.ts +45 -0
  11. package/dist/vite/angled-screen-shader.d.ts.map +1 -0
  12. package/dist/vite/angled-screen-shader.js +573 -0
  13. package/dist/vite/angled-screen.d.ts +12 -3
  14. package/dist/vite/angled-screen.d.ts.map +1 -1
  15. package/dist/vite/angled-screen.js +12 -3
  16. package/dist/vite/code-block.d.ts.map +1 -1
  17. package/dist/vite/code-block.js +26 -5
  18. package/dist/vite/layout-editor.js +1 -1
  19. package/dist/vite/mdx-video.d.ts +4 -2
  20. package/dist/vite/mdx-video.d.ts.map +1 -1
  21. package/dist/vite/mdx-video.js +4 -2
  22. package/dist/vite/mdx-video.test.js +1 -79
  23. package/dist/vite/player-page.d.ts.map +1 -1
  24. package/dist/vite/player-page.js +6 -94
  25. package/dist/vite/render-client.d.ts +27 -0
  26. package/dist/vite/render-client.d.ts.map +1 -1
  27. package/dist/vite/render-client.js +47 -51
  28. package/dist/vite/sdk.d.ts.map +1 -1
  29. package/dist/vite/sdk.js +4 -3
  30. package/dist/vite/shader-renderer.d.ts +4 -0
  31. package/dist/vite/shader-renderer.d.ts.map +1 -1
  32. package/dist/vite/shader-renderer.js +3 -3
  33. package/dist/vite/store-hooks.d.ts +2 -0
  34. package/dist/vite/store-hooks.d.ts.map +1 -1
  35. package/dist/vite/store-hooks.js +8 -0
  36. package/dist/vite/store.d.ts +5 -0
  37. package/dist/vite/store.d.ts.map +1 -1
  38. package/dist/vite/store.js +5 -0
  39. package/dist/vite/tweakpane-hook.d.ts +12 -3
  40. package/dist/vite/tweakpane-hook.d.ts.map +1 -1
  41. package/dist/vite/tweakpane-hook.js +128 -20
  42. package/dist/vite/vite-plugin.d.ts +0 -6
  43. package/dist/vite/vite-plugin.d.ts.map +1 -1
  44. package/dist/vite/vite-plugin.js +1 -92
  45. package/package.json +9 -6
  46. package/skills/egaki/SKILL.md +26 -224
  47. package/src/cli/bpm-detect.ts +165 -0
  48. package/src/cli/cli.ts +149 -0
  49. package/src/cli/loudness-detect.ts +129 -0
  50. package/src/vite/angled-screen-shader.tsx +706 -0
  51. package/src/vite/angled-screen.tsx +13 -4
  52. package/src/vite/code-block.tsx +28 -6
  53. package/src/vite/layout-editor.tsx +1 -1
  54. package/src/vite/mdx-video.test.tsx +1 -94
  55. package/src/vite/mdx-video.tsx +4 -1
  56. package/src/vite/player-page.tsx +28 -117
  57. package/src/vite/render-client.ts +52 -51
  58. package/src/vite/sdk.ts +4 -3
  59. package/src/vite/shader-renderer.tsx +3 -3
  60. package/src/vite/store-hooks.ts +11 -0
  61. package/src/vite/store.ts +10 -0
  62. package/src/vite/tweakpane-hook.tsx +173 -28
  63. 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
+ }