@motionscript/audio 0.1.0-alpha.5 → 0.1.0-alpha.7
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 +24 -0
- package/README.md +1 -1
- package/dist/browser/index.js +1 -1
- package/dist/browser/index.js.map +3 -3
- package/dist/browser/manifest.json +1 -1
- package/dist/nodes.d.ts +6 -9
- package/dist/nodes.d.ts.map +1 -1
- package/dist/nodes.js +7 -9
- package/dist/nodes.js.map +1 -1
- package/dist/waveform/full-waveform.d.ts +88 -73
- package/dist/waveform/full-waveform.d.ts.map +1 -1
- package/dist/waveform/full-waveform.js +114 -160
- package/dist/waveform/full-waveform.js.map +1 -1
- package/dist/waveform/live-waveform.d.ts +93 -63
- package/dist/waveform/live-waveform.d.ts.map +1 -1
- package/dist/waveform/live-waveform.js +92 -134
- package/dist/waveform/live-waveform.js.map +1 -1
- package/package.json +4 -4
- package/src/nodes.ts +8 -10
- package/src/waveform/full-waveform.ts +128 -157
- package/src/waveform/live-waveform.ts +113 -128
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# @motionscript/audio
|
|
2
2
|
|
|
3
|
+
## 0.1.0-alpha.7
|
|
4
|
+
|
|
5
|
+
## 0.1.0-alpha.6
|
|
6
|
+
|
|
7
|
+
### Minor Changes
|
|
8
|
+
|
|
9
|
+
- 163b708: Node types are components: one spec object handed to `component2d`, `component3d` or `componentAudio`, with no classes, decorators or JSX.
|
|
10
|
+
|
|
11
|
+
- **Writing a node type.** A component is one object literal: `key`, `state` (literals and `Types`), `defaults`, `commands`, `children`, `allowedChildren`/`allowedParents`, `provide`, `load`, and its tree's own fields (`measure`, `layout`, `render` for 2D). Behaviour comes from modules you spread in and call: `Paint`, `Flow`, `Shapes`, `Typeface`, `TextLayout`, `Aim`, `Material`, `View3D`. ADR 0007 and the custom-node docs cover it.
|
|
12
|
+
- **Building nodes in code** uses calls: `Rect({ fill: 'tomato', children: [Text({ text: 'Hi' })] })`, or `new Rect({...})` for the node itself.
|
|
13
|
+
- **Every built-in and every kit node is a component**, under the same document key. Code that tested `instanceof` a class now uses the component's `.is`, or `instanceof X.node`. Changed members:
|
|
14
|
+
- `SurfacePlot3d.valueAt` and `FunctionPlot.valueAt` are now the functions `surfaceValueAt` and `curveValue`.
|
|
15
|
+
- A system's `coordinates` is replaced by `planeMap`/`axesMap`/`systemMap`.
|
|
16
|
+
- `PieChart.isSelected` is replaced by `isSliceSelected`.
|
|
17
|
+
- `ItemGrid` is a kit module (`ItemGrid.isGrid`, `ItemGrid.define`), not a base class.
|
|
18
|
+
- **The sdk no longer exports the class surface:**
|
|
19
|
+
- the decorators: `node`, `property` and every `*Property`, `command`, `withdraw`, `driveCommand`;
|
|
20
|
+
- the authoring bases: `PaintNode`, `ShapeNode`, `TextBase`, `Mesh3DBase`, `Canvas3DBase` and the `*Base` geometry classes;
|
|
21
|
+
- `asCommand`, `compose`, `drive` and `declarePaints` from `@motionscript/sdk/component`;
|
|
22
|
+
- the `jsx-runtime` and `jsx-dev-runtime` subpaths.
|
|
23
|
+
- `Node`, `Node2D`, `Node3D` and `NodeAudio` remain as types.
|
|
24
|
+
- **Editor metadata leaves the engine.** The kits no longer declare `parentKey`, `forkable`, `layout`, `seed`, `within`, `beside` or `closed`; a host keeps those in its own configs. `describe()` now reports `holds`, the trees a type takes children from, and the placement checks use it, so a 2D viewport of your own that takes 3D children places them.
|
|
25
|
+
- A command called from code with no argument runs, and a refused child is told what its parent takes (`allowedChildren`) or where it goes (`allowedParents`).
|
|
26
|
+
|
|
3
27
|
## 0.1.0-alpha.5
|
|
4
28
|
|
|
5
29
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ with `Array.from` before it puts the envelope in a document.
|
|
|
47
47
|
|
|
48
48
|
## Nodes
|
|
49
49
|
|
|
50
|
-
| Key |
|
|
50
|
+
| Key | Made for (w x h) | Commands | Notes |
|
|
51
51
|
|---|---|---|---|
|
|
52
52
|
| `fullWaveform` | 900 x 160 | `sweep` | whole recording; `progress` is the played share, or with `follow` the node's elapsed time over `envelope.duration`. `sweep` is a `to` of `progress` to 1 |
|
|
53
53
|
| `liveWaveform` | 420 x 220 | none | a `window` of seconds centred on `offset + elapsed` (or frozen at `offset` when `playing` is off); `style` is `bars`, `line` or `blocks`; `speaker` (-1 is every voice) filters to one voice |
|
package/dist/browser/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{a as
|
|
1
|
+
import{a as p,b as s,c as w,d as W,e as L,f as T,g as f,h as c,i as u,j as m,k as g,l as B}from"./chunks/chunk-DAFIVUTT.js";import{Types as M,component2d as C,easeInOut as S,fillOps as N}from"@motionscript/sdk";var P=o=>M.custom(o,{resolve:N.resolve,lerp:N.lerp}),O=C({key:"fullWaveform",state:{envelope:M.custom(p),bars:120,gap:.3,radius:2,progress:0,follow:!1,gain:1,floor:.015,align:M.custom("center"),byVoice:!1,voiceColors:M.custom([]),fill:P("#5b5b7a"),playedFill:P("#6366f1")},load:(o,e)=>{o.fill(e.fill),o.fill(e.playedFill)},commands:{sweep:(o,e)=>{let r=o.easing?null:S(),n=e.progress;return o.animate(t=>({progress:n+(1-n)*(r?r(t):t)}))}},render:(o,e)=>{_(o,e),o.children()}});function _(o,e){let{width:r,height:n}=o.size;if(r<=0||n<=0)return;let t=Math.max(1,Math.round(e.bars)),a=s(e.envelope,0,e.envelope.duration,t),l=c(a,{width:r,height:n,gap:e.gap,align:e.align,gain:e.gain,floor:e.floor});if(l.length===0)return;let v=A(o,e)*l.length,h=m(l,i=>e.byVoice?`voice:${a[i].speaker}`:i<v?"played":"ahead");for(let[i,y]of h){let E=u(y,Y(e,i),e.radius);E&&o.draw(E)}}function A(o,e){if(!e.follow)return g(e.progress);let r=e.envelope.duration;return r<=0?0:g(o.time.elapsed/r)}function Y(o,e){if(e==="played")return o.playedFill;if(e==="ahead")return o.fill;let r=Number(e.slice(6));return!Number.isFinite(r)||r<0?o.fill:o.voiceColors[r]??f(r)}import{Types as d,component2d as I,fillOps as x}from"@motionscript/sdk";var F=I({key:"liveWaveform",state:{envelope:d.custom(p),bars:48,gap:.35,radius:4,window:3,offset:0,playing:!0,gain:1.4,floor:.04,align:d.custom("center"),style:d.custom("bars"),speaker:-1,byVoice:!1,voiceColors:d.custom([]),fill:d.custom("#6366f1",{resolve:x.resolve,lerp:x.lerp})},load:(o,e)=>o.fill(e.fill),render:(o,e)=>{D(o,e),o.children()}});function z(o,e){return e.offset+(e.playing?Math.max(0,o.time.elapsed):0)}function D(o,e){let{width:r,height:n}=o.size;if(r<=0||n<=0)return;let t=Math.max(1,Math.round(e.bars)),a=Math.max(.05,e.window),l=z(o,e),v=e.speaker<0?null:Math.round(e.speaker),h=w(s(e.envelope,l-a/2,l+a/2,t),v),i=e.style==="line"?G(h):h,y=c(i,{width:r,height:n,gap:e.style==="blocks"?.12:e.gap,align:e.align,gain:e.gain,floor:e.floor});if(y.length===0)return;let E=m(y,b=>B(i[b].speaker,e.byVoice,v));for(let[b,V]of E){let k=u(V,R(e,b),e.radius);k&&o.draw(k)}}function R(o,e){if(e==="all")return o.fill;let r=Number(e.slice(6));return!Number.isFinite(r)||r<0?o.fill:o.voiceColors[r]??f(r)}function G(o){let e=o.length;if(e<3)return[...o];let r=Math.max(1,Math.floor(e/5));return o.map((n,t)=>{let a=Math.min(t,e-1-t),l=g(a/r);return{magnitude:n.magnitude*l,speaker:n.speaker}})}var K=[O,F];export{p as EMPTY_ENVELOPE,O as FullWaveform,F as LiveWaveform,K as NODES,T as VOICE_PALETTE,W as WAVEFORM_ALIGNMENTS,L as WAVEFORM_STYLES,w as forSpeaker,m as groupByPaint,c as layOutBars,u as paintBars,s as sampleEnvelope,f as voiceColor};
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/waveform/full-waveform.ts", "../../src/waveform/live-waveform.ts", "../../src/nodes.ts"],
|
|
4
|
-
"sourcesContent": ["import {\r\n Node2D,\r\n node,\r\n command,\r\n easeInOut,\r\n fillOps,\r\n property,\r\n type AssetScope,\r\n type CommandArgs,\r\n type Fill,\r\n type FillResolved,\r\n type Node2DProps,\r\n type RenderContext2D,\r\n} from \"@motionscript/sdk\"\r\n\r\nimport { declarePaints } from \"@motionscript/sdk/component\"\r\nimport type { Seekable } from \"@motionscript/sdk/component\"\r\nimport { EMPTY_ENVELOPE, sampleEnvelope, type AudioEnvelope } from \"./envelope\"\r\nimport {\r\n clamp01,\r\n groupByPaint,\r\n layOutBars,\r\n paintBars,\r\n voiceColor,\r\n type WaveformAlignment,\r\n} from \"./shared\"\r\n\r\nexport interface FullWaveformProps extends Node2DProps {\r\n /** The baked recording. See {@link AudioEnvelope} for why it is baked. */\r\n envelope: AudioEnvelope\r\n /** How many columns the whole file is drawn as. */\r\n bars: number\r\n /** Space between bars, as a fraction of the pitch. See {@link layOutBars}. */\r\n gap: number\r\n /** Corner radius of a bar, capped at half its width. */\r\n radius: number\r\n /** How much of the file reads as played, `0`–`1`. Tweenable. */\r\n progress: number\r\n /** Drive {@link progress} from the node's own clock instead of the prop. */\r\n follow: boolean\r\n /** Multiplies every magnitude — a quiet track pulled up to fill its box. */\r\n gain: number\r\n /** How much of the box a silent bar still occupies, `0`–`1`. */\r\n floor: number\r\n align: WaveformAlignment\r\n /** Colour each voice's turns rather than splitting on played / not played. */\r\n byVoice: boolean\r\n /** A colour per voice index. Short or empty falls back to the palette. */\r\n voiceColors: string[]\r\n /** The bars ahead of the playhead. */\r\n fill: Fill\r\n /** …and the bars behind it. */\r\n playedFill: Fill\r\n}\r\n\r\n/**\r\n * A **waveform overview**: the whole recording's shape, drawn at once.\r\n *\r\n * The picture every audio editor opens with, and the one a podcast clip puts\r\n * under its caption — a static silhouette of the file, with a played portion\r\n * that advances. It answers \"how long is this and where are the loud parts\",\r\n * which is a question about the *file*, not about the moment; its sibling\r\n * {@link LiveWaveform} answers the other one.\r\n *\r\n * ## Progress is authored, not assumed\r\n *\r\n * {@link follow} is off by default, and that default is the considered one. A\r\n * scene has no idea where the recording it is drawing sits on the video's audio\r\n * lane — a clip is placed, trimmed and sped up out there, and none of that is\r\n * visible from in here. So the honest default is that the fill is *animation*:\r\n * the author sweeps it with a command, the same as any other tween, and it lands\r\n * wherever they put it. `follow` is the shortcut for the common arrangement\r\n * where the node appears exactly as the track starts, and it says so by being a\r\n * switch someone has to turn on rather than a behaviour that quietly disagrees\r\n * with the mix.\r\n *\r\n * ## Voices instead of progress\r\n *\r\n * With {@link byVoice} on, the played/ahead split is replaced by one colour per\r\n * speaking voice, at a size you can read. For a two-hander it is a map of the conversation: who has the\r\n * floor, where the turns are, how long each one ran. It replaces the progress\r\n * colours rather than combining with them, because a bar cannot mean two things\r\n * at once and \"played, and also Grace\" is a legend nobody can hold in their\r\n * head.\r\n */\r\n@node({\r\n key: \"fullWaveform\",\r\n parentKey: \"node\",\r\n forkable: true,\r\n layout: {\r\n children: \"freeform\",\r\n defaultWidthMode: \"fixed\",\r\n defaultHeightMode: \"fixed\",\r\n acceptsChildren: true,\r\n },\r\n seed: {\r\n width: 900,\r\n height: 160,\r\n },\r\n})\r\nexport class FullWaveform extends Node2D<FullWaveformProps> {\r\n @property({ default: EMPTY_ENVELOPE }) declare envelope: AudioEnvelope\r\n @property({ default: 120 }) declare bars: number\r\n @property({ default: 0.3 }) declare gap: number\r\n @property({ default: 2 }) declare radius: number\r\n @property({ default: 0 }) declare progress: number\r\n @property({ default: false }) declare follow: boolean\r\n @property({ default: 1 }) declare gain: number\r\n @property({ default: 0.015 }) declare floor: number\r\n @property({ default: \"center\" }) declare align: WaveformAlignment\r\n @property({ default: false }) declare byVoice: boolean\r\n @property({ default: [] }) declare voiceColors: string[]\r\n\r\n @property({\r\n default: \"#5b5b7a\",\r\n mapper: fillOps.resolve,\r\n tween: fillOps.lerp,\r\n })\r\n declare fill: Fill\r\n\r\n @property({\r\n default: \"#6366f1\",\r\n mapper: fillOps.resolve,\r\n tween: fillOps.lerp,\r\n })\r\n declare playedFill: Fill\r\n\r\n /**\r\n * Fill the waveform in, left to right — the node's one command, since\r\n * everything else about it is expressible as an ordinary `to`.\r\n */\r\n @command()\r\n sweep(args: CommandArgs<Record<string, never>> & { duration: number }): Seekable {\r\n return this.to({\r\n data: { progress: 1 } as Partial<FullWaveformProps>,\r\n duration: args.duration,\r\n easing: args.easing ?? easeInOut(),\r\n })\r\n }\r\n\r\n /**\r\n * This node extends `Node2D` rather than `ShapeNode`, so nothing declares the\r\n * paint props it carries — see {@link declarePaints}. `playedFill` is named\r\n * past the conventional slots, so it is passed explicitly.\r\n */\r\n override declareAssets(assets: AssetScope): void {\r\n super.declareAssets(assets)\r\n declarePaints(this, assets, { fills: [this.playedFill as FillResolved[]] })\r\n }\r\n\r\n /** Where the fill has reached, from whichever source is in charge. */\r\n private get played(): number {\r\n if (!this.follow) return clamp01(this.progress)\r\n const length = this.envelope.duration\r\n if (length <= 0) return 0\r\n return clamp01(this.time.elapsed / length)\r\n }\r\n\r\n protected renderSelf(ctx: RenderContext2D): void {\r\n const rect = this.layoutBounds\r\n const width = rect?.width ?? 0\r\n const height = rect?.height ?? 0\r\n if (width <= 0 || height <= 0) return\r\n\r\n const count = Math.max(1, Math.round(this.bars))\r\n const sampled = sampleEnvelope(\r\n this.envelope,\r\n 0,\r\n this.envelope.duration,\r\n count\r\n )\r\n const boxes = layOutBars(sampled, {\r\n width,\r\n height,\r\n gap: this.gap,\r\n align: this.align,\r\n gain: this.gain,\r\n floor: this.floor,\r\n })\r\n if (boxes.length === 0) return\r\n\r\n // One draw per colour rather than per bar — see {@link paintBars}. The key\r\n // is a string because it has to be comparable; the paint it stands for is\r\n // resolved after the grouping.\r\n const edge = this.played * boxes.length\r\n const groups = groupByPaint(boxes, (i) =>\r\n this.byVoice\r\n ? `voice:${sampled[i].speaker}`\r\n : i < edge\r\n ? \"played\"\r\n : \"ahead\"\r\n )\r\n\r\n for (const [key, run] of groups) {\r\n const graphics = paintBars(run, this.paintFor(key), this.radius)\r\n if (graphics) ctx.draw(graphics)\r\n }\r\n }\r\n\r\n /**\r\n * The paint a group takes.\r\n *\r\n * A voice's colour is a **colour string**, never one of the node's two fill\r\n * props, and that is deliberate: a voice is identified by hue, so a gradient\r\n * or an image there would defeat the only thing the colour is for. It also\r\n * means these paints declare no assets, which is why {@link declareAssets}\r\n * only has to account for the two real fill props.\r\n */\r\n private paintFor(key: string): Fill {\r\n if (key === \"played\") return this.playedFill\r\n if (key === \"ahead\") return this.fill\r\n\r\n const voice = Number(key.slice(\"voice:\".length))\r\n if (!Number.isFinite(voice) || voice < 0) return this.fill\r\n return this.voiceColors[voice] ?? voiceColor(voice)\r\n }\r\n}\r\n", "import {\r\n Node2D,\r\n node,\r\n fillOps,\r\n property,\r\n type AssetScope,\r\n type Fill,\r\n type Node2DProps,\r\n type RenderContext2D,\r\n} from \"@motionscript/sdk\"\r\n\r\nimport { declarePaints } from \"@motionscript/sdk/component\"\r\nimport {\r\n EMPTY_ENVELOPE,\r\n forSpeaker,\r\n sampleEnvelope,\r\n type AudioEnvelope,\r\n type EnvelopeBar,\r\n} from \"./envelope\"\r\nimport {\r\n clamp01,\r\n groupByPaint,\r\n layOutBars,\r\n livePaintKey,\r\n paintBars,\r\n voiceColor,\r\n type WaveformAlignment,\r\n type WaveformStyle,\r\n} from \"./shared\"\r\n\r\nexport interface LiveWaveformProps extends Node2DProps {\r\n /** The baked recording. See {@link AudioEnvelope} for why it is baked. */\r\n envelope: AudioEnvelope\r\n /** How many bars the visualiser is made of. */\r\n bars: number\r\n /** Space between bars, as a fraction of the pitch. */\r\n gap: number\r\n radius: number\r\n /** How many seconds of audio are on screen at once. */\r\n window: number\r\n /** Seconds into the file the node's first frame shows. */\r\n offset: number\r\n /** Advance with the scene clock. Off freezes the picture at {@link offset}. */\r\n playing: boolean\r\n /** Multiplies every magnitude before it is clamped. */\r\n gain: number\r\n /** How much of the box a silent bar still occupies, `0`–`1`. */\r\n floor: number\r\n align: WaveformAlignment\r\n style: WaveformStyle\r\n /**\r\n * Show only this voice's activity, by index into the recording's own\r\n * numbering. `-1` — the default — is every voice.\r\n */\r\n speaker: number\r\n /** Colour each bar by whoever is speaking in it. */\r\n byVoice: boolean\r\n /** A colour per voice index. Short or empty falls back to the palette. */\r\n voiceColors: string[]\r\n fill: Fill\r\n}\r\n\r\n/**\r\n * A **live audio visualiser**: bars that move with the recording as it plays.\r\n *\r\n * The thing every podcast-to-video tool puts beside the artwork. It is not a\r\n * picture of the file — that is {@link FullWaveform} — it is a picture of *this\r\n * moment*, and what it is for is making a static frame of a talking head feel\r\n * like it is playing.\r\n *\r\n * ## \"Live\" without listening\r\n *\r\n * Nothing here reads an audio device. The bars are sampled out of a baked\r\n * envelope at the node's own elapsed time, which is what makes the visualiser\r\n * survive the thing it exists for: an **export**, where there is no playback to\r\n * analyse and frames may be rendered out of order or in parallel. A frame is a\r\n * pure function of its time, so scrubbing back and forth over the visualiser\r\n * redraws exactly what it drew the first time — which an analyser node, by\r\n * construction, cannot promise. See {@link AudioEnvelope}.\r\n *\r\n * ## The speaker filter\r\n *\r\n * {@link speaker} narrows the node to one voice of a diarized recording, and it\r\n * is the reason this node knows about transcripts at all. Two visualisers beside\r\n * two headshots, each filtered to its own host, is a podcast edit that would\r\n * otherwise be done by hand for every turn — and because the filter *silences*\r\n * rather than skips (see {@link forSpeaker}), the quiet host visibly stops\r\n * talking instead of vanishing. Nothing about that requires the words: only who\r\n * is talking when, which is what the diarizer produces.\r\n *\r\n * A filter naming a voice the recording doesn't have draws a resting row rather\r\n * than everything — a picture of \"this person never speaks here\", which is\r\n * true, where falling back to the unfiltered mix would be a silent lie about\r\n * whose voice you were watching.\r\n */\r\n@node({\r\n key: \"liveWaveform\",\r\n parentKey: \"node\",\r\n forkable: true,\r\n layout: {\r\n children: \"freeform\",\r\n defaultWidthMode: \"fixed\",\r\n defaultHeightMode: \"fixed\",\r\n acceptsChildren: true,\r\n },\r\n seed: {\r\n width: 420,\r\n height: 220,\r\n },\r\n})\r\nexport class LiveWaveform extends Node2D<LiveWaveformProps> {\r\n @property({ default: EMPTY_ENVELOPE }) declare envelope: AudioEnvelope\r\n @property({ default: 48 }) declare bars: number\r\n @property({ default: 0.35 }) declare gap: number\r\n @property({ default: 4 }) declare radius: number\r\n @property({ default: 3 }) declare window: number\r\n @property({ default: 0 }) declare offset: number\r\n @property({ default: true }) declare playing: boolean\r\n @property({ default: 1.4 }) declare gain: number\r\n @property({ default: 0.04 }) declare floor: number\r\n @property({ default: \"center\" }) declare align: WaveformAlignment\r\n @property({ default: \"bars\" }) declare style: WaveformStyle\r\n @property({ default: -1 }) declare speaker: number\r\n @property({ default: false }) declare byVoice: boolean\r\n @property({ default: [] }) declare voiceColors: string[]\r\n\r\n @property({\r\n default: \"#6366f1\",\r\n mapper: fillOps.resolve,\r\n tween: fillOps.lerp,\r\n })\r\n declare fill: Fill\r\n\r\n override declareAssets(assets: AssetScope): void {\r\n super.declareAssets(assets)\r\n declarePaints(this, assets)\r\n }\r\n\r\n /**\r\n * The second of the recording the visualiser is showing.\r\n *\r\n * Off the node's own clock rather than the scene's, which is what \"from the\r\n * moment this node appeared\" means — the same clock the engine's own\r\n * `Video` times its picture from, and the same one that makes a node dropped\r\n * halfway through a clip start its recording at its entrance rather than\r\n * partway in.\r\n */\r\n private get at(): number {\r\n return this.offset + (this.playing ? Math.max(0, this.time.elapsed) : 0)\r\n }\r\n\r\n protected renderSelf(ctx: RenderContext2D): void {\r\n const rect = this.layoutBounds\r\n const width = rect?.width ?? 0\r\n const height = rect?.height ?? 0\r\n if (width <= 0 || height <= 0) return\r\n\r\n const count = Math.max(1, Math.round(this.bars))\r\n const span = Math.max(0.05, this.window)\r\n const at = this.at\r\n const voice = this.speaker < 0 ? null : Math.round(this.speaker)\r\n\r\n // The window is centred on the playhead, so the bars run *toward* the\r\n // middle and away again — the arrangement every mixer's meter uses, and the\r\n // one that reads as sound arriving rather than as a strip scrolling past.\r\n const sampled = forSpeaker(\r\n sampleEnvelope(this.envelope, at - span / 2, at + span / 2, count),\r\n voice\r\n )\r\n\r\n const shaped = this.style === \"line\" ? taper(sampled) : sampled\r\n const boxes = layOutBars(shaped, {\r\n width,\r\n height,\r\n gap: this.style === \"blocks\" ? 0.12 : this.gap,\r\n align: this.align,\r\n gain: this.gain,\r\n floor: this.floor,\r\n })\r\n if (boxes.length === 0) return\r\n\r\n const groups = groupByPaint(boxes, (i) =>\r\n livePaintKey(shaped[i].speaker, this.byVoice, voice)\r\n )\r\n\r\n for (const [key, run] of groups) {\r\n const graphics = paintBars(run, this.paintFor(key), this.radius)\r\n if (graphics) ctx.draw(graphics)\r\n }\r\n }\r\n\r\n /** See {@link FullWaveform.paintFor} — a voice is a hue, never a gradient. */\r\n private paintFor(key: string): Fill {\r\n if (key === \"all\") return this.fill\r\n const voice = Number(key.slice(\"voice:\".length))\r\n if (!Number.isFinite(voice) || voice < 0) return this.fill\r\n return this.voiceColors[voice] ?? voiceColor(voice)\r\n }\r\n}\r\n\r\n/**\r\n * Rolls the ends of a window off toward silence.\r\n *\r\n * What separates the `line` style from `bars`: a hard-edged window makes the\r\n * loud bar entering at the right edge appear from nothing, which reads as a\r\n * glitch rather than as sound arriving. Fading the outer fifth turns the same\r\n * data into a shape that grows out of the baseline — the profile a spectrum\r\n * display has, and the reason it looks like one object rather than a row of\r\n * separate meters.\r\n */\r\nfunction taper(bars: readonly EnvelopeBar[]): EnvelopeBar[] {\r\n const count = bars.length\r\n if (count < 3) return [...bars]\r\n const edge = Math.max(1, Math.floor(count / 5))\r\n\r\n return bars.map((bar, i) => {\r\n const fromEnd = Math.min(i, count - 1 - i)\r\n const weight = clamp01(fromEnd / edge)\r\n return { magnitude: bar.magnitude * weight, speaker: bar.speaker }\r\n })\r\n}\r\n", "import { FullWaveform } from \"./waveform/full-waveform\";\r\nimport { LiveWaveform } from \"./waveform/live-waveform\";\r\n\r\n/**\r\n * Every node type this package publishes.\r\n *\r\n * A host registers these by handing the array to an engine —\r\n * `new Engine(platform, { nodes: NODES })` — which reads each class's `@node()`\r\n * key. The decorator only *declares* that key: nothing is registered by the mere\r\n * act of importing a module, so a registry holds exactly what its owner asked\r\n * for and two engines in one process can differ about it.\r\n *\r\n * Listing class **values** is also what survives bundling. This package declares\r\n * `sideEffects: false`, which licenses a bundler to drop a module nothing\r\n * imports a value from, and a document names a node type by string — so\r\n * `import \"./waveform/full-waveform\"` for its side effect is exactly what gets shaken out, where an\r\n * array of bindings is a data dependency that cannot be.\r\n */\r\nexport const NODES = [FullWaveform, LiveWaveform];\r\n"],
|
|
5
|
-
"mappings": "4HAAA,OACE,
|
|
6
|
-
"names": ["
|
|
4
|
+
"sourcesContent": ["import {\r\n Types,\r\n component2d,\r\n easeInOut,\r\n fillOps,\r\n type Fill,\r\n type FillResolved,\r\n type NodeOf,\r\n type RenderContext,\r\n // Named so the emitted declaration reaches these through the sdk, not core.\r\n type Node2D as _Node2D,\r\n type Node2DProps as _Node2DProps,\r\n} from \"@motionscript/sdk\"\r\n\r\nimport { EMPTY_ENVELOPE, sampleEnvelope, type AudioEnvelope } from \"./envelope\"\r\nimport {\r\n clamp01,\r\n groupByPaint,\r\n layOutBars,\r\n paintBars,\r\n voiceColor,\r\n type WaveformAlignment,\r\n} from \"./shared\"\r\n\r\n/** A paint entry as the waveforms declare theirs: resolved and tweened like a fill, its kind read off the default. */\r\nconst paint = (initial: string) =>\r\n Types.custom<Fill, FillResolved[]>(initial, { resolve: fillOps.resolve, lerp: fillOps.lerp })\r\n\r\n/**\r\n * A **waveform overview**: the whole recording's shape, drawn at once.\r\n *\r\n * The picture every audio editor opens with, and the one a podcast clip puts\r\n * under its caption — a static silhouette of the file, with a played portion\r\n * that advances. It answers \"how long is this and where are the loud parts\",\r\n * which is a question about the *file*, not about the moment; its sibling\r\n * {@link LiveWaveform} answers the other one.\r\n *\r\n * ## Progress is authored, not assumed\r\n *\r\n * `follow` is off by default, and that default is the considered one. A\r\n * scene has no idea where the recording it is drawing sits on the video's audio\r\n * lane — a clip is placed, trimmed and sped up out there, and none of that is\r\n * visible from in here. So the honest default is that the fill is *animation*:\r\n * the author sweeps it with a command, the same as any other tween, and it lands\r\n * wherever they put it. `follow` is the shortcut for the common arrangement\r\n * where the node appears exactly as the track starts, and it says so by being a\r\n * switch someone has to turn on rather than a behaviour that quietly disagrees\r\n * with the mix.\r\n *\r\n * ## Voices instead of progress\r\n *\r\n * With `byVoice` on, the played/ahead split is replaced by one colour per\r\n * speaking voice, at a size you can read. For a two-hander it is a map of the conversation: who has the\r\n * floor, where the turns are, how long each one ran. It replaces the progress\r\n * colours rather than combining with them, because a bar cannot mean two things\r\n * at once and \"played, and also Grace\" is a legend nobody can hold in their\r\n * head.\r\n */\r\nexport const FullWaveform = component2d({\r\n key: \"fullWaveform\",\r\n state: {\r\n /** The baked recording. See {@link AudioEnvelope} for why it is baked. */\r\n envelope: Types.custom<AudioEnvelope>(EMPTY_ENVELOPE),\r\n /** How many columns the whole file is drawn as. */\r\n bars: 120,\r\n /** Space between bars, as a fraction of the pitch. See {@link layOutBars}. */\r\n gap: 0.3,\r\n /** Corner radius of a bar, capped at half its width. */\r\n radius: 2,\r\n /** How much of the file reads as played, `0`–`1`. Tweenable. */\r\n progress: 0,\r\n /** Drive `progress` from the node's own clock instead of the prop. */\r\n follow: false,\r\n /** Multiplies every magnitude — a quiet track pulled up to fill its box. */\r\n gain: 1,\r\n /** How much of the box a silent bar still occupies, `0`–`1`. */\r\n floor: 0.015,\r\n align: Types.custom<WaveformAlignment>(\"center\"),\r\n /** Colour each voice's turns rather than splitting on played / not played. */\r\n byVoice: false,\r\n /** A colour per voice index. Short or empty falls back to the palette. */\r\n voiceColors: Types.custom<string[]>([]),\r\n /** The bars ahead of the playhead. */\r\n fill: paint(\"#5b5b7a\"),\r\n /** …and the bars behind it. */\r\n playedFill: paint(\"#6366f1\"),\r\n },\r\n // Nothing declares a component's paints but itself: `fill` and `playedFill` both.\r\n load: (loader, state) => {\r\n loader.fill(state.fill)\r\n loader.fill(state.playedFill)\r\n },\r\n commands: {\r\n /**\r\n * Fill the waveform in, left to right — the node's one command, since\r\n * everything else about it is expressible as an ordinary `to`. Eased in-out\r\n * unless it is given an easing of its own.\r\n */\r\n sweep: (timeline, state) => {\r\n const ease = timeline.easing ? null : easeInOut()\r\n const from = state.progress\r\n return timeline.animate((t) => ({ progress: from + (1 - from) * (ease ? ease(t) : t) }))\r\n },\r\n },\r\n render: (ctx, state) => {\r\n drawBars(ctx, state)\r\n ctx.children()\r\n },\r\n})\r\n\r\nexport type FullWaveform = NodeOf<typeof FullWaveform>\r\nexport type FullWaveformProps = NonNullable<Parameters<typeof FullWaveform>[0]>\r\n\r\ninterface FullState {\r\n readonly envelope: AudioEnvelope\r\n readonly bars: number\r\n readonly gap: number\r\n readonly radius: number\r\n readonly progress: number\r\n readonly follow: boolean\r\n readonly gain: number\r\n readonly floor: number\r\n readonly align: WaveformAlignment\r\n readonly byVoice: boolean\r\n readonly voiceColors: string[]\r\n readonly fill: FillResolved[]\r\n readonly playedFill: FillResolved[]\r\n}\r\n\r\nfunction drawBars(ctx: RenderContext, s: FullState): void {\r\n const { width, height } = ctx.size\r\n if (width <= 0 || height <= 0) return\r\n\r\n const count = Math.max(1, Math.round(s.bars))\r\n const sampled = sampleEnvelope(s.envelope, 0, s.envelope.duration, count)\r\n const boxes = layOutBars(sampled, {\r\n width,\r\n height,\r\n gap: s.gap,\r\n align: s.align,\r\n gain: s.gain,\r\n floor: s.floor,\r\n })\r\n if (boxes.length === 0) return\r\n\r\n // One draw per colour rather than per bar — see {@link paintBars}. The key\r\n // is a string because it has to be comparable; the paint it stands for is\r\n // resolved after the grouping.\r\n const edge = played(ctx, s) * boxes.length\r\n const groups = groupByPaint(boxes, (i) =>\r\n s.byVoice\r\n ? `voice:${sampled[i].speaker}`\r\n : i < edge\r\n ? \"played\"\r\n : \"ahead\"\r\n )\r\n\r\n for (const [key, run] of groups) {\r\n const graphics = paintBars(run, paintFor(s, key), s.radius)\r\n if (graphics) ctx.draw(graphics)\r\n }\r\n}\r\n\r\n/** Where the fill has reached, from whichever source is in charge. */\r\nfunction played(ctx: RenderContext, s: FullState): number {\r\n if (!s.follow) return clamp01(s.progress)\r\n const length = s.envelope.duration\r\n if (length <= 0) return 0\r\n return clamp01(ctx.time.elapsed / length)\r\n}\r\n\r\n/**\r\n * The paint a group takes.\r\n *\r\n * A voice's colour is a **colour string**, never one of the node's two fill\r\n * props, and that is deliberate: a voice is identified by hue, so a gradient\r\n * or an image there would defeat the only thing the colour is for. It also\r\n * means these paints declare no assets, which is why `load` only has to account\r\n * for the two real fill props.\r\n */\r\nfunction paintFor(s: FullState, key: string): Fill {\r\n if (key === \"played\") return s.playedFill\r\n if (key === \"ahead\") return s.fill\r\n\r\n const voice = Number(key.slice(\"voice:\".length))\r\n if (!Number.isFinite(voice) || voice < 0) return s.fill\r\n return s.voiceColors[voice] ?? voiceColor(voice)\r\n}\r\n", "import {\r\n Types,\r\n component2d,\r\n fillOps,\r\n type Fill,\r\n type FillResolved,\r\n type NodeOf,\r\n type RenderContext,\r\n // Named so the emitted declaration reaches these through the sdk, not core.\r\n type Node2D as _Node2D,\r\n type Node2DProps as _Node2DProps,\r\n} from \"@motionscript/sdk\"\r\n\r\nimport {\r\n EMPTY_ENVELOPE,\r\n forSpeaker,\r\n sampleEnvelope,\r\n type AudioEnvelope,\r\n type EnvelopeBar,\r\n} from \"./envelope\"\r\nimport {\r\n clamp01,\r\n groupByPaint,\r\n layOutBars,\r\n livePaintKey,\r\n paintBars,\r\n voiceColor,\r\n type WaveformAlignment,\r\n type WaveformStyle,\r\n} from \"./shared\"\r\n\r\n/**\r\n * A **live audio visualiser**: bars that move with the recording as it plays.\r\n *\r\n * The thing every podcast-to-video tool puts beside the artwork. It is not a\r\n * picture of the file — that is {@link FullWaveform} — it is a picture of *this\r\n * moment*, and what it is for is making a static frame of a talking head feel\r\n * like it is playing.\r\n *\r\n * ## \"Live\" without listening\r\n *\r\n * Nothing here reads an audio device. The bars are sampled out of a baked\r\n * envelope at the node's own elapsed time, which is what makes the visualiser\r\n * survive the thing it exists for: an **export**, where there is no playback to\r\n * analyse and frames may be rendered out of order or in parallel. A frame is a\r\n * pure function of its time, so scrubbing back and forth over the visualiser\r\n * redraws exactly what it drew the first time — which an analyser node, by\r\n * construction, cannot promise. See {@link AudioEnvelope}.\r\n *\r\n * ## The speaker filter\r\n *\r\n * `speaker` narrows the node to one voice of a diarized recording, and it\r\n * is the reason this node knows about transcripts at all. Two visualisers beside\r\n * two headshots, each filtered to its own host, is a podcast edit that would\r\n * otherwise be done by hand for every turn — and because the filter *silences*\r\n * rather than skips (see {@link forSpeaker}), the quiet host visibly stops\r\n * talking instead of vanishing. Nothing about that requires the words: only who\r\n * is talking when, which is what the diarizer produces.\r\n *\r\n * A filter naming a voice the recording doesn't have draws a resting row rather\r\n * than everything — a picture of \"this person never speaks here\", which is\r\n * true, where falling back to the unfiltered mix would be a silent lie about\r\n * whose voice you were watching.\r\n */\r\nexport const LiveWaveform = component2d({\r\n key: \"liveWaveform\",\r\n state: {\r\n /** The baked recording. See {@link AudioEnvelope} for why it is baked. */\r\n envelope: Types.custom<AudioEnvelope>(EMPTY_ENVELOPE),\r\n /** How many bars the visualiser is made of. */\r\n bars: 48,\r\n /** Space between bars, as a fraction of the pitch. */\r\n gap: 0.35,\r\n radius: 4,\r\n /** How many seconds of audio are on screen at once. */\r\n window: 3,\r\n /** Seconds into the file the node's first frame shows. */\r\n offset: 0,\r\n /** Advance with the scene clock. Off freezes the picture at `offset`. */\r\n playing: true,\r\n /** Multiplies every magnitude before it is clamped. */\r\n gain: 1.4,\r\n /** How much of the box a silent bar still occupies, `0`–`1`. */\r\n floor: 0.04,\r\n align: Types.custom<WaveformAlignment>(\"center\"),\r\n style: Types.custom<WaveformStyle>(\"bars\"),\r\n /**\r\n * Show only this voice's activity, by index into the recording's own\r\n * numbering. `-1` — the default — is every voice.\r\n */\r\n speaker: -1,\r\n /** Colour each bar by whoever is speaking in it. */\r\n byVoice: false,\r\n /** A colour per voice index. Short or empty falls back to the palette. */\r\n voiceColors: Types.custom<string[]>([]),\r\n fill: Types.custom<Fill, FillResolved[]>(\"#6366f1\", { resolve: fillOps.resolve, lerp: fillOps.lerp }),\r\n },\r\n // Nothing declares a component's paints but itself.\r\n load: (loader, state) => loader.fill(state.fill),\r\n render: (ctx, state) => {\r\n drawBars(ctx, state)\r\n ctx.children()\r\n },\r\n})\r\n\r\nexport type LiveWaveform = NodeOf<typeof LiveWaveform>\r\nexport type LiveWaveformProps = NonNullable<Parameters<typeof LiveWaveform>[0]>\r\n\r\ninterface LiveState {\r\n readonly envelope: AudioEnvelope\r\n readonly bars: number\r\n readonly gap: number\r\n readonly radius: number\r\n readonly window: number\r\n readonly offset: number\r\n readonly playing: boolean\r\n readonly gain: number\r\n readonly floor: number\r\n readonly align: WaveformAlignment\r\n readonly style: WaveformStyle\r\n readonly speaker: number\r\n readonly byVoice: boolean\r\n readonly voiceColors: string[]\r\n readonly fill: FillResolved[]\r\n}\r\n\r\n/**\r\n * The second of the recording the visualiser is showing.\r\n *\r\n * Off the node's own clock rather than the scene's, which is what \"from the\r\n * moment this node appeared\" means — the same clock the engine's own\r\n * `Video` times its picture from, and the same one that makes a node dropped\r\n * halfway through a clip start its recording at its entrance rather than\r\n * partway in.\r\n */\r\nfunction at(ctx: RenderContext, s: LiveState): number {\r\n return s.offset + (s.playing ? Math.max(0, ctx.time.elapsed) : 0)\r\n}\r\n\r\nfunction drawBars(ctx: RenderContext, s: LiveState): void {\r\n const { width, height } = ctx.size\r\n if (width <= 0 || height <= 0) return\r\n\r\n const count = Math.max(1, Math.round(s.bars))\r\n const span = Math.max(0.05, s.window)\r\n const now = at(ctx, s)\r\n const voice = s.speaker < 0 ? null : Math.round(s.speaker)\r\n\r\n // The window is centred on the playhead, so the bars run *toward* the\r\n // middle and away again — the arrangement every mixer's meter uses, and the\r\n // one that reads as sound arriving rather than as a strip scrolling past.\r\n const sampled = forSpeaker(\r\n sampleEnvelope(s.envelope, now - span / 2, now + span / 2, count),\r\n voice\r\n )\r\n\r\n const shaped = s.style === \"line\" ? taper(sampled) : sampled\r\n const boxes = layOutBars(shaped, {\r\n width,\r\n height,\r\n gap: s.style === \"blocks\" ? 0.12 : s.gap,\r\n align: s.align,\r\n gain: s.gain,\r\n floor: s.floor,\r\n })\r\n if (boxes.length === 0) return\r\n\r\n const groups = groupByPaint(boxes, (i) =>\r\n livePaintKey(shaped[i].speaker, s.byVoice, voice)\r\n )\r\n\r\n for (const [key, run] of groups) {\r\n const graphics = paintBars(run, paintFor(s, key), s.radius)\r\n if (graphics) ctx.draw(graphics)\r\n }\r\n}\r\n\r\n/** See `FullWaveform`'s paints — a voice is a hue, never a gradient. */\r\nfunction paintFor(s: LiveState, key: string): Fill {\r\n if (key === \"all\") return s.fill\r\n const voice = Number(key.slice(\"voice:\".length))\r\n if (!Number.isFinite(voice) || voice < 0) return s.fill\r\n return s.voiceColors[voice] ?? voiceColor(voice)\r\n}\r\n\r\n/**\r\n * Rolls the ends of a window off toward silence.\r\n *\r\n * What separates the `line` style from `bars`: a hard-edged window makes the\r\n * loud bar entering at the right edge appear from nothing, which reads as a\r\n * glitch rather than as sound arriving. Fading the outer fifth turns the same\r\n * data into a shape that grows out of the baseline — the profile a spectrum\r\n * display has, and the reason it looks like one object rather than a row of\r\n * separate meters.\r\n */\r\nfunction taper(bars: readonly EnvelopeBar[]): EnvelopeBar[] {\r\n const count = bars.length\r\n if (count < 3) return [...bars]\r\n const edge = Math.max(1, Math.floor(count / 5))\r\n\r\n return bars.map((bar, i) => {\r\n const fromEnd = Math.min(i, count - 1 - i)\r\n const weight = clamp01(fromEnd / edge)\r\n return { magnitude: bar.magnitude * weight, speaker: bar.speaker }\r\n })\r\n}\r\n", "import { FullWaveform } from \"./waveform/full-waveform\";\r\nimport { LiveWaveform } from \"./waveform/live-waveform\";\r\n\r\n/**\r\n * Every node type this package publishes.\r\n *\r\n * A host registers these by handing the array to an engine —\r\n * `new Engine(platform, { nodes: NODES })` — which reads each component's key.\r\n * Defining a component only *declares* that key: nothing is registered by\r\n * importing a module, so a registry holds exactly what its owner asked for.\r\n *\r\n * Listing the component **values** is also what survives bundling: this package\r\n * declares `sideEffects: false`, and a document names a node type by string, so\r\n * an import kept only for its side effect is exactly what gets shaken out.\r\n */\r\n// Typed by name, so the declaration names each component rather than spelling out its type through core.\r\nexport const NODES: (typeof FullWaveform | typeof LiveWaveform)[] = [FullWaveform, LiveWaveform];\r\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["Types", "component2d", "easeInOut", "fillOps", "paint", "initial", "Types", "fillOps", "FullWaveform", "component2d", "EMPTY_ENVELOPE", "loader", "state", "timeline", "ease", "easeInOut", "from", "ctx", "drawBars", "s", "width", "height", "count", "sampled", "sampleEnvelope", "boxes", "layOutBars", "edge", "played", "groups", "groupByPaint", "key", "run", "graphics", "paintBars", "paintFor", "clamp01", "length", "voice", "voiceColor", "Types", "component2d", "fillOps", "LiveWaveform", "component2d", "Types", "EMPTY_ENVELOPE", "fillOps", "loader", "state", "ctx", "drawBars", "at", "s", "width", "height", "count", "span", "now", "voice", "sampled", "forSpeaker", "sampleEnvelope", "shaped", "taper", "boxes", "layOutBars", "groups", "groupByPaint", "i", "livePaintKey", "key", "run", "graphics", "paintBars", "paintFor", "voiceColor", "bars", "edge", "bar", "fromEnd", "weight", "clamp01", "NODES", "FullWaveform", "LiveWaveform"]
|
|
7
7
|
}
|
package/dist/nodes.d.ts
CHANGED
|
@@ -4,16 +4,13 @@ import { LiveWaveform } from "./waveform/live-waveform.js";
|
|
|
4
4
|
* Every node type this package publishes.
|
|
5
5
|
*
|
|
6
6
|
* A host registers these by handing the array to an engine —
|
|
7
|
-
* `new Engine(platform, { nodes: NODES })` — which reads each
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* for and two engines in one process can differ about it.
|
|
7
|
+
* `new Engine(platform, { nodes: NODES })` — which reads each component's key.
|
|
8
|
+
* Defining a component only *declares* that key: nothing is registered by
|
|
9
|
+
* importing a module, so a registry holds exactly what its owner asked for.
|
|
11
10
|
*
|
|
12
|
-
* Listing
|
|
13
|
-
* `sideEffects: false`,
|
|
14
|
-
*
|
|
15
|
-
* `import "./waveform/full-waveform.js"` for its side effect is exactly what gets shaken out, where an
|
|
16
|
-
* array of bindings is a data dependency that cannot be.
|
|
11
|
+
* Listing the component **values** is also what survives bundling: this package
|
|
12
|
+
* declares `sideEffects: false`, and a document names a node type by string, so
|
|
13
|
+
* an import kept only for its side effect is exactly what gets shaken out.
|
|
17
14
|
*/
|
|
18
15
|
export declare const NODES: (typeof FullWaveform | typeof LiveWaveform)[];
|
|
19
16
|
//# sourceMappingURL=nodes.d.ts.map
|
package/dist/nodes.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nodes.d.ts","sourceRoot":"","sources":["../src/nodes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAExD
|
|
1
|
+
{"version":3,"file":"nodes.d.ts","sourceRoot":"","sources":["../src/nodes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAExD;;;;;;;;;;;GAWG;AAEH,eAAO,MAAM,KAAK,EAAE,CAAC,OAAO,YAAY,GAAG,OAAO,YAAY,CAAC,EAAiC,CAAC"}
|
package/dist/nodes.js
CHANGED
|
@@ -4,16 +4,14 @@ import { LiveWaveform } from "./waveform/live-waveform.js";
|
|
|
4
4
|
* Every node type this package publishes.
|
|
5
5
|
*
|
|
6
6
|
* A host registers these by handing the array to an engine —
|
|
7
|
-
* `new Engine(platform, { nodes: NODES })` — which reads each
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* for and two engines in one process can differ about it.
|
|
7
|
+
* `new Engine(platform, { nodes: NODES })` — which reads each component's key.
|
|
8
|
+
* Defining a component only *declares* that key: nothing is registered by
|
|
9
|
+
* importing a module, so a registry holds exactly what its owner asked for.
|
|
11
10
|
*
|
|
12
|
-
* Listing
|
|
13
|
-
* `sideEffects: false`,
|
|
14
|
-
*
|
|
15
|
-
* `import "./waveform/full-waveform.js"` for its side effect is exactly what gets shaken out, where an
|
|
16
|
-
* array of bindings is a data dependency that cannot be.
|
|
11
|
+
* Listing the component **values** is also what survives bundling: this package
|
|
12
|
+
* declares `sideEffects: false`, and a document names a node type by string, so
|
|
13
|
+
* an import kept only for its side effect is exactly what gets shaken out.
|
|
17
14
|
*/
|
|
15
|
+
// Typed by name, so the declaration names each component rather than spelling out its type through core.
|
|
18
16
|
export const NODES = [FullWaveform, LiveWaveform];
|
|
19
17
|
//# sourceMappingURL=nodes.js.map
|
package/dist/nodes.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nodes.js","sourceRoot":"","sources":["../src/nodes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAExD
|
|
1
|
+
{"version":3,"file":"nodes.js","sourceRoot":"","sources":["../src/nodes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAExD;;;;;;;;;;;GAWG;AACH,yGAAyG;AACzG,MAAM,CAAC,MAAM,KAAK,GAAkD,CAAC,YAAY,EAAE,YAAY,CAAC,CAAC","sourcesContent":["import { FullWaveform } from \"./waveform/full-waveform\";\r\nimport { LiveWaveform } from \"./waveform/live-waveform\";\r\n\r\n/**\r\n * Every node type this package publishes.\r\n *\r\n * A host registers these by handing the array to an engine —\r\n * `new Engine(platform, { nodes: NODES })` — which reads each component's key.\r\n * Defining a component only *declares* that key: nothing is registered by\r\n * importing a module, so a registry holds exactly what its owner asked for.\r\n *\r\n * Listing the component **values** is also what survives bundling: this package\r\n * declares `sideEffects: false`, and a document names a node type by string, so\r\n * an import kept only for its side effect is exactly what gets shaken out.\r\n */\r\n// Typed by name, so the declaration names each component rather than spelling out its type through core.\r\nexport const NODES: (typeof FullWaveform | typeof LiveWaveform)[] = [FullWaveform, LiveWaveform];\r\n"]}
|
|
@@ -1,34 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { Seekable } from "@motionscript/sdk/component";
|
|
1
|
+
import { type Fill, type FillResolved, type NodeOf, type Node2D as _Node2D, type Node2DProps as _Node2DProps } from "@motionscript/sdk";
|
|
3
2
|
import { type AudioEnvelope } from "./envelope.js";
|
|
4
|
-
import { type WaveformAlignment } from "./shared.js";
|
|
5
|
-
export interface FullWaveformProps extends Node2DProps {
|
|
6
|
-
/** The baked recording. See {@link AudioEnvelope} for why it is baked. */
|
|
7
|
-
envelope: AudioEnvelope;
|
|
8
|
-
/** How many columns the whole file is drawn as. */
|
|
9
|
-
bars: number;
|
|
10
|
-
/** Space between bars, as a fraction of the pitch. See {@link layOutBars}. */
|
|
11
|
-
gap: number;
|
|
12
|
-
/** Corner radius of a bar, capped at half its width. */
|
|
13
|
-
radius: number;
|
|
14
|
-
/** How much of the file reads as played, `0`–`1`. Tweenable. */
|
|
15
|
-
progress: number;
|
|
16
|
-
/** Drive {@link progress} from the node's own clock instead of the prop. */
|
|
17
|
-
follow: boolean;
|
|
18
|
-
/** Multiplies every magnitude — a quiet track pulled up to fill its box. */
|
|
19
|
-
gain: number;
|
|
20
|
-
/** How much of the box a silent bar still occupies, `0`–`1`. */
|
|
21
|
-
floor: number;
|
|
22
|
-
align: WaveformAlignment;
|
|
23
|
-
/** Colour each voice's turns rather than splitting on played / not played. */
|
|
24
|
-
byVoice: boolean;
|
|
25
|
-
/** A colour per voice index. Short or empty falls back to the palette. */
|
|
26
|
-
voiceColors: string[];
|
|
27
|
-
/** The bars ahead of the playhead. */
|
|
28
|
-
fill: Fill;
|
|
29
|
-
/** …and the bars behind it. */
|
|
30
|
-
playedFill: Fill;
|
|
31
|
-
}
|
|
32
3
|
/**
|
|
33
4
|
* A **waveform overview**: the whole recording's shape, drawn at once.
|
|
34
5
|
*
|
|
@@ -40,7 +11,7 @@ export interface FullWaveformProps extends Node2DProps {
|
|
|
40
11
|
*
|
|
41
12
|
* ## Progress is authored, not assumed
|
|
42
13
|
*
|
|
43
|
-
*
|
|
14
|
+
* `follow` is off by default, and that default is the considered one. A
|
|
44
15
|
* scene has no idea where the recording it is drawing sits on the video's audio
|
|
45
16
|
* lane — a clip is placed, trimmed and sped up out there, and none of that is
|
|
46
17
|
* visible from in here. So the honest default is that the fill is *animation*:
|
|
@@ -52,52 +23,96 @@ export interface FullWaveformProps extends Node2DProps {
|
|
|
52
23
|
*
|
|
53
24
|
* ## Voices instead of progress
|
|
54
25
|
*
|
|
55
|
-
* With
|
|
26
|
+
* With `byVoice` on, the played/ahead split is replaced by one colour per
|
|
56
27
|
* speaking voice, at a size you can read. For a two-hander it is a map of the conversation: who has the
|
|
57
28
|
* floor, where the turns are, how long each one ran. It replaces the progress
|
|
58
29
|
* colours rather than combining with them, because a bar cannot mean two things
|
|
59
30
|
* at once and "played, and also Grace" is a legend nobody can hold in their
|
|
60
31
|
* head.
|
|
61
32
|
*/
|
|
62
|
-
export declare
|
|
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
|
-
|
|
33
|
+
export declare const FullWaveform: import("@motionscript/sdk").Component<"fullWaveform", import("@motionscript/sdk").Merge<import("@motionscript/sdk").Box2DState, {
|
|
34
|
+
/** The baked recording. See {@link AudioEnvelope} for why it is baked. */
|
|
35
|
+
readonly envelope: import("@motionscript/sdk").Type<AudioEnvelope, AudioEnvelope, false>;
|
|
36
|
+
/** How many columns the whole file is drawn as. */
|
|
37
|
+
readonly bars: 120;
|
|
38
|
+
/** Space between bars, as a fraction of the pitch. See {@link layOutBars}. */
|
|
39
|
+
readonly gap: 0.3;
|
|
40
|
+
/** Corner radius of a bar, capped at half its width. */
|
|
41
|
+
readonly radius: 2;
|
|
42
|
+
/** How much of the file reads as played, `0`–`1`. Tweenable. */
|
|
43
|
+
readonly progress: 0;
|
|
44
|
+
/** Drive `progress` from the node's own clock instead of the prop. */
|
|
45
|
+
readonly follow: false;
|
|
46
|
+
/** Multiplies every magnitude — a quiet track pulled up to fill its box. */
|
|
47
|
+
readonly gain: 1;
|
|
48
|
+
/** How much of the box a silent bar still occupies, `0`–`1`. */
|
|
49
|
+
readonly floor: 0.015;
|
|
50
|
+
readonly align: import("@motionscript/sdk").Type<"center" | "bottom", "center" | "bottom", false>;
|
|
51
|
+
/** Colour each voice's turns rather than splitting on played / not played. */
|
|
52
|
+
readonly byVoice: false;
|
|
53
|
+
/** A colour per voice index. Short or empty falls back to the palette. */
|
|
54
|
+
readonly voiceColors: import("@motionscript/sdk").Type<string[], string[], false>;
|
|
55
|
+
/** The bars ahead of the playhead. */
|
|
56
|
+
readonly fill: import("@motionscript/sdk").Type<Fill, FillResolved[], false>;
|
|
57
|
+
/** …and the bars behind it. */
|
|
58
|
+
readonly playedFill: import("@motionscript/sdk").Type<Fill, FillResolved[], false>;
|
|
59
|
+
}>, {
|
|
60
|
+
readonly sweep: unknown;
|
|
61
|
+
}, null, _Node2D<_Node2DProps>, import("@motionscript/sdk").CallProps<import("@motionscript/sdk").Merge<import("@motionscript/sdk").Box2DState, {
|
|
62
|
+
/** The baked recording. See {@link AudioEnvelope} for why it is baked. */
|
|
63
|
+
readonly envelope: import("@motionscript/sdk").Type<AudioEnvelope, AudioEnvelope, false>;
|
|
64
|
+
/** How many columns the whole file is drawn as. */
|
|
65
|
+
readonly bars: 120;
|
|
66
|
+
/** Space between bars, as a fraction of the pitch. See {@link layOutBars}. */
|
|
67
|
+
readonly gap: 0.3;
|
|
68
|
+
/** Corner radius of a bar, capped at half its width. */
|
|
69
|
+
readonly radius: 2;
|
|
70
|
+
/** How much of the file reads as played, `0`–`1`. Tweenable. */
|
|
71
|
+
readonly progress: 0;
|
|
72
|
+
/** Drive `progress` from the node's own clock instead of the prop. */
|
|
73
|
+
readonly follow: false;
|
|
74
|
+
/** Multiplies every magnitude — a quiet track pulled up to fill its box. */
|
|
75
|
+
readonly gain: 1;
|
|
76
|
+
/** How much of the box a silent bar still occupies, `0`–`1`. */
|
|
77
|
+
readonly floor: 0.015;
|
|
78
|
+
readonly align: import("@motionscript/sdk").Type<"center" | "bottom", "center" | "bottom", false>;
|
|
79
|
+
/** Colour each voice's turns rather than splitting on played / not played. */
|
|
80
|
+
readonly byVoice: false;
|
|
81
|
+
/** A colour per voice index. Short or empty falls back to the palette. */
|
|
82
|
+
readonly voiceColors: import("@motionscript/sdk").Type<string[], string[], false>;
|
|
83
|
+
/** The bars ahead of the playhead. */
|
|
84
|
+
readonly fill: import("@motionscript/sdk").Type<Fill, FillResolved[], false>;
|
|
85
|
+
/** …and the bars behind it. */
|
|
86
|
+
readonly playedFill: import("@motionscript/sdk").Type<Fill, FillResolved[], false>;
|
|
87
|
+
}>, null, import("@motionscript/sdk").BuiltNode<import("@motionscript/sdk").Merge<import("@motionscript/sdk").Box2DState, {
|
|
88
|
+
/** The baked recording. See {@link AudioEnvelope} for why it is baked. */
|
|
89
|
+
readonly envelope: import("@motionscript/sdk").Type<AudioEnvelope, AudioEnvelope, false>;
|
|
90
|
+
/** How many columns the whole file is drawn as. */
|
|
91
|
+
readonly bars: 120;
|
|
92
|
+
/** Space between bars, as a fraction of the pitch. See {@link layOutBars}. */
|
|
93
|
+
readonly gap: 0.3;
|
|
94
|
+
/** Corner radius of a bar, capped at half its width. */
|
|
95
|
+
readonly radius: 2;
|
|
96
|
+
/** How much of the file reads as played, `0`–`1`. Tweenable. */
|
|
97
|
+
readonly progress: 0;
|
|
98
|
+
/** Drive `progress` from the node's own clock instead of the prop. */
|
|
99
|
+
readonly follow: false;
|
|
100
|
+
/** Multiplies every magnitude — a quiet track pulled up to fill its box. */
|
|
101
|
+
readonly gain: 1;
|
|
102
|
+
/** How much of the box a silent bar still occupies, `0`–`1`. */
|
|
103
|
+
readonly floor: 0.015;
|
|
104
|
+
readonly align: import("@motionscript/sdk").Type<"center" | "bottom", "center" | "bottom", false>;
|
|
105
|
+
/** Colour each voice's turns rather than splitting on played / not played. */
|
|
106
|
+
readonly byVoice: false;
|
|
107
|
+
/** A colour per voice index. Short or empty falls back to the palette. */
|
|
108
|
+
readonly voiceColors: import("@motionscript/sdk").Type<string[], string[], false>;
|
|
109
|
+
/** The bars ahead of the playhead. */
|
|
110
|
+
readonly fill: import("@motionscript/sdk").Type<Fill, FillResolved[], false>;
|
|
111
|
+
/** …and the bars behind it. */
|
|
112
|
+
readonly playedFill: import("@motionscript/sdk").Type<Fill, FillResolved[], false>;
|
|
113
|
+
}>, {
|
|
114
|
+
readonly sweep: unknown;
|
|
115
|
+
}, _Node2D<_Node2DProps>>>>;
|
|
116
|
+
export type FullWaveform = NodeOf<typeof FullWaveform>;
|
|
117
|
+
export type FullWaveformProps = NonNullable<Parameters<typeof FullWaveform>[0]>;
|
|
103
118
|
//# sourceMappingURL=full-waveform.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"full-waveform.d.ts","sourceRoot":"","sources":["../../src/waveform/full-waveform.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"full-waveform.d.ts","sourceRoot":"","sources":["../../src/waveform/full-waveform.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,IAAI,EACT,KAAK,YAAY,EACjB,KAAK,MAAM,EAGX,KAAK,MAAM,IAAI,OAAO,EACtB,KAAK,WAAW,IAAI,YAAY,EACjC,MAAM,mBAAmB,CAAA;AAE1B,OAAO,EAAkC,KAAK,aAAa,EAAE,MAAM,YAAY,CAAA;AAc/E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,eAAO,MAAM,YAAY;IAGrB,0EAA0E;;IAE1E,mDAAmD;;IAEnD,8EAA8E;;IAE9E,wDAAwD;;IAExD,gEAAgE;;IAEhE,sEAAsE;;IAEtE,4EAA4E;;IAE5E,gEAAgE;;;IAGhE,8EAA8E;;IAE9E,0EAA0E;;IAE1E,sCAAsC;;IAEtC,+BAA+B;;;;;IAvB/B,0EAA0E;;IAE1E,mDAAmD;;IAEnD,8EAA8E;;IAE9E,wDAAwD;;IAExD,gEAAgE;;IAEhE,sEAAsE;;IAEtE,4EAA4E;;IAE5E,gEAAgE;;;IAGhE,8EAA8E;;IAE9E,0EAA0E;;IAE1E,sCAAsC;;IAEtC,+BAA+B;;;IAvB/B,0EAA0E;;IAE1E,mDAAmD;;IAEnD,8EAA8E;;IAE9E,wDAAwD;;IAExD,gEAAgE;;IAEhE,sEAAsE;;IAEtE,4EAA4E;;IAE5E,gEAAgE;;;IAGhE,8EAA8E;;IAE9E,0EAA0E;;IAE1E,sCAAsC;;IAEtC,+BAA+B;;;;2BAwBjC,CAAA;AAEF,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,OAAO,YAAY,CAAC,CAAA;AACtD,MAAM,MAAM,iBAAiB,GAAG,WAAW,CAAC,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA"}
|