@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.
- package/CHANGELOG.md +5 -0
- package/LICENSE +201 -0
- package/dist/browser/chunks/chunk-U5OUQJHQ.js +2 -0
- package/dist/browser/chunks/chunk-U5OUQJHQ.js.map +7 -0
- package/dist/browser/index.js +2 -0
- package/dist/browser/index.js.map +7 -0
- package/dist/browser/kit.js +2 -0
- package/dist/browser/kit.js.map +7 -0
- package/dist/browser/manifest.json +12 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/kit.d.ts +13 -0
- package/dist/kit.d.ts.map +1 -0
- package/dist/kit.js +11 -0
- package/dist/kit.js.map +1 -0
- package/dist/nodes.d.ts +19 -0
- package/dist/nodes.d.ts.map +1 -0
- package/dist/nodes.js +19 -0
- package/dist/nodes.js.map +1 -0
- package/dist/waveform/envelope.d.ts +87 -0
- package/dist/waveform/envelope.d.ts.map +1 -0
- package/dist/waveform/envelope.js +96 -0
- package/dist/waveform/envelope.js.map +1 -0
- package/dist/waveform/full-waveform.d.ts +104 -0
- package/dist/waveform/full-waveform.d.ts.map +1 -0
- package/dist/waveform/full-waveform.js +193 -0
- package/dist/waveform/full-waveform.js.map +1 -0
- package/dist/waveform/index.d.ts +23 -0
- package/dist/waveform/index.d.ts.map +1 -0
- package/dist/waveform/index.js +19 -0
- package/dist/waveform/index.js.map +1 -0
- package/dist/waveform/live-waveform.d.ts +99 -0
- package/dist/waveform/live-waveform.d.ts.map +1 -0
- package/dist/waveform/live-waveform.js +191 -0
- package/dist/waveform/live-waveform.js.map +1 -0
- package/dist/waveform/shared.d.ts +96 -0
- package/dist/waveform/shared.d.ts.map +1 -0
- package/dist/waveform/shared.js +150 -0
- package/dist/waveform/shared.js.map +1 -0
- package/package.json +69 -3
- package/registry.json +28 -0
- package/src/index.ts +2 -0
- package/src/kit.ts +22 -0
- package/src/nodes.ts +19 -0
- package/src/waveform/envelope.ts +143 -0
- package/src/waveform/full-waveform.ts +218 -0
- package/src/waveform/index.ts +30 -0
- package/src/waveform/live-waveform.ts +221 -0
- package/src/waveform/shared.ts +194 -0
- 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.
|