@collabhut/plugin-sdk 0.1.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/LICENSE +21 -0
- package/README.md +1046 -0
- package/dist/helpers/note-utils.d.ts +128 -0
- package/dist/helpers/note-utils.d.ts.map +1 -0
- package/dist/helpers/note-utils.js +155 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/types/audio-effect.d.ts +70 -0
- package/dist/types/audio-effect.d.ts.map +1 -0
- package/dist/types/audio-effect.js +1 -0
- package/dist/types/context.d.ts +39 -0
- package/dist/types/context.d.ts.map +1 -0
- package/dist/types/context.js +1 -0
- package/dist/types/events.d.ts +119 -0
- package/dist/types/events.d.ts.map +1 -0
- package/dist/types/events.js +19 -0
- package/dist/types/instrument.d.ts +83 -0
- package/dist/types/instrument.d.ts.map +1 -0
- package/dist/types/instrument.js +1 -0
- package/dist/types/licensing.d.ts +118 -0
- package/dist/types/licensing.d.ts.map +1 -0
- package/dist/types/licensing.js +27 -0
- package/dist/types/manifest.d.ts +90 -0
- package/dist/types/manifest.d.ts.map +1 -0
- package/dist/types/manifest.js +1 -0
- package/dist/types/midi-effect.d.ts +101 -0
- package/dist/types/midi-effect.d.ts.map +1 -0
- package/dist/types/midi-effect.js +1 -0
- package/dist/types/parameters.d.ts +76 -0
- package/dist/types/parameters.d.ts.map +1 -0
- package/dist/types/parameters.js +1 -0
- package/dist/types/shader.d.ts +110 -0
- package/dist/types/shader.d.ts.map +1 -0
- package/dist/types/shader.js +1 -0
- package/dist/types/vocal-preset.d.ts +149 -0
- package/dist/types/vocal-preset.d.ts.map +1 -0
- package/dist/types/vocal-preset.js +54 -0
- package/package.json +50 -0
package/README.md
ADDED
|
@@ -0,0 +1,1046 @@
|
|
|
1
|
+
# @collabhut/plugin-sdk
|
|
2
|
+
|
|
3
|
+
> Official TypeScript SDK for building CollabDAW plugins.
|
|
4
|
+
|
|
5
|
+
The `@collabhut/plugin-sdk` package gives you everything you need to create,
|
|
6
|
+
type-check, and publish plugins for [CollabDAW](https://collabhut.com) — the
|
|
7
|
+
collaborative music production environment.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Table of Contents
|
|
12
|
+
|
|
13
|
+
1. [Overview](#overview)
|
|
14
|
+
2. [Plugin types](#plugin-types)
|
|
15
|
+
3. [Installation](#installation)
|
|
16
|
+
4. [Project setup](#project-setup)
|
|
17
|
+
5. [Getting started — Hello World audio effect](#getting-started--hello-world-audio-effect)
|
|
18
|
+
6. [Audio effect plugins](#audio-effect-plugins)
|
|
19
|
+
7. [MIDI effect plugins](#midi-effect-plugins)
|
|
20
|
+
8. [Instrument plugins](#instrument-plugins)
|
|
21
|
+
9. [Vocal preset plugins](#vocal-preset-plugins)
|
|
22
|
+
10. [Shader plugins & ShaderToy compatibility](#shader-plugins--shadertoy-compatibility)
|
|
23
|
+
11. [Parameter system](#parameter-system)
|
|
24
|
+
12. [Note utilities](#note-utilities)
|
|
25
|
+
13. [Security & sandbox model](#security--sandbox-model)
|
|
26
|
+
14. [Licensing & verification](#licensing--verification)
|
|
27
|
+
15. [Collaboration access rules](#collaboration-access-rules)
|
|
28
|
+
16. [Publishing to the CollabHut Marketplace](#publishing-to-the-collabhut-marketplace)
|
|
29
|
+
17. [Plugin IDE & .dawplugin files](#plugin-ide--dawplugin-files)
|
|
30
|
+
18. [API reference](#api-reference)
|
|
31
|
+
19. [Frequently asked questions](#frequently-asked-questions)
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Overview
|
|
36
|
+
|
|
37
|
+
Plugins for CollabDAW are **TypeScript ES modules** that export a `manifest`
|
|
38
|
+
object and a factory function (or, for vocal presets and shaders, a data
|
|
39
|
+
object). They are compiled to a signed bundle by the CollabHut build service
|
|
40
|
+
and hosted on the CollabHut CDN.
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
your-plugin/
|
|
44
|
+
src/
|
|
45
|
+
index.ts ← exports manifest + factory (or preset / shader)
|
|
46
|
+
package.json
|
|
47
|
+
tsconfig.json
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Key design decisions:
|
|
51
|
+
|
|
52
|
+
| Property | Detail |
|
|
53
|
+
|---|---|
|
|
54
|
+
| **Language** | TypeScript 5+ (strict mode required) |
|
|
55
|
+
| **Runtime** | Sandboxed Web Worker inside CollabDAW |
|
|
56
|
+
| **Audio API** | Web Audio API (`AudioContext`) — injected via `PluginContext` |
|
|
57
|
+
| **Network** | Only `cdn.collabhut.com` — via `context.fetchAsset()` |
|
|
58
|
+
| **No DOM access** | `document`, `window`, `localStorage` are unmounted |
|
|
59
|
+
| **Licensing** | Server-side only — no user key-entry friction |
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Plugin types
|
|
64
|
+
|
|
65
|
+
| Type | `manifest.type` | What it does |
|
|
66
|
+
|---|---|---|
|
|
67
|
+
| Audio Effect | `"audio-effect"` | Processes a live audio stream (EQ, reverb, compressor…) |
|
|
68
|
+
| MIDI Effect | `"midi-effect"` | Transforms or generates MIDI events (arpeggiator, chord generator…) |
|
|
69
|
+
| Instrument | `"instrument"` | Synthesises audio from MIDI notes (synth, sampler…) |
|
|
70
|
+
| Vocal Preset | `"vocal-preset"` | Configures CollabDAW's built-in vocal chain (static data, no DSP code) |
|
|
71
|
+
| Shader | `"shader"` | GLSL fragment shader displayed in the ShaderToy panel |
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Installation
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npm install --save-dev @collabhut/plugin-sdk
|
|
79
|
+
# or
|
|
80
|
+
pnpm add -D @collabhut/plugin-sdk
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The package is **type-only at runtime** — all exports are TypeScript
|
|
84
|
+
interfaces. Helper functions (`noteToHz`, `gainToDb`, etc.) are the only
|
|
85
|
+
compiled JavaScript.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Project setup
|
|
90
|
+
|
|
91
|
+
### `tsconfig.json`
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"compilerOptions": {
|
|
96
|
+
"target": "ESNext",
|
|
97
|
+
"module": "ESNext",
|
|
98
|
+
"moduleResolution": "bundler",
|
|
99
|
+
"strict": true,
|
|
100
|
+
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
|
101
|
+
"outDir": "dist",
|
|
102
|
+
"rootDir": "src",
|
|
103
|
+
"declaration": true,
|
|
104
|
+
"declarationDir": "dist",
|
|
105
|
+
"sourceMap": true
|
|
106
|
+
},
|
|
107
|
+
"include": ["src"]
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
> `"DOM"` is required so TypeScript knows about `AudioContext`, `AudioNode`,
|
|
112
|
+
> `Worker`, and `SharedArrayBuffer`.
|
|
113
|
+
|
|
114
|
+
### `package.json`
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
{
|
|
118
|
+
"name": "@your-org/my-plugin",
|
|
119
|
+
"version": "1.0.0",
|
|
120
|
+
"type": "module",
|
|
121
|
+
"main": "./dist/index.js",
|
|
122
|
+
"types": "./dist/index.d.ts",
|
|
123
|
+
"exports": {
|
|
124
|
+
".": {
|
|
125
|
+
"import": "./dist/index.js",
|
|
126
|
+
"types": "./dist/index.d.ts"
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
"scripts": {
|
|
130
|
+
"build": "tsc"
|
|
131
|
+
},
|
|
132
|
+
"devDependencies": {
|
|
133
|
+
"@collabhut/plugin-sdk": "^0.1.0",
|
|
134
|
+
"typescript": "^5.0.0"
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Getting started — Hello World audio effect
|
|
142
|
+
|
|
143
|
+
This is the minimal complete plugin. It creates a `GainNode` that lets the
|
|
144
|
+
user control the volume from within CollabDAW.
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
// src/index.ts
|
|
148
|
+
import type {
|
|
149
|
+
AudioEffectModule,
|
|
150
|
+
AudioEffectFactory,
|
|
151
|
+
PluginManifest,
|
|
152
|
+
} from "@collabhut/plugin-sdk"
|
|
153
|
+
|
|
154
|
+
export const manifest = {
|
|
155
|
+
id: "com.myorg.hello-gain",
|
|
156
|
+
name: "Hello Gain",
|
|
157
|
+
version: "1.0.0",
|
|
158
|
+
type: "audio-effect",
|
|
159
|
+
description: "A simple gain control — the Hello World of CollabDAW plugins.",
|
|
160
|
+
author: {
|
|
161
|
+
name: "My Org",
|
|
162
|
+
url: "https://myorg.com",
|
|
163
|
+
},
|
|
164
|
+
params: [
|
|
165
|
+
{
|
|
166
|
+
type: "range",
|
|
167
|
+
id: "gain",
|
|
168
|
+
label: "Gain",
|
|
169
|
+
min: 0,
|
|
170
|
+
max: 2,
|
|
171
|
+
default: 1,
|
|
172
|
+
decimals: 2,
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
pricing: "free",
|
|
176
|
+
minApiVersion: "0.1.0",
|
|
177
|
+
} satisfies PluginManifest
|
|
178
|
+
|
|
179
|
+
const factory: AudioEffectFactory<typeof manifest.params> = (context, params) => {
|
|
180
|
+
const gain = context.audioContext.createGain()
|
|
181
|
+
gain.gain.value = params.gain as number
|
|
182
|
+
return {
|
|
183
|
+
input: gain,
|
|
184
|
+
output: gain,
|
|
185
|
+
automationTargets: { gain: gain.gain },
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const mod: AudioEffectModule = { manifest, factory }
|
|
190
|
+
export default mod
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Build it:
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
pnpm build
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Audio effect plugins
|
|
202
|
+
|
|
203
|
+
An audio effect plugin creates a **Web Audio subgraph** and exposes input/output
|
|
204
|
+
nodes. The DAW connects your plugin into the track's processing chain.
|
|
205
|
+
|
|
206
|
+
```
|
|
207
|
+
Track source → [your input node] → [your DSP graph] → [your output node] → track bus
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Stereo compressor example
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
// src/index.ts
|
|
214
|
+
import type { AudioEffectModule, AudioEffectFactory, PluginManifest } from "@collabhut/plugin-sdk"
|
|
215
|
+
|
|
216
|
+
export const manifest = {
|
|
217
|
+
id: "com.myorg.stereo-compressor",
|
|
218
|
+
name: "Stereo Compressor",
|
|
219
|
+
version: "1.0.0",
|
|
220
|
+
type: "audio-effect",
|
|
221
|
+
description: "Classic stereo dynamics compressor with optional side-chain.",
|
|
222
|
+
author: { name: "My Org" },
|
|
223
|
+
params: [
|
|
224
|
+
{ type: "range", id: "threshold", label: "Threshold", min: -60, max: 0, default: -18, unit: "dB" },
|
|
225
|
+
{ type: "range", id: "ratio", label: "Ratio", min: 1, max: 20, default: 4, decimals: 1 },
|
|
226
|
+
{ type: "range", id: "attack", label: "Attack", min: 0, max: 1, default: 0.003, unit: "s", decimals: 3, curve: "log" },
|
|
227
|
+
{ type: "range", id: "release", label: "Release", min: 0, max: 2, default: 0.15, unit: "s", decimals: 2, curve: "log" },
|
|
228
|
+
{ type: "range", id: "makeUpGain",label: "Make-up", min: 0, max: 24, default: 6, unit: "dB" },
|
|
229
|
+
{ type: "bool", id: "softClip", label: "Soft Clip", default: false },
|
|
230
|
+
],
|
|
231
|
+
pricing: "paid",
|
|
232
|
+
minApiVersion: "0.1.0",
|
|
233
|
+
} satisfies PluginManifest
|
|
234
|
+
|
|
235
|
+
const factory: AudioEffectFactory<typeof manifest.params> = (context, params) => {
|
|
236
|
+
const comp = context.audioContext.createDynamicsCompressor()
|
|
237
|
+
comp.threshold.value = params.threshold as number
|
|
238
|
+
comp.ratio.value = params.ratio as number
|
|
239
|
+
comp.attack.value = params.attack as number
|
|
240
|
+
comp.release.value = params.release as number
|
|
241
|
+
|
|
242
|
+
const makeUp = context.audioContext.createGain()
|
|
243
|
+
makeUp.gain.value = 10 ** ((params.makeUpGain as number) / 20)
|
|
244
|
+
|
|
245
|
+
comp.connect(makeUp)
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
input: comp,
|
|
249
|
+
output: makeUp,
|
|
250
|
+
automationTargets: {
|
|
251
|
+
threshold: comp.threshold,
|
|
252
|
+
ratio: comp.ratio,
|
|
253
|
+
attack: comp.attack,
|
|
254
|
+
release: comp.release,
|
|
255
|
+
},
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const mod: AudioEffectModule = { manifest, factory }
|
|
260
|
+
export default mod
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Fetching an impulse response for a convolution reverb
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
const factory: AudioEffectFactory = async (context, _params) => {
|
|
267
|
+
// Only cdn.collabhut.com URLs are permitted
|
|
268
|
+
const irBuffer = await context.fetchAsset(
|
|
269
|
+
"https://cdn.collabhut.com/ir/hall-large.wav"
|
|
270
|
+
)
|
|
271
|
+
const decoded = await context.audioContext.decodeAudioData(irBuffer)
|
|
272
|
+
|
|
273
|
+
const convolver = context.audioContext.createConvolver()
|
|
274
|
+
convolver.buffer = decoded
|
|
275
|
+
|
|
276
|
+
return { input: convolver, output: convolver }
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
> `context.fetchAsset()` throws `TypeError` for any URL not on
|
|
281
|
+
> `cdn.collabhut.com`. The restriction is enforced in the sandbox.
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## MIDI effect plugins
|
|
286
|
+
|
|
287
|
+
MIDI effects transform or generate MIDI events on a per-block basis.
|
|
288
|
+
Your plugin receives the incoming events and returns the processed list.
|
|
289
|
+
|
|
290
|
+
### Arpeggiator example
|
|
291
|
+
|
|
292
|
+
```ts
|
|
293
|
+
// src/index.ts
|
|
294
|
+
import type {
|
|
295
|
+
MidiEffectModule,
|
|
296
|
+
MidiEffectFactory,
|
|
297
|
+
MidiEvent,
|
|
298
|
+
MidiNoteOnEvent,
|
|
299
|
+
PluginManifest,
|
|
300
|
+
} from "@collabhut/plugin-sdk"
|
|
301
|
+
import { transpose } from "@collabhut/plugin-sdk"
|
|
302
|
+
|
|
303
|
+
export const manifest = {
|
|
304
|
+
id: "com.myorg.simple-arp",
|
|
305
|
+
name: "Simple Arpeggiator",
|
|
306
|
+
version: "1.0.0",
|
|
307
|
+
type: "midi-effect",
|
|
308
|
+
description: "Fans held notes into an up-down arpeggio pattern.",
|
|
309
|
+
author: { name: "My Org" },
|
|
310
|
+
params: [
|
|
311
|
+
{
|
|
312
|
+
type: "choice",
|
|
313
|
+
id: "pattern",
|
|
314
|
+
label: "Pattern",
|
|
315
|
+
choices: ["up", "down", "up-down"],
|
|
316
|
+
default: "up",
|
|
317
|
+
},
|
|
318
|
+
],
|
|
319
|
+
pricing: "free",
|
|
320
|
+
minApiVersion: "0.1.0",
|
|
321
|
+
} satisfies PluginManifest
|
|
322
|
+
|
|
323
|
+
const factory: MidiEffectFactory = (_context, params) => {
|
|
324
|
+
const heldNotes: MidiNoteOnEvent[] = []
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
process(events) {
|
|
328
|
+
const out: MidiEvent[] = []
|
|
329
|
+
|
|
330
|
+
for (const ev of events) {
|
|
331
|
+
if (ev.type === "note-on") {
|
|
332
|
+
heldNotes.push(ev)
|
|
333
|
+
} else if (ev.type === "note-off") {
|
|
334
|
+
const idx = heldNotes.findIndex(n => n.note === ev.note)
|
|
335
|
+
if (idx !== -1) heldNotes.splice(idx, 1)
|
|
336
|
+
}
|
|
337
|
+
// Emit original event
|
|
338
|
+
out.push(ev)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Emit transposed copies for each held note based on pattern
|
|
342
|
+
const pattern = params.pattern as string
|
|
343
|
+
for (const held of heldNotes) {
|
|
344
|
+
if (pattern === "up" || pattern === "up-down") {
|
|
345
|
+
out.push({ ...held, note: transpose(held.note, 12) })
|
|
346
|
+
}
|
|
347
|
+
if (pattern === "up-down") {
|
|
348
|
+
out.push({ ...held, note: transpose(held.note, -12) })
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return out
|
|
353
|
+
},
|
|
354
|
+
dispose() {
|
|
355
|
+
heldNotes.length = 0
|
|
356
|
+
},
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const mod: MidiEffectModule = { manifest, factory }
|
|
361
|
+
export default mod
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
---
|
|
365
|
+
|
|
366
|
+
## Instrument plugins
|
|
367
|
+
|
|
368
|
+
Instrument plugins are **polyphonic synthesisers**. The DAW calls `noteOn()`
|
|
369
|
+
for each MIDI note and stores the returned voice. When the note ends it calls
|
|
370
|
+
`voice.stop()`.
|
|
371
|
+
|
|
372
|
+
### Simple sine synth
|
|
373
|
+
|
|
374
|
+
```ts
|
|
375
|
+
// src/index.ts
|
|
376
|
+
import type { InstrumentModule, InstrumentFactory, InstrumentVoice, PluginManifest } from "@collabhut/plugin-sdk"
|
|
377
|
+
import { noteToHz, dbToGain } from "@collabhut/plugin-sdk"
|
|
378
|
+
|
|
379
|
+
export const manifest = {
|
|
380
|
+
id: "com.myorg.sine-synth",
|
|
381
|
+
name: "Sine Synth",
|
|
382
|
+
version: "1.0.0",
|
|
383
|
+
type: "instrument",
|
|
384
|
+
description: "A pure sine-wave synthesiser with ADSR envelope.",
|
|
385
|
+
author: { name: "My Org" },
|
|
386
|
+
params: [
|
|
387
|
+
{ type: "range", id: "attack", label: "Attack", min: 0.001, max: 2, default: 0.01, unit: "s", curve: "log", decimals: 3 },
|
|
388
|
+
{ type: "range", id: "decay", label: "Decay", min: 0.001, max: 2, default: 0.1, unit: "s", curve: "log", decimals: 3 },
|
|
389
|
+
{ type: "range", id: "sustain", label: "Sustain", min: 0, max: 1, default: 0.7, decimals: 2 },
|
|
390
|
+
{ type: "range", id: "release", label: "Release", min: 0.001, max: 4, default: 0.3, unit: "s", curve: "log", decimals: 3 },
|
|
391
|
+
{ type: "range", id: "volume", label: "Volume", min: -60, max: 0, default: -12, unit: "dB" },
|
|
392
|
+
],
|
|
393
|
+
pricing: "free",
|
|
394
|
+
minApiVersion: "0.1.0",
|
|
395
|
+
} satisfies PluginManifest
|
|
396
|
+
|
|
397
|
+
const factory: InstrumentFactory = (context, params) => ({
|
|
398
|
+
noteOn(note, velocity, time): InstrumentVoice {
|
|
399
|
+
const { audioContext } = context
|
|
400
|
+
|
|
401
|
+
const osc = audioContext.createOscillator()
|
|
402
|
+
const gain = audioContext.createGain()
|
|
403
|
+
|
|
404
|
+
osc.type = "sine"
|
|
405
|
+
osc.frequency.value = noteToHz(note)
|
|
406
|
+
gain.gain.value = 0
|
|
407
|
+
|
|
408
|
+
osc.connect(gain)
|
|
409
|
+
osc.start(time)
|
|
410
|
+
|
|
411
|
+
const velGain = velocity / 127
|
|
412
|
+
const maxGain = dbToGain(params.volume as number) * velGain
|
|
413
|
+
const attack = params.attack as number
|
|
414
|
+
const decay = params.decay as number
|
|
415
|
+
const sustain = params.sustain as number
|
|
416
|
+
const release = params.release as number
|
|
417
|
+
|
|
418
|
+
// ADSR
|
|
419
|
+
gain.gain.linearRampToValueAtTime(maxGain, time + attack)
|
|
420
|
+
gain.gain.linearRampToValueAtTime(maxGain * sustain, time + attack + decay)
|
|
421
|
+
|
|
422
|
+
return {
|
|
423
|
+
output: gain,
|
|
424
|
+
stop(_vel, releaseTime) {
|
|
425
|
+
gain.gain.cancelScheduledValues(releaseTime)
|
|
426
|
+
gain.gain.setValueAtTime(gain.gain.value, releaseTime)
|
|
427
|
+
gain.gain.linearRampToValueAtTime(0, releaseTime + release)
|
|
428
|
+
osc.stop(releaseTime + release + 0.01)
|
|
429
|
+
},
|
|
430
|
+
kill() {
|
|
431
|
+
osc.stop()
|
|
432
|
+
gain.disconnect()
|
|
433
|
+
},
|
|
434
|
+
}
|
|
435
|
+
},
|
|
436
|
+
onMidi(_ev) {},
|
|
437
|
+
dispose() {},
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
const mod: InstrumentModule = { manifest, factory }
|
|
441
|
+
export default mod
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
---
|
|
445
|
+
|
|
446
|
+
## Vocal preset plugins
|
|
447
|
+
|
|
448
|
+
A vocal preset is **only data** — no JavaScript runs at playback time.
|
|
449
|
+
CollabDAW reads the `VocalPreset` object and applies it to the built-in vocal
|
|
450
|
+
engine. This means vocal presets have:
|
|
451
|
+
|
|
452
|
+
- **Zero sandbox risk** (no code execution)
|
|
453
|
+
- **No latency penalty**
|
|
454
|
+
- **Instant parameter recall**
|
|
455
|
+
|
|
456
|
+
```ts
|
|
457
|
+
// src/index.ts
|
|
458
|
+
import type { VocalPresetModule } from "@collabhut/plugin-sdk"
|
|
459
|
+
|
|
460
|
+
const mod: VocalPresetModule = {
|
|
461
|
+
manifest: {
|
|
462
|
+
id: "com.myorg.vintage-tape-vocals",
|
|
463
|
+
name: "Vintage Tape Vocals",
|
|
464
|
+
version: "1.0.0",
|
|
465
|
+
type: "vocal-preset",
|
|
466
|
+
description: "Warm, slightly compressed vocals with subtle tape saturation.",
|
|
467
|
+
author: { name: "My Org" },
|
|
468
|
+
pricing: "paid",
|
|
469
|
+
minApiVersion: "0.1.0",
|
|
470
|
+
},
|
|
471
|
+
preset: {
|
|
472
|
+
inputGain: 0,
|
|
473
|
+
outputGain: -2,
|
|
474
|
+
eq: {
|
|
475
|
+
enabled: true,
|
|
476
|
+
bands: [
|
|
477
|
+
{ frequency: 100, gain: -4, q: 0.7, type: "highpass" },
|
|
478
|
+
{ frequency: 250, gain: -2, q: 1.2, type: "peaking" },
|
|
479
|
+
{ frequency: 1500, gain: 2, q: 1.5, type: "peaking" },
|
|
480
|
+
{ frequency: 6000, gain: 3, q: 0.9, type: "peaking" },
|
|
481
|
+
{ frequency: 12000, gain: 1.5, q: 0.7, type: "highshelf"},
|
|
482
|
+
],
|
|
483
|
+
},
|
|
484
|
+
compressor: {
|
|
485
|
+
enabled: true,
|
|
486
|
+
threshold: -20,
|
|
487
|
+
knee: 8,
|
|
488
|
+
ratio: 4,
|
|
489
|
+
attack: 0.005,
|
|
490
|
+
release: 0.12,
|
|
491
|
+
makeUpGain: 5,
|
|
492
|
+
},
|
|
493
|
+
deEsser: { enabled: true, frequency: 7200, threshold: -28, range: 10 },
|
|
494
|
+
saturation: { enabled: true, drive: 0.15, mix: 0.35 },
|
|
495
|
+
pitchCorrection: { enabled: false, scale: "chromatic", strength: 0, speed: 0.1 },
|
|
496
|
+
chorus: { enabled: false, rate: 0.5, depth: 0.3, delay: 0.02, mix: 0.3 },
|
|
497
|
+
reverb: { enabled: true, roomSize: 0.25, damping: 0.75, preDelay: 0.015, mix: 0.18 },
|
|
498
|
+
delay: { enabled: false, time: 0.25, feedback: 0.3, filter: 4000, mix: 0.2 },
|
|
499
|
+
},
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
export default mod
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
---
|
|
506
|
+
|
|
507
|
+
## Shader plugins & ShaderToy compatibility
|
|
508
|
+
|
|
509
|
+
Shader plugins render a GLSL fragment shader in the **ShaderToy panel** of
|
|
510
|
+
CollabDAW. They can be static visuals, or they can react to audio in real time.
|
|
511
|
+
|
|
512
|
+
### Audio-reactive circle shader
|
|
513
|
+
|
|
514
|
+
```ts
|
|
515
|
+
// src/index.ts
|
|
516
|
+
import type { ShaderModule } from "@collabhut/plugin-sdk"
|
|
517
|
+
|
|
518
|
+
const mod: ShaderModule = {
|
|
519
|
+
manifest: {
|
|
520
|
+
id: "com.myorg.audio-circle",
|
|
521
|
+
name: "Audio Circle",
|
|
522
|
+
version: "1.0.0",
|
|
523
|
+
type: "shader",
|
|
524
|
+
description: "A circle that pulses with the music.",
|
|
525
|
+
author: { name: "My Org" },
|
|
526
|
+
pricing: "free",
|
|
527
|
+
minApiVersion: "0.1.0",
|
|
528
|
+
shadertoyCompatible: false,
|
|
529
|
+
},
|
|
530
|
+
shader: {
|
|
531
|
+
glsl: `
|
|
532
|
+
precision mediump float;
|
|
533
|
+
|
|
534
|
+
uniform vec2 uResolution;
|
|
535
|
+
uniform float uRms;
|
|
536
|
+
uniform float uBeat;
|
|
537
|
+
|
|
538
|
+
void main() {
|
|
539
|
+
vec2 uv = (gl_FragCoord.xy / uResolution) * 2.0 - 1.0;
|
|
540
|
+
uv.x *= uResolution.x / uResolution.y;
|
|
541
|
+
|
|
542
|
+
float radius = 0.3 + uRms * 0.4;
|
|
543
|
+
float ring = smoothstep(radius + 0.01, radius, length(uv));
|
|
544
|
+
float pulse = 0.5 + 0.5 * sin(uBeat * 6.2831);
|
|
545
|
+
|
|
546
|
+
vec3 col = mix(vec3(0.0, 0.4, 0.8), vec3(0.8, 0.2, 0.5), pulse);
|
|
547
|
+
gl_FragColor = vec4(col * ring, 1.0);
|
|
548
|
+
}
|
|
549
|
+
`,
|
|
550
|
+
uniforms: [
|
|
551
|
+
{ name: "uResolution", type: "vec2", source: "resolution" },
|
|
552
|
+
{ name: "uRms", type: "float", source: "audio-rms" },
|
|
553
|
+
{ name: "uBeat", type: "float", source: "beat" },
|
|
554
|
+
],
|
|
555
|
+
},
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
export default mod
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
### ShaderToy port
|
|
562
|
+
|
|
563
|
+
When `shadertoyCompatible: true`, CollabDAW pre-injects the standard ShaderToy
|
|
564
|
+
uniforms so you can paste ShaderToy code almost as-is.
|
|
565
|
+
|
|
566
|
+
Pre-injected uniforms (do **not** declare these in `uniforms`):
|
|
567
|
+
|
|
568
|
+
| ShaderToy name | Type | Source |
|
|
569
|
+
|---|---|---|
|
|
570
|
+
| `iTime` | float | Playback time in seconds |
|
|
571
|
+
| `iResolution` | vec3 | Canvas size (z = pixel ratio) |
|
|
572
|
+
| `iMouse` | vec4 | Mouse position |
|
|
573
|
+
| `iChannel0` | sampler2D | Audio spectrum texture |
|
|
574
|
+
|
|
575
|
+
```ts
|
|
576
|
+
import type { ShaderModule } from "@collabhut/plugin-sdk"
|
|
577
|
+
|
|
578
|
+
const mod: ShaderModule = {
|
|
579
|
+
manifest: {
|
|
580
|
+
id: "com.myorg.shadertoy-port",
|
|
581
|
+
name: "Classic Plasma",
|
|
582
|
+
version: "1.0.0",
|
|
583
|
+
type: "shader",
|
|
584
|
+
shadertoyCompatible: true, // ← enables automatic uniform injection
|
|
585
|
+
description: "ShaderToy plasma effect ported to CollabDAW.",
|
|
586
|
+
author: { name: "My Org" },
|
|
587
|
+
pricing: "free",
|
|
588
|
+
minApiVersion: "0.1.0",
|
|
589
|
+
},
|
|
590
|
+
shader: {
|
|
591
|
+
// iTime, iResolution, iMouse are injected automatically
|
|
592
|
+
glsl: `
|
|
593
|
+
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|
594
|
+
vec2 uv = fragCoord / iResolution.xy;
|
|
595
|
+
float t = iTime;
|
|
596
|
+
vec3 col = 0.5 + 0.5 * cos(t + uv.xyx + vec3(0, 2, 4));
|
|
597
|
+
fragColor = vec4(col, 1.0);
|
|
598
|
+
}
|
|
599
|
+
`,
|
|
600
|
+
},
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
export default mod
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
---
|
|
607
|
+
|
|
608
|
+
## Parameter system
|
|
609
|
+
|
|
610
|
+
Parameters appear as knobs, toggles, and dropdowns in the CollabDAW plugin GUI.
|
|
611
|
+
|
|
612
|
+
### Range parameter (knob / slider)
|
|
613
|
+
|
|
614
|
+
```ts
|
|
615
|
+
{
|
|
616
|
+
type: "range",
|
|
617
|
+
id: "cutoff", // unique within the plugin, used as key in params object
|
|
618
|
+
label: "Cutoff",
|
|
619
|
+
min: 20,
|
|
620
|
+
max: 20000,
|
|
621
|
+
default: 1000,
|
|
622
|
+
unit: "Hz", // displayed in the tooltip
|
|
623
|
+
curve: "log", // "linear" | "log" | "exp"
|
|
624
|
+
decimals: 0,
|
|
625
|
+
group: "Filter", // optional — groups params in the UI
|
|
626
|
+
}
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
### Bool parameter (toggle)
|
|
630
|
+
|
|
631
|
+
```ts
|
|
632
|
+
{
|
|
633
|
+
type: "bool",
|
|
634
|
+
id: "bypass",
|
|
635
|
+
label: "Bypass",
|
|
636
|
+
default: false,
|
|
637
|
+
}
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
### Choice parameter (dropdown)
|
|
641
|
+
|
|
642
|
+
```ts
|
|
643
|
+
{
|
|
644
|
+
type: "choice",
|
|
645
|
+
id: "filterType",
|
|
646
|
+
label: "Filter",
|
|
647
|
+
choices: ["lowpass", "highpass", "bandpass", "notch"],
|
|
648
|
+
default: "lowpass",
|
|
649
|
+
}
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
### Type-safe param access
|
|
653
|
+
|
|
654
|
+
The `params` argument in your factory is typed as
|
|
655
|
+
`Readonly<Record<string, number | boolean | string>>`. Cast to the concrete
|
|
656
|
+
type within your factory:
|
|
657
|
+
|
|
658
|
+
```ts
|
|
659
|
+
const cutoff = params.cutoff as number // from a "range" param
|
|
660
|
+
const bypass = params.bypass as boolean // from a "bool" param
|
|
661
|
+
const filter = params.filterType as string // from a "choice" param
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
---
|
|
665
|
+
|
|
666
|
+
## Note utilities
|
|
667
|
+
|
|
668
|
+
All helpers are pure functions, zero-dependency, and safe to use in Workers.
|
|
669
|
+
|
|
670
|
+
```ts
|
|
671
|
+
import {
|
|
672
|
+
noteToHz, hzToNote, hzToNoteFractional,
|
|
673
|
+
midiToName, nameToMidi,
|
|
674
|
+
semitonesBetween, transpose,
|
|
675
|
+
beatsToSeconds, secondsToBeats,
|
|
676
|
+
gainToDb, dbToGain,
|
|
677
|
+
clamp, lerp,
|
|
678
|
+
} from "@collabhut/plugin-sdk"
|
|
679
|
+
|
|
680
|
+
noteToHz(69) // 440 (A4)
|
|
681
|
+
noteToHz(60) // 261.63 (C4, middle C)
|
|
682
|
+
hzToNote(440) // 69
|
|
683
|
+
hzToNoteFractional(450) // 69.39 (fractional for pitch detection)
|
|
684
|
+
midiToName(60) // "C4"
|
|
685
|
+
midiToName(57) // "A3"
|
|
686
|
+
nameToMidi("A", 4) // 69
|
|
687
|
+
transpose(60, 7) // 67 (C4 → G4, perfect fifth)
|
|
688
|
+
transpose(60, -12) // 48 (C4 → C3, one octave down)
|
|
689
|
+
semitonesBetween(60, 67)// 7
|
|
690
|
+
|
|
691
|
+
beatsToSeconds(1, 120) // 0.5
|
|
692
|
+
secondsToBeats(0.5, 120)// 1
|
|
693
|
+
|
|
694
|
+
gainToDb(1) // 0
|
|
695
|
+
gainToDb(0.5) // -6.02
|
|
696
|
+
dbToGain(0) // 1
|
|
697
|
+
dbToGain(-6) // 0.501
|
|
698
|
+
|
|
699
|
+
clamp(1.5, 0, 1) // 1
|
|
700
|
+
lerp(0, 100, 0.5) // 50
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
---
|
|
704
|
+
|
|
705
|
+
## Security & sandbox model
|
|
706
|
+
|
|
707
|
+
### What plugins can do
|
|
708
|
+
|
|
709
|
+
- Use the Web Audio API through `context.audioContext`
|
|
710
|
+
- Fetch binary assets from `cdn.collabhut.com` via `context.fetchAsset()`
|
|
711
|
+
- Use `SharedArrayBuffer` for lock-free transport reads (provided by the host)
|
|
712
|
+
- Use standard Web APIs available in Workers: `crypto`, `TextEncoder`,
|
|
713
|
+
`URL`, `Math`, `JSON`, `console`
|
|
714
|
+
|
|
715
|
+
### What plugins cannot do
|
|
716
|
+
|
|
717
|
+
| Blocked | Reason |
|
|
718
|
+
|---|---|
|
|
719
|
+
| `fetch()` to arbitrary URLs | Prevented at the sandbox level; only `context.fetchAsset()` is available and it checks `cdn.collabhut.com` |
|
|
720
|
+
| `XMLHttpRequest` | Not available in Workers by default |
|
|
721
|
+
| DOM access (`document`, `window`) | Workers have no DOM |
|
|
722
|
+
| `localStorage`, `sessionStorage`, `IndexedDB` | Storage isolation |
|
|
723
|
+
| `WebSocket`, `WebRTC` | Network isolation |
|
|
724
|
+
| `navigator.mediaDevices` | Mic/camera are host-only |
|
|
725
|
+
| `eval()`, `new Function()` | Dynamic code execution is prohibited in the build validator |
|
|
726
|
+
|
|
727
|
+
### How the sandbox is enforced
|
|
728
|
+
|
|
729
|
+
1. **Build-time**: The CollabHut build service analyses your compiled bundle
|
|
730
|
+
with static analysis. Prohibited APIs (`eval`, `Function`, `XMLHttpRequest`,
|
|
731
|
+
direct `fetch`) cause the build to be rejected.
|
|
732
|
+
2. **Runtime**: The plugin Worker is created with a strict
|
|
733
|
+
`Content-Security-Policy` that blocks all network access except
|
|
734
|
+
`cdn.collabhut.com`.
|
|
735
|
+
3. **CDN fetch proxy**: All asset fetches go through `context.fetchAsset()`
|
|
736
|
+
which validates the URL before making the request on the main thread.
|
|
737
|
+
|
|
738
|
+
### Malicious code detection
|
|
739
|
+
|
|
740
|
+
The build service scans for:
|
|
741
|
+
|
|
742
|
+
- Obfuscated strings that expand to prohibited API names
|
|
743
|
+
- Attempts to access `globalThis` to bypass restrictions
|
|
744
|
+
- Prototype pollution patterns
|
|
745
|
+
- Infinite loops without exit conditions
|
|
746
|
+
|
|
747
|
+
Submissions that fail these checks are rejected and the developer account is
|
|
748
|
+
flagged for manual review.
|
|
749
|
+
|
|
750
|
+
---
|
|
751
|
+
|
|
752
|
+
## Licensing & verification
|
|
753
|
+
|
|
754
|
+
### How it works (no user friction)
|
|
755
|
+
|
|
756
|
+
CollabDAW verifies plugin licenses **server-side** when a project containing a
|
|
757
|
+
plugin is opened. The flow is:
|
|
758
|
+
|
|
759
|
+
```
|
|
760
|
+
DAW opens project
|
|
761
|
+
→ DAW sends session token + plugin manifest IDs to CollabHut API
|
|
762
|
+
→ API returns signed LicenseToken per plugin (or denial)
|
|
763
|
+
→ DAW validates token signature against embedded public key
|
|
764
|
+
→ Plugin is loaded (granted) or an in-app notice is shown (denied)
|
|
765
|
+
```
|
|
766
|
+
|
|
767
|
+
There is **no key-entry dialog** — users never type a license key.
|
|
768
|
+
|
|
769
|
+
### Token lifetime
|
|
770
|
+
|
|
771
|
+
Tokens are short-lived (typically 1 hour) so revocations take effect quickly.
|
|
772
|
+
The DAW re-verifies automatically in the background.
|
|
773
|
+
|
|
774
|
+
### Denial reasons
|
|
775
|
+
|
|
776
|
+
| Code | Shown when |
|
|
777
|
+
|---|---|
|
|
778
|
+
| `"not-purchased"` | User has not bought the plugin |
|
|
779
|
+
| `"revoked"` | License was revoked (charge-back, TOS violation) |
|
|
780
|
+
| `"collab-completed"` | The collaboration granting access is finished |
|
|
781
|
+
| `"collab-not-member"` | User was removed from the collaboration |
|
|
782
|
+
| `"expired"` | Token is past expiry — re-verification needed |
|
|
783
|
+
| `"plugin-delisted"` | Plugin was removed from the marketplace |
|
|
784
|
+
|
|
785
|
+
---
|
|
786
|
+
|
|
787
|
+
## Collaboration access rules
|
|
788
|
+
|
|
789
|
+
> **This section describes mandatory policy — deviation is not permitted.**
|
|
790
|
+
|
|
791
|
+
A user who does **not** directly own a paid plugin may still use it **if all of
|
|
792
|
+
the following conditions are met simultaneously**:
|
|
793
|
+
|
|
794
|
+
1. They are a participant in an **active** (non-completed) collaboration on
|
|
795
|
+
CollabHut.
|
|
796
|
+
2. At least one **other** participant in that same collaboration owns a valid
|
|
797
|
+
license for the plugin.
|
|
798
|
+
3. The plugin's `manifest.pricing` is `"paid"` (free plugins require no
|
|
799
|
+
license at all).
|
|
800
|
+
|
|
801
|
+
### What "active" means
|
|
802
|
+
|
|
803
|
+
A collaboration is active while its `status` field in the database is
|
|
804
|
+
`"active"` or `"pending"`. Once it is marked `"completed"`, collab-access
|
|
805
|
+
tokens are revoked at the next re-verification cycle.
|
|
806
|
+
|
|
807
|
+
### What non-owners can and cannot do
|
|
808
|
+
|
|
809
|
+
| Action | Non-owner with collab access | Without collab access |
|
|
810
|
+
|---|---|---|
|
|
811
|
+
| Use plugin during project session | ✅ | ❌ |
|
|
812
|
+
| Export / bounce with plugin active | ✅ (while collab active) | ❌ |
|
|
813
|
+
| Export after collab completes | ❌ | ❌ |
|
|
814
|
+
| Upload plugin to marketplace | ❌ (only the license owner can) | ❌ |
|
|
815
|
+
|
|
816
|
+
### Developer responsibility
|
|
817
|
+
|
|
818
|
+
You **do not** need to implement these rules in your plugin code — they are
|
|
819
|
+
enforced by CollabDAW and the CollabHut API. The `LicenseCheckResult` your
|
|
820
|
+
plugin receives is already resolved.
|
|
821
|
+
|
|
822
|
+
This policy is also documented in:
|
|
823
|
+
- CollabDAW's in-app scripting manual (Docs panel, `/docs`)
|
|
824
|
+
- The CollabHut Marketplace terms of service
|
|
825
|
+
- The developer agreement you accept when publishing
|
|
826
|
+
|
|
827
|
+
---
|
|
828
|
+
|
|
829
|
+
## Publishing to the CollabHut Marketplace
|
|
830
|
+
|
|
831
|
+
### Build your plugin
|
|
832
|
+
|
|
833
|
+
```bash
|
|
834
|
+
pnpm build
|
|
835
|
+
# or
|
|
836
|
+
npx tsc
|
|
837
|
+
```
|
|
838
|
+
|
|
839
|
+
Verify the output:
|
|
840
|
+
|
|
841
|
+
```bash
|
|
842
|
+
ls dist/
|
|
843
|
+
# index.js index.d.ts index.js.map
|
|
844
|
+
```
|
|
845
|
+
|
|
846
|
+
### Validate locally
|
|
847
|
+
|
|
848
|
+
```bash
|
|
849
|
+
npx collabhut-plugin-validate dist/index.js
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
This runs the same static analysis as the build service.
|
|
853
|
+
|
|
854
|
+
### Create a Marketplace listing
|
|
855
|
+
|
|
856
|
+
1. Go to [collabhut.com/marketplace/developer](https://collabhut.com/marketplace/developer)
|
|
857
|
+
2. Click **New Plugin Listing**
|
|
858
|
+
3. Fill in:
|
|
859
|
+
- Plugin name and description (pulled from `manifest` on upload)
|
|
860
|
+
- Pricing: free or paid (must match `manifest.pricing`)
|
|
861
|
+
- Category, tags, cover image
|
|
862
|
+
4. Upload `dist/index.js` — the build service signs the bundle
|
|
863
|
+
5. After approval (automated for free, manual review for paid), the plugin
|
|
864
|
+
goes live
|
|
865
|
+
|
|
866
|
+
### Bundle upload rules
|
|
867
|
+
|
|
868
|
+
| Rule | Detail |
|
|
869
|
+
|---|---|
|
|
870
|
+
| Single file only | `index.js` — no external dependencies at runtime |
|
|
871
|
+
| Max bundle size | 2 MB (request an increase for sample-based instruments) |
|
|
872
|
+
| No `.dawplugin` uploads | Plugin IDE working files are not marketplace bundles |
|
|
873
|
+
| No source maps required | Strip them for smaller uploads |
|
|
874
|
+
| No TypeScript source | Compiled JS only |
|
|
875
|
+
|
|
876
|
+
### Versioning
|
|
877
|
+
|
|
878
|
+
Follow semantic versioning. To publish an update:
|
|
879
|
+
1. Increment `version` in your `manifest` object
|
|
880
|
+
2. Rebuild and upload the new bundle
|
|
881
|
+
3. Old installs auto-update when users next open a project containing the plugin
|
|
882
|
+
|
|
883
|
+
### Pricing
|
|
884
|
+
|
|
885
|
+
- **Free plugins**: Unlimited downloads, no review delay
|
|
886
|
+
- **Paid plugins**: Set a price in USD (minimum $0.99). 80% revenue share.
|
|
887
|
+
Manual review takes 1–3 business days.
|
|
888
|
+
|
|
889
|
+
---
|
|
890
|
+
|
|
891
|
+
## Plugin IDE & .dawplugin files
|
|
892
|
+
|
|
893
|
+
CollabDAW includes a built-in **Plugin IDE** accessible from the sidebar
|
|
894
|
+
(Plugins screen). It lets you:
|
|
895
|
+
|
|
896
|
+
- Write and edit plugin TypeScript in a code editor
|
|
897
|
+
- Preview shader output in real time
|
|
898
|
+
- Test audio/MIDI effect output on a test signal
|
|
899
|
+
- Save your work as a **`.dawplugin` file** — a local project format
|
|
900
|
+
|
|
901
|
+
### .dawplugin format
|
|
902
|
+
|
|
903
|
+
A `.dawplugin` file is a ZIP archive containing:
|
|
904
|
+
|
|
905
|
+
```
|
|
906
|
+
my-plugin.dawplugin/
|
|
907
|
+
plugin.json ← manifest + IDE metadata
|
|
908
|
+
src/
|
|
909
|
+
index.ts ← your TypeScript source
|
|
910
|
+
assets/ ← local test assets (not included in marketplace bundle)
|
|
911
|
+
```
|
|
912
|
+
|
|
913
|
+
**`.dawplugin` files are for development only.** They cannot be uploaded to
|
|
914
|
+
the marketplace directly — you must build the compiled bundle first.
|
|
915
|
+
|
|
916
|
+
### Workflow
|
|
917
|
+
|
|
918
|
+
```
|
|
919
|
+
IDE → .dawplugin → tsc → dist/index.js → collabhut-plugin-validate → marketplace upload
|
|
920
|
+
```
|
|
921
|
+
|
|
922
|
+
---
|
|
923
|
+
|
|
924
|
+
## API reference
|
|
925
|
+
|
|
926
|
+
### `PluginManifest`
|
|
927
|
+
|
|
928
|
+
| Field | Type | Required | Description |
|
|
929
|
+
|---|---|---|---|
|
|
930
|
+
| `id` | `string` | ✅ | Globally unique reverse-domain ID, e.g. `"com.myorg.plugin-name"` |
|
|
931
|
+
| `name` | `string` | ✅ | Display name |
|
|
932
|
+
| `version` | `SemVer` | ✅ | Semantic version string, e.g. `"1.2.3"` |
|
|
933
|
+
| `type` | `PluginType` | ✅ | One of the five plugin types |
|
|
934
|
+
| `description` | `string` | ✅ | Short description (max 200 chars) |
|
|
935
|
+
| `author` | `PluginAuthor` | ✅ | Author name, optional URL and email |
|
|
936
|
+
| `homepage` | `string` | — | Plugin or developer homepage URL |
|
|
937
|
+
| `tags` | `string[]` | — | Discoverability tags |
|
|
938
|
+
| `params` | `PluginParam[]` | — | Parameter definitions |
|
|
939
|
+
| `pricing` | `"free" \| "paid"` | ✅ | Must match marketplace listing |
|
|
940
|
+
| `minApiVersion` | `SemVer` | ✅ | Minimum CollabDAW API version required |
|
|
941
|
+
| `shadertoyCompatible` | `boolean` | — | Only for `type: "shader"` |
|
|
942
|
+
|
|
943
|
+
### `PluginContext`
|
|
944
|
+
|
|
945
|
+
Injected into every factory function.
|
|
946
|
+
|
|
947
|
+
| Property | Type | Description |
|
|
948
|
+
|---|---|---|
|
|
949
|
+
| `audioContext` | `AudioContext` | The track's Web Audio context |
|
|
950
|
+
| `sampleRate` | `number` | Current sample rate in Hz |
|
|
951
|
+
| `bpm` | `number` | Current session BPM |
|
|
952
|
+
| `positionBeats` | `number` | Current playback position in beats |
|
|
953
|
+
| `isPlaying` | `boolean` | Whether the transport is playing |
|
|
954
|
+
| `fetchAsset(url)` | `Promise<ArrayBuffer>` | Fetch a CDN asset (CDN-only) |
|
|
955
|
+
|
|
956
|
+
`InstrumentContext` extends `PluginContext` with:
|
|
957
|
+
|
|
958
|
+
| Property | Type | Description |
|
|
959
|
+
|---|---|---|
|
|
960
|
+
| `maxPolyphony` | `number` | Maximum simultaneous voices the host will trigger |
|
|
961
|
+
|
|
962
|
+
### `AudioEffectIO`
|
|
963
|
+
|
|
964
|
+
Returned by an audio-effect factory.
|
|
965
|
+
|
|
966
|
+
| Property | Type | Required | Description |
|
|
967
|
+
|---|---|---|---|
|
|
968
|
+
| `input` | `AudioNode` | ✅ | Host connects track signal here |
|
|
969
|
+
| `output` | `AudioNode` | ✅ | Plugin outputs processed audio here |
|
|
970
|
+
| `sidechain` | `AudioNode` | — | Optional side-chain input |
|
|
971
|
+
| `automationTargets` | `Record<string, AudioParam>` | — | Automation-capable params |
|
|
972
|
+
|
|
973
|
+
### `MidiEffectPlugin`
|
|
974
|
+
|
|
975
|
+
| Method | Signature | Description |
|
|
976
|
+
|---|---|---|
|
|
977
|
+
| `process` | `(events: ReadonlyArray<MidiEvent>) => MidiEvent[]` | Process one audio block |
|
|
978
|
+
| `dispose` | `() => void` | Clean up resources |
|
|
979
|
+
|
|
980
|
+
### `InstrumentVoice`
|
|
981
|
+
|
|
982
|
+
| Member | Description |
|
|
983
|
+
|---|---|
|
|
984
|
+
| `output: AudioNode` | Audio output for this voice |
|
|
985
|
+
| `stop(velocity, time)` | Begin release phase |
|
|
986
|
+
| `kill()` | Immediate hard stop (no release) |
|
|
987
|
+
|
|
988
|
+
### `InstrumentPlugin`
|
|
989
|
+
|
|
990
|
+
| Method | Description |
|
|
991
|
+
|---|---|
|
|
992
|
+
| `noteOn(note, velocity, time)` | Start a new voice |
|
|
993
|
+
| `onMidi(event)` | Handle non-note MIDI events |
|
|
994
|
+
| `dispose()` | Clean up resources |
|
|
995
|
+
|
|
996
|
+
---
|
|
997
|
+
|
|
998
|
+
## Frequently asked questions
|
|
999
|
+
|
|
1000
|
+
**Can I use npm packages in my plugin?**
|
|
1001
|
+
|
|
1002
|
+
Only packages that compile to pure ES2022+ JavaScript with no DOM or Node.js
|
|
1003
|
+
dependencies. All dependencies must be bundled into your single `index.js`.
|
|
1004
|
+
Use a bundler like `esbuild` or `rollup` before upload.
|
|
1005
|
+
|
|
1006
|
+
**Can I use WebAssembly?**
|
|
1007
|
+
|
|
1008
|
+
Yes — load your `.wasm` module via `context.fetchAsset()` and instantiate with
|
|
1009
|
+
`WebAssembly.instantiate()`. The `.wasm` file must be hosted on
|
|
1010
|
+
`cdn.collabhut.com`; contact support to have assets uploaded there.
|
|
1011
|
+
|
|
1012
|
+
**Can I access the microphone?**
|
|
1013
|
+
|
|
1014
|
+
No — audio input is managed exclusively by the host. For instrument plugins
|
|
1015
|
+
the host provides a MIDI stream; for audio effects the host provides the
|
|
1016
|
+
rendered audio block.
|
|
1017
|
+
|
|
1018
|
+
**How do I test my plugin locally before publishing?**
|
|
1019
|
+
|
|
1020
|
+
Use the Plugin IDE built into CollabDAW (sidebar → Plugins). It gives you a
|
|
1021
|
+
live preview environment with a test signal.
|
|
1022
|
+
|
|
1023
|
+
**My plugin uses external SDKs (Tone.js, etc.) — is that allowed?**
|
|
1024
|
+
|
|
1025
|
+
You can bundle them, but they must not attempt network access or DOM manipulation.
|
|
1026
|
+
Tone.js is partially safe; avoid its transport and timeline features
|
|
1027
|
+
(which inject into `window`) and use only its DSP utilities.
|
|
1028
|
+
|
|
1029
|
+
**Can I use `async` / `await` in my factory functions?**
|
|
1030
|
+
|
|
1031
|
+
Yes — the DAW awaits the factory if it returns a `Promise`. Use it freely
|
|
1032
|
+
for one-shot setup like loading impulse responses or WASM modules. The plugin
|
|
1033
|
+
will not be connected to the audio graph until the promise resolves, so there
|
|
1034
|
+
is no risk of glitches.
|
|
1035
|
+
|
|
1036
|
+
**What happens to collab-access plugins when a project is opened outside of a collab?**
|
|
1037
|
+
|
|
1038
|
+
The plugin will show a "License not available outside collaboration" notice.
|
|
1039
|
+
The track will be muted until either the user purchases the plugin or they open
|
|
1040
|
+
the project from within an active collaboration where a participant owns a
|
|
1041
|
+
license.
|
|
1042
|
+
|
|
1043
|
+
---
|
|
1044
|
+
|
|
1045
|
+
*For support and community discussion, visit the
|
|
1046
|
+
[CollabHut Developer Discord](https://discord.gg/collabhut).*
|