@motionscript/audio 0.0.0-stage → 0.1.0-alpha.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 (52) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/LICENSE +201 -0
  3. package/dist/browser/chunks/chunk-U5OUQJHQ.js +2 -0
  4. package/dist/browser/chunks/chunk-U5OUQJHQ.js.map +7 -0
  5. package/dist/browser/index.js +2 -0
  6. package/dist/browser/index.js.map +7 -0
  7. package/dist/browser/kit.js +2 -0
  8. package/dist/browser/kit.js.map +7 -0
  9. package/dist/browser/manifest.json +12 -0
  10. package/dist/index.d.ts +3 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +3 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/kit.d.ts +13 -0
  15. package/dist/kit.d.ts.map +1 -0
  16. package/dist/kit.js +11 -0
  17. package/dist/kit.js.map +1 -0
  18. package/dist/nodes.d.ts +19 -0
  19. package/dist/nodes.d.ts.map +1 -0
  20. package/dist/nodes.js +19 -0
  21. package/dist/nodes.js.map +1 -0
  22. package/dist/waveform/envelope.d.ts +87 -0
  23. package/dist/waveform/envelope.d.ts.map +1 -0
  24. package/dist/waveform/envelope.js +96 -0
  25. package/dist/waveform/envelope.js.map +1 -0
  26. package/dist/waveform/full-waveform.d.ts +104 -0
  27. package/dist/waveform/full-waveform.d.ts.map +1 -0
  28. package/dist/waveform/full-waveform.js +193 -0
  29. package/dist/waveform/full-waveform.js.map +1 -0
  30. package/dist/waveform/index.d.ts +23 -0
  31. package/dist/waveform/index.d.ts.map +1 -0
  32. package/dist/waveform/index.js +19 -0
  33. package/dist/waveform/index.js.map +1 -0
  34. package/dist/waveform/live-waveform.d.ts +99 -0
  35. package/dist/waveform/live-waveform.d.ts.map +1 -0
  36. package/dist/waveform/live-waveform.js +191 -0
  37. package/dist/waveform/live-waveform.js.map +1 -0
  38. package/dist/waveform/shared.d.ts +96 -0
  39. package/dist/waveform/shared.d.ts.map +1 -0
  40. package/dist/waveform/shared.js +150 -0
  41. package/dist/waveform/shared.js.map +1 -0
  42. package/package.json +69 -3
  43. package/registry.json +28 -0
  44. package/src/index.ts +2 -0
  45. package/src/kit.ts +22 -0
  46. package/src/nodes.ts +19 -0
  47. package/src/waveform/envelope.ts +143 -0
  48. package/src/waveform/full-waveform.ts +218 -0
  49. package/src/waveform/index.ts +30 -0
  50. package/src/waveform/live-waveform.ts +221 -0
  51. package/src/waveform/shared.ts +194 -0
  52. package/README.md +0 -4
@@ -0,0 +1,194 @@
1
+ import { Graphics2D, type Fill, type FillResolved } from "@motionscript/core"
2
+
3
+ import type { EnvelopeBar } from "./envelope"
4
+
5
+ /**
6
+ * Geometry and paint shared by the two waveform nodes.
7
+ *
8
+ * They draw the same object — a row of bars whose heights are loudness — and
9
+ * differ only in what decides the heights: the whole file for one, a window
10
+ * around the playhead for the other. Everything about *how a row of bars is
11
+ * laid out and painted* is therefore common, and lives here so the two cannot
12
+ * drift into looking like different components.
13
+ */
14
+
15
+ /** How a bar is grown out of its baseline. */
16
+ export const WAVEFORM_ALIGNMENTS = ["center", "bottom"] as const
17
+ export type WaveformAlignment = (typeof WAVEFORM_ALIGNMENTS)[number]
18
+
19
+ /** What the bars are shaped like. */
20
+ export const WAVEFORM_STYLES = ["bars", "line", "blocks"] as const
21
+ export type WaveformStyle = (typeof WAVEFORM_STYLES)[number]
22
+
23
+ /**
24
+ * The colours voices are drawn in when nobody has assigned any.
25
+ *
26
+ * The same eight the app's speaker library suggests, in the same order, so a
27
+ * transcript whose voices are still unnamed already draws in the colours the
28
+ * transcript panel is showing them in. Duplicated as literals rather than
29
+ * imported because `scene-core` knows nothing about the app's speaker table —
30
+ * and must not, since a scene has to build in a headless render with no
31
+ * database in scope.
32
+ */
33
+ export const VOICE_PALETTE = [
34
+ "#6366f1",
35
+ "#14b8a6",
36
+ "#f59e0b",
37
+ "#ec4899",
38
+ "#22c55e",
39
+ "#3b82f6",
40
+ "#f97316",
41
+ "#a855f7",
42
+ ] as const
43
+
44
+ /** The colour a voice takes, wrapping past the end of the palette. */
45
+ export function voiceColor(index: number): string {
46
+ return VOICE_PALETTE[
47
+ ((index % VOICE_PALETTE.length) + VOICE_PALETTE.length) %
48
+ VOICE_PALETTE.length
49
+ ]
50
+ }
51
+
52
+ /** Where one bar sits and how tall it came out. */
53
+ export interface BarBox {
54
+ x: number
55
+ y: number
56
+ width: number
57
+ height: number
58
+ }
59
+
60
+ /**
61
+ * Lays a row of bars out across a box.
62
+ *
63
+ * The gap is stated as a *fraction of the pitch* rather than in pixels, and
64
+ * that is the one decision here worth stating out loud: the bar count is a
65
+ * control, so a gap in pixels would silently become a solid block at 200 bars
66
+ * and a picket fence at 20. As a fraction, the row keeps the same rhythm at
67
+ * every density — which is what lets the count be a slider anyone can drag.
68
+ *
69
+ * A bar is never thinner than a hairline and never shorter than one either: a
70
+ * silent passage has to read as "audio, and nothing here" rather than as a hole
71
+ * in the drawing, exactly as it does on the timeline's clip bars.
72
+ */
73
+ export function layOutBars(
74
+ bars: readonly EnvelopeBar[],
75
+ options: {
76
+ width: number
77
+ height: number
78
+ gap: number
79
+ align: WaveformAlignment
80
+ /** Multiplies every magnitude before it is clamped back into 0…1. */
81
+ gain: number
82
+ /** Fraction of the box a silent bar still occupies. */
83
+ floor: number
84
+ }
85
+ ): BarBox[] {
86
+ const { width, height, gap, align, gain, floor } = options
87
+ const count = bars.length
88
+ if (count === 0 || width <= 0 || height <= 0) return []
89
+
90
+ const pitch = width / count
91
+ const barWidth = Math.max(1, pitch * (1 - clamp01(gap)))
92
+ // The tallest a bar may be. Centre-aligned bars grow both ways from the
93
+ // middle, so the *half* is what a magnitude of 1 fills.
94
+ const full = align === "center" ? height / 2 : height
95
+
96
+ const out: BarBox[] = new Array(count)
97
+ for (let i = 0; i < count; i++) {
98
+ const magnitude = clamp01(bars[i].magnitude * gain)
99
+ const reach = Math.max(full * clamp01(floor), full * magnitude)
100
+ const x = -width / 2 + pitch * i + (pitch - barWidth) / 2
101
+
102
+ out[i] =
103
+ align === "center"
104
+ ? { x, y: -reach, width: barWidth, height: Math.max(1, reach * 2) }
105
+ : {
106
+ x,
107
+ y: height / 2 - reach,
108
+ width: barWidth,
109
+ height: Math.max(1, reach),
110
+ }
111
+ }
112
+ return out
113
+ }
114
+
115
+ /**
116
+ * Draws a run of bars in one paint.
117
+ *
118
+ * Batched by colour by the caller, and this is why: every `Graphics2D` handed to
119
+ * `ctx.draw` is a surface the renderer composites, so a bar apiece would be a
120
+ * thousand surfaces a frame. Chaining the rects of one colour into a single
121
+ * `Graphics2D` before filling makes it one — and, as a bonus, a gradient fill
122
+ * maps across the whole run rather than restarting inside every bar, which is
123
+ * the behaviour anyone reaching for a gradient here actually wants.
124
+ */
125
+ export function paintBars(
126
+ boxes: readonly BarBox[],
127
+ fill: Fill,
128
+ radius: number
129
+ ): Graphics2D | null {
130
+ if (boxes.length === 0) return null
131
+ if ((fill as FillResolved[]).length === 0) return null
132
+
133
+ const graphics = new Graphics2D()
134
+ for (const box of boxes) {
135
+ graphics.rect({
136
+ x: box.x + box.width / 2,
137
+ y: box.y + box.height / 2,
138
+ width: box.width,
139
+ height: box.height,
140
+ // Never rounder than the bar is wide, or a thin bar becomes a lozenge
141
+ // whose ends taper away to nothing.
142
+ cornerRadius: Math.min(radius, box.width / 2, box.height / 2),
143
+ })
144
+ }
145
+ return graphics.fill(fill)
146
+ }
147
+
148
+ /**
149
+ * Groups bars by the paint they take, preserving order within each group.
150
+ *
151
+ * The seam that makes {@link paintBars} worth batching: a diarized waveform has
152
+ * as many colours as it has voices, not as many as it has bars, so a
153
+ * conversation of two people is two draws however long it runs.
154
+ */
155
+ export function groupByPaint<K>(
156
+ boxes: readonly BarBox[],
157
+ keyOf: (index: number) => K
158
+ ): Map<K, BarBox[]> {
159
+ const groups = new Map<K, BarBox[]>()
160
+ for (let i = 0; i < boxes.length; i++) {
161
+ const key = keyOf(i)
162
+ const run = groups.get(key)
163
+ if (run) run.push(boxes[i])
164
+ else groups.set(key, [boxes[i]])
165
+ }
166
+ return groups
167
+ }
168
+
169
+ export function clamp01(value: number): number {
170
+ return value < 0 ? 0 : value > 1 ? 1 : value
171
+ }
172
+
173
+ /**
174
+ * The {@link groupByPaint} key one bar of a live visualiser takes.
175
+ *
176
+ * Filtered to one voice, every bar is that voice regardless of `byVoice` or
177
+ * whose turn the bucket originally belonged to. That second part is the one
178
+ * worth spelling out: {@link forSpeaker} silences a bar by zeroing its
179
+ * magnitude but leaves `speaker` exactly as it was, so a bar sitting in the
180
+ * middle of a *different* voice's turn is still stamped with that voice's
181
+ * index — grouping by `byVoice`'s usual per-bar key would paint those silent
182
+ * stretches in whoever used to be talking there, in flickers too short to
183
+ * read as anything but noise. Forcing every bar under the filtered voice's own
184
+ * key is the true picture instead: the node is drawing exactly one person, on
185
+ * or off, never anyone else's colour.
186
+ */
187
+ export function livePaintKey(
188
+ barSpeaker: number,
189
+ byVoice: boolean,
190
+ filteredVoice: number | null
191
+ ): string {
192
+ if (filteredVoice !== null) return `voice:${filteredVoice}`
193
+ return byVoice ? `voice:${barSpeaker}` : "all"
194
+ }
package/README.md DELETED
@@ -1,4 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
4
- If no other versions are published within 30 days, this package and version will be deleted.