@audio/host-clap 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/binding.gyp +18 -0
- package/index.js +159 -0
- package/native/binding.c +160 -0
- package/native/clap.h +163 -0
- package/native/host.c +184 -0
- package/native/host.h +29 -0
- package/package.json +40 -0
- package/src/addon.js +26 -0
package/binding.gyp
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"targets": [{
|
|
3
|
+
"target_name": "host_clap",
|
|
4
|
+
"sources": ["native/host.c", "native/binding.c"],
|
|
5
|
+
"include_dirs": ["native"],
|
|
6
|
+
"cflags": ["-std=c99", "-O2"],
|
|
7
|
+
"xcode_settings": {
|
|
8
|
+
"OTHER_CFLAGS": ["-std=c99", "-O2"],
|
|
9
|
+
"MACOSX_DEPLOYMENT_TARGET": "10.13"
|
|
10
|
+
},
|
|
11
|
+
"msvs_settings": {
|
|
12
|
+
"VCCLCompilerTool": { "AdditionalOptions": ["/O2"] }
|
|
13
|
+
},
|
|
14
|
+
"conditions": [
|
|
15
|
+
["OS=='linux'", { "libraries": ["-ldl"] }]
|
|
16
|
+
]
|
|
17
|
+
}]
|
|
18
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @audio/host-clap
|
|
3
|
+
* CLAP plugin host for audiojs
|
|
4
|
+
*/
|
|
5
|
+
import addon from './src/addon.js'
|
|
6
|
+
import { readdirSync, statSync } from 'fs'
|
|
7
|
+
import { join } from 'path'
|
|
8
|
+
import { execFileSync } from 'child_process'
|
|
9
|
+
|
|
10
|
+
export function load(path, opts = {}) {
|
|
11
|
+
const { sampleRate = 44100, channels = 2, blockSize = 128 } = opts
|
|
12
|
+
const handle = addon.open(path, sampleRate, channels, blockSize)
|
|
13
|
+
let closed = false
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
get name() { return addon.getName(handle) },
|
|
17
|
+
get vendor() { return addon.getVendor(handle) },
|
|
18
|
+
blockSize,
|
|
19
|
+
|
|
20
|
+
get params() {
|
|
21
|
+
const n = addon.getParamCount(handle)
|
|
22
|
+
const out = []
|
|
23
|
+
for (let i = 0; i < n; i++) {
|
|
24
|
+
const info = addon.getParamInfo(handle, i)
|
|
25
|
+
if (info) out.push(info)
|
|
26
|
+
}
|
|
27
|
+
return out
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
getParam(id) { return addon.getParam(handle, id) },
|
|
31
|
+
|
|
32
|
+
process(inputs, outputs) { addon.process(handle, inputs, outputs) },
|
|
33
|
+
processAll(ch) { return processBlocks(addon, handle, ch, blockSize) },
|
|
34
|
+
|
|
35
|
+
close() {
|
|
36
|
+
if (closed) return
|
|
37
|
+
closed = true
|
|
38
|
+
addon.close(handle)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Scan system for installed CLAP plugins.
|
|
45
|
+
* Returns array of { path, name, vendor, format } for each loadable plugin.
|
|
46
|
+
*/
|
|
47
|
+
export function scan(dirs) {
|
|
48
|
+
return scanDir(dirs || defaultPaths({
|
|
49
|
+
darwin: ['/Library/Audio/Plug-Ins/CLAP', '$HOME/Library/Audio/Plug-Ins/CLAP'],
|
|
50
|
+
linux: ['$HOME/.clap', '/usr/lib/clap', '/usr/local/lib/clap'],
|
|
51
|
+
win32: ['$PROGRAMFILES/Common Files/CLAP'],
|
|
52
|
+
}), '.clap', 'clap')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Register a CLAP plugin as an AudioWorkletProcessor.
|
|
57
|
+
*
|
|
58
|
+
* import { AudioWorkletProcessor } from 'web-audio-api'
|
|
59
|
+
* await ctx.audioWorklet.addModule(register(path, AudioWorkletProcessor))
|
|
60
|
+
* const node = new AudioWorkletNode(ctx, 'Plugin Name')
|
|
61
|
+
*/
|
|
62
|
+
export function register(path, BaseClass, opts = {}) {
|
|
63
|
+
const probe = load(path, { ...opts, sampleRate: opts.sampleRate || 44100 })
|
|
64
|
+
const name = probe.name
|
|
65
|
+
const descriptors = probe.params.map(p => ({
|
|
66
|
+
name: p.name,
|
|
67
|
+
defaultValue: p.defaultValue,
|
|
68
|
+
minValue: p.min,
|
|
69
|
+
maxValue: p.max,
|
|
70
|
+
automationRate: 'k-rate'
|
|
71
|
+
}))
|
|
72
|
+
probe.close()
|
|
73
|
+
|
|
74
|
+
return function(scope) {
|
|
75
|
+
class PluginProcessor extends BaseClass {
|
|
76
|
+
static get parameterDescriptors() { return descriptors }
|
|
77
|
+
|
|
78
|
+
constructor(options) {
|
|
79
|
+
super(options)
|
|
80
|
+
this._handle = addon.open(path, scope.sampleRate,
|
|
81
|
+
opts.channels || 2, opts.blockSize || 128)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
process(inputs, outputs) {
|
|
85
|
+
const input = inputs[0], output = outputs[0]
|
|
86
|
+
if (!output || !output.length) return true
|
|
87
|
+
addon.process(this._handle, input.length ? input : null, output)
|
|
88
|
+
return true
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
scope.registerProcessor(name, PluginProcessor)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function defaultPaths(map) {
|
|
97
|
+
const plat = process.platform
|
|
98
|
+
return (map[plat] || []).map(p =>
|
|
99
|
+
p.replace('$HOME', process.env.HOME || process.env.USERPROFILE || '')
|
|
100
|
+
.replace('$PROGRAMFILES', process.env.PROGRAMFILES || 'C:\\Program Files'))
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function scanDir(dirs, ext, format) {
|
|
104
|
+
const paths = []
|
|
105
|
+
for (const dir of dirs) {
|
|
106
|
+
let entries
|
|
107
|
+
try { entries = readdirSync(dir) } catch { continue }
|
|
108
|
+
for (const name of entries) {
|
|
109
|
+
const full = join(dir, name)
|
|
110
|
+
if (name.endsWith(ext)) { paths.push(full); continue }
|
|
111
|
+
try {
|
|
112
|
+
if (statSync(full).isDirectory())
|
|
113
|
+
for (const sub of readdirSync(full))
|
|
114
|
+
if (sub.endsWith(ext)) paths.push(join(full, sub))
|
|
115
|
+
} catch {}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const results = []
|
|
120
|
+
for (const path of paths) {
|
|
121
|
+
try {
|
|
122
|
+
const code = `import{load}from'${import.meta.url}';try{const p=load(${JSON.stringify(path)});console.log(JSON.stringify({path:${JSON.stringify(path)},name:p.name,vendor:p.vendor,format:'${format}'}));p.close()}catch{}`
|
|
123
|
+
const out = execFileSync(process.execPath, ['--input-type=module', '-e', code],
|
|
124
|
+
{ timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] })
|
|
125
|
+
const line = out.toString().trim()
|
|
126
|
+
if (line) results.push(JSON.parse(line))
|
|
127
|
+
} catch {}
|
|
128
|
+
}
|
|
129
|
+
return results
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function processBlocks(addon, handle, channels, blockSize) {
|
|
133
|
+
const len = channels[0].length, numCh = channels.length
|
|
134
|
+
const out = Array.from({ length: numCh }, () => new Float32Array(len))
|
|
135
|
+
|
|
136
|
+
const inBlock = Array.from({ length: numCh }, () => new Float32Array(blockSize))
|
|
137
|
+
const outBlock = Array.from({ length: numCh }, () => new Float32Array(blockSize))
|
|
138
|
+
|
|
139
|
+
for (let off = 0; off < len; off += blockSize) {
|
|
140
|
+
const n = Math.min(blockSize, len - off)
|
|
141
|
+
if (n === blockSize) {
|
|
142
|
+
const inSub = [], outSub = []
|
|
143
|
+
for (let c = 0; c < numCh; c++) {
|
|
144
|
+
inSub[c] = channels[c].subarray(off, off + blockSize)
|
|
145
|
+
outSub[c] = out[c].subarray(off, off + blockSize)
|
|
146
|
+
}
|
|
147
|
+
addon.process(handle, inSub, outSub)
|
|
148
|
+
} else {
|
|
149
|
+
for (let c = 0; c < numCh; c++) {
|
|
150
|
+
inBlock[c].fill(0)
|
|
151
|
+
inBlock[c].set(channels[c].subarray(off, off + n))
|
|
152
|
+
outBlock[c].fill(0)
|
|
153
|
+
}
|
|
154
|
+
addon.process(handle, inBlock, outBlock)
|
|
155
|
+
for (let c = 0; c < numCh; c++) out[c].set(outBlock[c].subarray(0, n), off)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return out
|
|
159
|
+
}
|
package/native/binding.c
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* binding.c — NAPI bindings for CLAP host
|
|
3
|
+
*/
|
|
4
|
+
#include <node_api.h>
|
|
5
|
+
#include "host.h"
|
|
6
|
+
|
|
7
|
+
#define NAPI_CALL(env, call) do { \
|
|
8
|
+
napi_status s = (call); \
|
|
9
|
+
if (s != napi_ok) { napi_throw_error(env, NULL, #call " failed"); return NULL; } \
|
|
10
|
+
} while(0)
|
|
11
|
+
|
|
12
|
+
static void destructor(napi_env env, void* data, void* hint) {
|
|
13
|
+
(void)env; (void)hint;
|
|
14
|
+
clap_host_destroy((clap_host_plugin_t*)data);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
static napi_value node_open(napi_env env, napi_callback_info info) {
|
|
18
|
+
size_t argc = 4; napi_value argv[4];
|
|
19
|
+
NAPI_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL));
|
|
20
|
+
|
|
21
|
+
char path[2048]; size_t len;
|
|
22
|
+
NAPI_CALL(env, napi_get_value_string_utf8(env, argv[0], path, sizeof(path), &len));
|
|
23
|
+
|
|
24
|
+
double sampleRate = 44100; int channels = 2, blockSize = 128;
|
|
25
|
+
if (argc > 1) napi_get_value_double(env, argv[1], &sampleRate);
|
|
26
|
+
if (argc > 2) napi_get_value_int32(env, argv[2], &channels);
|
|
27
|
+
if (argc > 3) napi_get_value_int32(env, argv[3], &blockSize);
|
|
28
|
+
|
|
29
|
+
clap_host_plugin_t* plugin = clap_host_open(path, sampleRate, channels, blockSize);
|
|
30
|
+
if (!plugin) {
|
|
31
|
+
const char* err = clap_host_get_error();
|
|
32
|
+
napi_throw_error(env, NULL, err[0] ? err : "Failed to load CLAP plugin");
|
|
33
|
+
return NULL;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
napi_value ext;
|
|
37
|
+
NAPI_CALL(env, napi_create_external(env, plugin, destructor, NULL, &ext));
|
|
38
|
+
return ext;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static napi_value node_close(napi_env env, napi_callback_info info) {
|
|
42
|
+
size_t argc = 1; napi_value argv[1];
|
|
43
|
+
napi_get_cb_info(env, info, &argc, argv, NULL, NULL);
|
|
44
|
+
clap_host_plugin_t* p; napi_get_value_external(env, argv[0], (void**)&p);
|
|
45
|
+
clap_host_close(p);
|
|
46
|
+
return NULL;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
static napi_value node_getName(napi_env env, napi_callback_info info) {
|
|
50
|
+
size_t argc = 1; napi_value argv[1];
|
|
51
|
+
napi_get_cb_info(env, info, &argc, argv, NULL, NULL);
|
|
52
|
+
clap_host_plugin_t* p; napi_get_value_external(env, argv[0], (void**)&p);
|
|
53
|
+
napi_value r; napi_create_string_utf8(env, clap_host_get_name(p), NAPI_AUTO_LENGTH, &r);
|
|
54
|
+
return r;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
static napi_value node_getVendor(napi_env env, napi_callback_info info) {
|
|
58
|
+
size_t argc = 1; napi_value argv[1];
|
|
59
|
+
napi_get_cb_info(env, info, &argc, argv, NULL, NULL);
|
|
60
|
+
clap_host_plugin_t* p; napi_get_value_external(env, argv[0], (void**)&p);
|
|
61
|
+
napi_value r; napi_create_string_utf8(env, clap_host_get_vendor(p), NAPI_AUTO_LENGTH, &r);
|
|
62
|
+
return r;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
static napi_value node_getParamCount(napi_env env, napi_callback_info info) {
|
|
66
|
+
size_t argc = 1; napi_value argv[1];
|
|
67
|
+
napi_get_cb_info(env, info, &argc, argv, NULL, NULL);
|
|
68
|
+
clap_host_plugin_t* p; napi_get_value_external(env, argv[0], (void**)&p);
|
|
69
|
+
napi_value r; napi_create_int32(env, clap_host_get_param_count(p), &r);
|
|
70
|
+
return r;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
static napi_value node_getParamInfo(napi_env env, napi_callback_info info) {
|
|
74
|
+
size_t argc = 2; napi_value argv[2];
|
|
75
|
+
napi_get_cb_info(env, info, &argc, argv, NULL, NULL);
|
|
76
|
+
clap_host_plugin_t* p; napi_get_value_external(env, argv[0], (void**)&p);
|
|
77
|
+
int index; napi_get_value_int32(env, argv[1], &index);
|
|
78
|
+
|
|
79
|
+
clap_host_param_info_t pi;
|
|
80
|
+
if (clap_host_get_param_info(p, index, &pi) != 0) return NULL;
|
|
81
|
+
|
|
82
|
+
napi_value obj, val;
|
|
83
|
+
napi_create_object(env, &obj);
|
|
84
|
+
napi_create_uint32(env, pi.id, &val); napi_set_named_property(env, obj, "id", val);
|
|
85
|
+
napi_create_string_utf8(env, pi.name, NAPI_AUTO_LENGTH, &val); napi_set_named_property(env, obj, "name", val);
|
|
86
|
+
napi_create_double(env, pi.min, &val); napi_set_named_property(env, obj, "min", val);
|
|
87
|
+
napi_create_double(env, pi.max, &val); napi_set_named_property(env, obj, "max", val);
|
|
88
|
+
napi_create_double(env, pi.defaultValue, &val); napi_set_named_property(env, obj, "defaultValue", val);
|
|
89
|
+
return obj;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
static napi_value node_getParam(napi_env env, napi_callback_info info) {
|
|
93
|
+
size_t argc = 2; napi_value argv[2];
|
|
94
|
+
napi_get_cb_info(env, info, &argc, argv, NULL, NULL);
|
|
95
|
+
clap_host_plugin_t* p; napi_get_value_external(env, argv[0], (void**)&p);
|
|
96
|
+
uint32_t id; napi_get_value_uint32(env, argv[1], &id);
|
|
97
|
+
napi_value r; napi_create_double(env, clap_host_get_param(p, id), &r);
|
|
98
|
+
return r;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
static napi_value node_process(napi_env env, napi_callback_info info) {
|
|
102
|
+
size_t argc = 3; napi_value argv[3];
|
|
103
|
+
napi_get_cb_info(env, info, &argc, argv, NULL, NULL);
|
|
104
|
+
clap_host_plugin_t* p; napi_get_value_external(env, argv[0], (void**)&p);
|
|
105
|
+
|
|
106
|
+
float* inPtrs[8] = {0};
|
|
107
|
+
float* outPtrs[8] = {0};
|
|
108
|
+
uint32_t numCh = 0;
|
|
109
|
+
size_t numSamples = 0;
|
|
110
|
+
int hasInputs = 0;
|
|
111
|
+
|
|
112
|
+
napi_valuetype inputType;
|
|
113
|
+
napi_typeof(env, argv[1], &inputType);
|
|
114
|
+
if (inputType != napi_null && inputType != napi_undefined) {
|
|
115
|
+
hasInputs = 1;
|
|
116
|
+
napi_get_array_length(env, argv[1], &numCh);
|
|
117
|
+
for (uint32_t i = 0; i < numCh && i < 8; i++) {
|
|
118
|
+
napi_value el; napi_get_element(env, argv[1], i, &el);
|
|
119
|
+
void* data; size_t len;
|
|
120
|
+
napi_get_typedarray_info(env, el, NULL, &len, &data, NULL, NULL);
|
|
121
|
+
inPtrs[i] = (float*)data;
|
|
122
|
+
if (i == 0) numSamples = len;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
uint32_t outCh;
|
|
127
|
+
napi_get_array_length(env, argv[2], &outCh);
|
|
128
|
+
for (uint32_t i = 0; i < outCh && i < 8; i++) {
|
|
129
|
+
napi_value el; napi_get_element(env, argv[2], i, &el);
|
|
130
|
+
void* data;
|
|
131
|
+
napi_get_typedarray_info(env, el, NULL, NULL, &data, NULL, NULL);
|
|
132
|
+
outPtrs[i] = (float*)data;
|
|
133
|
+
}
|
|
134
|
+
if (!numCh) numCh = outCh;
|
|
135
|
+
if (!numSamples) {
|
|
136
|
+
napi_value el; napi_get_element(env, argv[2], 0, &el);
|
|
137
|
+
size_t len; napi_get_typedarray_info(env, el, NULL, &len, NULL, NULL, NULL);
|
|
138
|
+
numSamples = len;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
clap_host_process(p, hasInputs ? inPtrs : NULL, outPtrs, numCh, (int)numSamples);
|
|
142
|
+
return NULL;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
static napi_value init(napi_env env, napi_value exports) {
|
|
146
|
+
napi_property_descriptor props[] = {
|
|
147
|
+
{ "open", NULL, node_open, NULL, NULL, NULL, napi_default, NULL },
|
|
148
|
+
{ "close", NULL, node_close, NULL, NULL, NULL, napi_default, NULL },
|
|
149
|
+
{ "getName", NULL, node_getName, NULL, NULL, NULL, napi_default, NULL },
|
|
150
|
+
{ "getVendor", NULL, node_getVendor, NULL, NULL, NULL, napi_default, NULL },
|
|
151
|
+
{ "getParamCount", NULL, node_getParamCount, NULL, NULL, NULL, napi_default, NULL },
|
|
152
|
+
{ "getParamInfo", NULL, node_getParamInfo, NULL, NULL, NULL, napi_default, NULL },
|
|
153
|
+
{ "getParam", NULL, node_getParam, NULL, NULL, NULL, napi_default, NULL },
|
|
154
|
+
{ "process", NULL, node_process, NULL, NULL, NULL, napi_default, NULL },
|
|
155
|
+
};
|
|
156
|
+
napi_define_properties(env, exports, 8, props);
|
|
157
|
+
return exports;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
NAPI_MODULE(NODE_GYP_MODULE_NAME, init)
|
package/native/clap.h
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* clap.h — Minimal CLAP interface definitions for plugin hosting
|
|
3
|
+
*
|
|
4
|
+
* Based on CLAP SDK (https://github.com/free-audio/clap), MIT License.
|
|
5
|
+
* Only types needed for basic audio hosting are included.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
#pragma once
|
|
9
|
+
#include <stdint.h>
|
|
10
|
+
#include <stdbool.h>
|
|
11
|
+
|
|
12
|
+
#define CLAP_VERSION_MAJOR 1
|
|
13
|
+
#define CLAP_VERSION_MINOR 2
|
|
14
|
+
#define CLAP_VERSION_REVISION 2
|
|
15
|
+
#define CLAP_VERSION_INIT { CLAP_VERSION_MAJOR, CLAP_VERSION_MINOR, CLAP_VERSION_REVISION }
|
|
16
|
+
|
|
17
|
+
typedef struct { uint32_t major, minor, revision; } clap_version_t;
|
|
18
|
+
|
|
19
|
+
/* --- Entry point (exported from .clap binary) --- */
|
|
20
|
+
|
|
21
|
+
typedef struct clap_plugin_entry {
|
|
22
|
+
clap_version_t clap_version;
|
|
23
|
+
bool (*init)(const char* plugin_path);
|
|
24
|
+
void (*deinit)(void);
|
|
25
|
+
const void* (*get_factory)(const char* factory_id);
|
|
26
|
+
} clap_plugin_entry_t;
|
|
27
|
+
|
|
28
|
+
/* --- Plugin factory --- */
|
|
29
|
+
|
|
30
|
+
#define CLAP_PLUGIN_FACTORY_ID "clap.plugin-factory"
|
|
31
|
+
|
|
32
|
+
typedef struct clap_plugin_descriptor {
|
|
33
|
+
clap_version_t clap_version;
|
|
34
|
+
const char* id;
|
|
35
|
+
const char* name;
|
|
36
|
+
const char* vendor;
|
|
37
|
+
const char* url;
|
|
38
|
+
const char* manual_url;
|
|
39
|
+
const char* support_url;
|
|
40
|
+
const char* version;
|
|
41
|
+
const char* description;
|
|
42
|
+
const char* const* features;
|
|
43
|
+
} clap_plugin_descriptor_t;
|
|
44
|
+
|
|
45
|
+
typedef struct clap_plugin clap_plugin_t;
|
|
46
|
+
typedef struct clap_host clap_host_t;
|
|
47
|
+
|
|
48
|
+
typedef struct clap_plugin_factory {
|
|
49
|
+
uint32_t (*get_plugin_count)(const struct clap_plugin_factory* factory);
|
|
50
|
+
const clap_plugin_descriptor_t* (*get_plugin_descriptor)(const struct clap_plugin_factory* factory, uint32_t index);
|
|
51
|
+
const clap_plugin_t* (*create_plugin)(const struct clap_plugin_factory* factory, const clap_host_t* host, const char* plugin_id);
|
|
52
|
+
} clap_plugin_factory_t;
|
|
53
|
+
|
|
54
|
+
/* --- Host (we implement this) --- */
|
|
55
|
+
|
|
56
|
+
struct clap_host {
|
|
57
|
+
clap_version_t clap_version;
|
|
58
|
+
void* host_data;
|
|
59
|
+
const char* name;
|
|
60
|
+
const char* vendor;
|
|
61
|
+
const char* url;
|
|
62
|
+
const char* version;
|
|
63
|
+
const void* (*get_extension)(const clap_host_t* host, const char* extension_id);
|
|
64
|
+
void (*request_restart)(const clap_host_t* host);
|
|
65
|
+
void (*request_process)(const clap_host_t* host);
|
|
66
|
+
void (*request_callback)(const clap_host_t* host);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/* --- Audio buffer --- */
|
|
70
|
+
|
|
71
|
+
typedef struct clap_audio_buffer {
|
|
72
|
+
float** data32;
|
|
73
|
+
double** data64;
|
|
74
|
+
uint32_t channel_count;
|
|
75
|
+
uint32_t latency;
|
|
76
|
+
uint64_t constant_mask;
|
|
77
|
+
} clap_audio_buffer_t;
|
|
78
|
+
|
|
79
|
+
/* --- Events --- */
|
|
80
|
+
|
|
81
|
+
typedef struct clap_event_header {
|
|
82
|
+
uint32_t size;
|
|
83
|
+
uint32_t time;
|
|
84
|
+
uint16_t space_id;
|
|
85
|
+
uint16_t type;
|
|
86
|
+
uint32_t flags;
|
|
87
|
+
} clap_event_header_t;
|
|
88
|
+
|
|
89
|
+
typedef struct clap_input_events {
|
|
90
|
+
void* ctx;
|
|
91
|
+
uint32_t (*size)(const struct clap_input_events* list);
|
|
92
|
+
const clap_event_header_t* (*get)(const struct clap_input_events* list, uint32_t index);
|
|
93
|
+
} clap_input_events_t;
|
|
94
|
+
|
|
95
|
+
typedef struct clap_output_events {
|
|
96
|
+
void* ctx;
|
|
97
|
+
bool (*try_push)(const struct clap_output_events* list, const clap_event_header_t* event);
|
|
98
|
+
} clap_output_events_t;
|
|
99
|
+
|
|
100
|
+
/* --- Process --- */
|
|
101
|
+
|
|
102
|
+
typedef enum {
|
|
103
|
+
CLAP_PROCESS_ERROR = 0,
|
|
104
|
+
CLAP_PROCESS_CONTINUE = 1,
|
|
105
|
+
CLAP_PROCESS_CONTINUE_IF_NOT_QUIET = 2,
|
|
106
|
+
CLAP_PROCESS_TAIL = 3,
|
|
107
|
+
CLAP_PROCESS_SLEEP = 4,
|
|
108
|
+
} clap_process_status;
|
|
109
|
+
|
|
110
|
+
typedef struct clap_process {
|
|
111
|
+
int64_t steady_time;
|
|
112
|
+
uint32_t frames_count;
|
|
113
|
+
const clap_input_events_t* in_events;
|
|
114
|
+
const clap_output_events_t* out_events;
|
|
115
|
+
const clap_audio_buffer_t* audio_inputs;
|
|
116
|
+
clap_audio_buffer_t* audio_outputs;
|
|
117
|
+
uint32_t audio_inputs_count;
|
|
118
|
+
uint32_t audio_outputs_count;
|
|
119
|
+
const void* transport;
|
|
120
|
+
} clap_process_t;
|
|
121
|
+
|
|
122
|
+
/* --- Plugin interface --- */
|
|
123
|
+
|
|
124
|
+
struct clap_plugin {
|
|
125
|
+
const clap_plugin_descriptor_t* desc;
|
|
126
|
+
void* plugin_data;
|
|
127
|
+
bool (*init)(const clap_plugin_t* plugin);
|
|
128
|
+
void (*destroy)(const clap_plugin_t* plugin);
|
|
129
|
+
bool (*activate)(const clap_plugin_t* plugin, double sample_rate, uint32_t min_frames, uint32_t max_frames);
|
|
130
|
+
void (*deactivate)(const clap_plugin_t* plugin);
|
|
131
|
+
bool (*start_processing)(const clap_plugin_t* plugin);
|
|
132
|
+
void (*stop_processing)(const clap_plugin_t* plugin);
|
|
133
|
+
void (*reset)(const clap_plugin_t* plugin);
|
|
134
|
+
clap_process_status (*process)(const clap_plugin_t* plugin, const clap_process_t* process);
|
|
135
|
+
const void* (*get_extension)(const clap_plugin_t* plugin, const char* id);
|
|
136
|
+
void (*on_main_thread)(const clap_plugin_t* plugin);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/* --- Params extension --- */
|
|
140
|
+
|
|
141
|
+
#define CLAP_EXT_PARAMS "clap.params"
|
|
142
|
+
typedef uint32_t clap_id;
|
|
143
|
+
|
|
144
|
+
typedef struct clap_param_info {
|
|
145
|
+
clap_id id;
|
|
146
|
+
uint32_t flags;
|
|
147
|
+
void* cookie;
|
|
148
|
+
char name[256];
|
|
149
|
+
char module[1024];
|
|
150
|
+
double min_value;
|
|
151
|
+
double max_value;
|
|
152
|
+
double default_value;
|
|
153
|
+
} clap_param_info_t;
|
|
154
|
+
|
|
155
|
+
typedef struct clap_plugin_params {
|
|
156
|
+
uint32_t (*count)(const clap_plugin_t* plugin);
|
|
157
|
+
bool (*get_info)(const clap_plugin_t* plugin, uint32_t param_index, clap_param_info_t* param_info);
|
|
158
|
+
bool (*get_value)(const clap_plugin_t* plugin, clap_id param_id, double* out_value);
|
|
159
|
+
bool (*value_to_text)(const clap_plugin_t* plugin, clap_id param_id, double value, char* out_buffer, uint32_t out_buffer_capacity);
|
|
160
|
+
bool (*text_to_value)(const clap_plugin_t* plugin, clap_id param_id, const char* param_value_text, double* out_value);
|
|
161
|
+
void (*flush)(const clap_plugin_t* plugin, const clap_input_events_t* in, const clap_output_events_t* out);
|
|
162
|
+
} clap_plugin_params_t;
|
|
163
|
+
|
package/native/host.c
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* host.c — CLAP plugin host implementation
|
|
3
|
+
*/
|
|
4
|
+
#include "clap.h"
|
|
5
|
+
#include "host.h"
|
|
6
|
+
#include <string.h>
|
|
7
|
+
#include <stdlib.h>
|
|
8
|
+
#include <stdio.h>
|
|
9
|
+
|
|
10
|
+
#ifdef _WIN32
|
|
11
|
+
#define WIN32_LEAN_AND_MEAN
|
|
12
|
+
#include <windows.h>
|
|
13
|
+
static void* lib_open(const char* p) { return (void*)LoadLibraryA(p); }
|
|
14
|
+
static void* lib_sym(void* l, const char* n) { return (void*)GetProcAddress((HMODULE)l, n); }
|
|
15
|
+
static void lib_close(void* l) { FreeLibrary((HMODULE)l); }
|
|
16
|
+
static const char* lib_error(void) { static char b[128]; snprintf(b, sizeof(b), "error %lu", GetLastError()); return b; }
|
|
17
|
+
#else
|
|
18
|
+
#include <dlfcn.h>
|
|
19
|
+
#include <dirent.h>
|
|
20
|
+
static void* lib_open(const char* p) { return dlopen(p, RTLD_NOW); }
|
|
21
|
+
static void* lib_sym(void* l, const char* n) { return dlsym(l, n); }
|
|
22
|
+
static void lib_close(void* l) { dlclose(l); }
|
|
23
|
+
static const char* lib_error(void) { return dlerror(); }
|
|
24
|
+
#endif
|
|
25
|
+
|
|
26
|
+
static _Thread_local char s_error[512] = {0};
|
|
27
|
+
|
|
28
|
+
/* --- Empty events --- */
|
|
29
|
+
|
|
30
|
+
static uint32_t empty_size(const clap_input_events_t* l) { (void)l; return 0; }
|
|
31
|
+
static const clap_event_header_t* empty_get(const clap_input_events_t* l, uint32_t i) { (void)l; (void)i; return NULL; }
|
|
32
|
+
static bool empty_push(const clap_output_events_t* l, const clap_event_header_t* e) { (void)l; (void)e; return true; }
|
|
33
|
+
|
|
34
|
+
static const clap_input_events_t s_in_events = { NULL, empty_size, empty_get };
|
|
35
|
+
static const clap_output_events_t s_out_events = { NULL, empty_push };
|
|
36
|
+
|
|
37
|
+
/* --- Host callbacks --- */
|
|
38
|
+
|
|
39
|
+
static const void* host_get_ext(const clap_host_t* h, const char* id) { (void)h; (void)id; return NULL; }
|
|
40
|
+
static void host_noop(const clap_host_t* h) { (void)h; }
|
|
41
|
+
|
|
42
|
+
static const clap_host_t s_host = {
|
|
43
|
+
CLAP_VERSION_INIT,
|
|
44
|
+
NULL,
|
|
45
|
+
"audio-host", "audiojs", "https://github.com/audiojs/host", "0.1.0",
|
|
46
|
+
host_get_ext, host_noop, host_noop, host_noop
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/* --- Plugin handle --- */
|
|
50
|
+
|
|
51
|
+
struct clap_host_plugin {
|
|
52
|
+
void* lib;
|
|
53
|
+
const clap_plugin_entry_t* entry;
|
|
54
|
+
const clap_plugin_t* plugin;
|
|
55
|
+
const clap_plugin_params_t* params;
|
|
56
|
+
char name[256];
|
|
57
|
+
char vendor[256];
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/* Find binary inside .clap bundle (macOS) or use path directly */
|
|
61
|
+
static const char* find_clap_binary(const char* path, char* out, size_t outSize) {
|
|
62
|
+
#ifndef _WIN32
|
|
63
|
+
char macosDir[2048];
|
|
64
|
+
snprintf(macosDir, sizeof(macosDir), "%s/Contents/MacOS", path);
|
|
65
|
+
DIR* dir = opendir(macosDir);
|
|
66
|
+
if (dir) {
|
|
67
|
+
struct dirent* entry;
|
|
68
|
+
while ((entry = readdir(dir))) {
|
|
69
|
+
if (entry->d_name[0] == '.') continue;
|
|
70
|
+
snprintf(out, outSize, "%s/%s", macosDir, entry->d_name);
|
|
71
|
+
closedir(dir);
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
closedir(dir);
|
|
75
|
+
}
|
|
76
|
+
#endif
|
|
77
|
+
strncpy(out, path, outSize - 1);
|
|
78
|
+
out[outSize - 1] = 0;
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* --- C API --- */
|
|
83
|
+
|
|
84
|
+
const char* clap_host_get_error(void) { return s_error; }
|
|
85
|
+
|
|
86
|
+
clap_host_plugin_t* clap_host_open(const char* path, double sampleRate, int channels, int blockSize) {
|
|
87
|
+
s_error[0] = 0;
|
|
88
|
+
|
|
89
|
+
char binPath[2048];
|
|
90
|
+
find_clap_binary(path, binPath, sizeof(binPath));
|
|
91
|
+
|
|
92
|
+
void* lib = lib_open(binPath);
|
|
93
|
+
if (!lib) { snprintf(s_error, sizeof(s_error), "dlopen: %s", lib_error()); return NULL; }
|
|
94
|
+
|
|
95
|
+
const clap_plugin_entry_t* entry = (const clap_plugin_entry_t*)lib_sym(lib, "clap_entry");
|
|
96
|
+
if (!entry) { strcpy(s_error, "No clap_entry symbol"); lib_close(lib); return NULL; }
|
|
97
|
+
|
|
98
|
+
if (!entry->init(path)) { strcpy(s_error, "clap_entry.init failed"); lib_close(lib); return NULL; }
|
|
99
|
+
|
|
100
|
+
const clap_plugin_factory_t* factory = (const clap_plugin_factory_t*)entry->get_factory(CLAP_PLUGIN_FACTORY_ID);
|
|
101
|
+
if (!factory || factory->get_plugin_count(factory) == 0) {
|
|
102
|
+
strcpy(s_error, "No plugins in factory");
|
|
103
|
+
entry->deinit(); lib_close(lib); return NULL;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const clap_plugin_descriptor_t* desc = factory->get_plugin_descriptor(factory, 0);
|
|
107
|
+
const clap_plugin_t* plugin = factory->create_plugin(factory, &s_host, desc->id);
|
|
108
|
+
if (!plugin) { strcpy(s_error, "create_plugin failed"); entry->deinit(); lib_close(lib); return NULL; }
|
|
109
|
+
|
|
110
|
+
if (!plugin->init(plugin)) { plugin->destroy(plugin); entry->deinit(); lib_close(lib); strcpy(s_error, "plugin.init failed"); return NULL; }
|
|
111
|
+
if (!plugin->activate(plugin, sampleRate, 1, blockSize)) { plugin->destroy(plugin); entry->deinit(); lib_close(lib); strcpy(s_error, "plugin.activate failed"); return NULL; }
|
|
112
|
+
plugin->start_processing(plugin);
|
|
113
|
+
|
|
114
|
+
clap_host_plugin_t* p = (clap_host_plugin_t*)calloc(1, sizeof(clap_host_plugin_t));
|
|
115
|
+
p->lib = lib; p->entry = entry; p->plugin = plugin;
|
|
116
|
+
p->params = (const clap_plugin_params_t*)plugin->get_extension(plugin, CLAP_EXT_PARAMS);
|
|
117
|
+
if (desc->name) strncpy(p->name, desc->name, 255);
|
|
118
|
+
if (desc->vendor) strncpy(p->vendor, desc->vendor, 255);
|
|
119
|
+
|
|
120
|
+
return p;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
void clap_host_close(clap_host_plugin_t* h) {
|
|
124
|
+
if (!h || !h->lib) return; /* already closed */
|
|
125
|
+
if (h->plugin) {
|
|
126
|
+
h->plugin->stop_processing(h->plugin);
|
|
127
|
+
h->plugin->deactivate(h->plugin);
|
|
128
|
+
h->plugin->destroy(h->plugin);
|
|
129
|
+
h->plugin = NULL;
|
|
130
|
+
}
|
|
131
|
+
if (h->entry) { h->entry->deinit(); h->entry = NULL; }
|
|
132
|
+
lib_close(h->lib);
|
|
133
|
+
h->lib = NULL;
|
|
134
|
+
h->params = NULL;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
void clap_host_destroy(clap_host_plugin_t* h) {
|
|
138
|
+
if (!h) return;
|
|
139
|
+
clap_host_close(h);
|
|
140
|
+
free(h);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
void clap_host_process(clap_host_plugin_t* h, float** inputs, float** outputs, int numChannels, int numSamples) {
|
|
144
|
+
if (!h || !h->plugin) return;
|
|
145
|
+
|
|
146
|
+
clap_audio_buffer_t in_buf = { inputs, NULL, (uint32_t)numChannels, 0, 0 };
|
|
147
|
+
clap_audio_buffer_t out_buf = { outputs, NULL, (uint32_t)numChannels, 0, 0 };
|
|
148
|
+
|
|
149
|
+
clap_process_t proc = {
|
|
150
|
+
-1, (uint32_t)numSamples,
|
|
151
|
+
&s_in_events, &s_out_events,
|
|
152
|
+
inputs ? &in_buf : NULL, &out_buf,
|
|
153
|
+
inputs ? 1u : 0u, 1u,
|
|
154
|
+
NULL
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
h->plugin->process(h->plugin, &proc);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const char* clap_host_get_name(clap_host_plugin_t* h) { return h ? h->name : ""; }
|
|
161
|
+
const char* clap_host_get_vendor(clap_host_plugin_t* h) { return h ? h->vendor : ""; }
|
|
162
|
+
|
|
163
|
+
int clap_host_get_param_count(clap_host_plugin_t* h) {
|
|
164
|
+
return (h && h->params) ? (int)h->params->count(h->plugin) : 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
int clap_host_get_param_info(clap_host_plugin_t* h, int index, clap_host_param_info_t* out) {
|
|
168
|
+
if (!h || !h->params) return -1;
|
|
169
|
+
clap_param_info_t info;
|
|
170
|
+
if (!h->params->get_info(h->plugin, index, &info)) return -1;
|
|
171
|
+
out->id = info.id;
|
|
172
|
+
strncpy(out->name, info.name, 255); out->name[255] = 0;
|
|
173
|
+
out->min = info.min_value;
|
|
174
|
+
out->max = info.max_value;
|
|
175
|
+
out->defaultValue = info.default_value;
|
|
176
|
+
return 0;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
double clap_host_get_param(clap_host_plugin_t* h, uint32_t id) {
|
|
180
|
+
if (!h || !h->params) return 0;
|
|
181
|
+
double val = 0;
|
|
182
|
+
h->params->get_value(h->plugin, id, &val);
|
|
183
|
+
return val;
|
|
184
|
+
}
|
package/native/host.h
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* host.h — C API for the CLAP host
|
|
3
|
+
*/
|
|
4
|
+
#pragma once
|
|
5
|
+
#include <stdint.h>
|
|
6
|
+
|
|
7
|
+
typedef struct clap_host_plugin clap_host_plugin_t;
|
|
8
|
+
|
|
9
|
+
typedef struct {
|
|
10
|
+
uint32_t id;
|
|
11
|
+
char name[256];
|
|
12
|
+
double min;
|
|
13
|
+
double max;
|
|
14
|
+
double defaultValue;
|
|
15
|
+
} clap_host_param_info_t;
|
|
16
|
+
|
|
17
|
+
clap_host_plugin_t* clap_host_open(const char* path, double sampleRate, int channels, int blockSize);
|
|
18
|
+
void clap_host_close(clap_host_plugin_t* handle); /* idempotent deactivate */
|
|
19
|
+
void clap_host_destroy(clap_host_plugin_t* handle); /* close + free */
|
|
20
|
+
void clap_host_process(clap_host_plugin_t* handle, float** inputs, float** outputs, int numChannels, int numSamples);
|
|
21
|
+
|
|
22
|
+
const char* clap_host_get_name(clap_host_plugin_t* handle);
|
|
23
|
+
const char* clap_host_get_vendor(clap_host_plugin_t* handle);
|
|
24
|
+
|
|
25
|
+
int clap_host_get_param_count(clap_host_plugin_t* handle);
|
|
26
|
+
int clap_host_get_param_info(clap_host_plugin_t* handle, int index, clap_host_param_info_t* info);
|
|
27
|
+
double clap_host_get_param(clap_host_plugin_t* handle, uint32_t id);
|
|
28
|
+
|
|
29
|
+
const char* clap_host_get_error(void);
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@audio/host-clap",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLAP plugin host for audiojs",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.js",
|
|
7
|
+
"types": "./index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./index.d.ts",
|
|
11
|
+
"default": "./index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"gypfile": true,
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "node-gyp rebuild",
|
|
17
|
+
"test": "node test.js"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"index.js",
|
|
21
|
+
"index.d.ts",
|
|
22
|
+
"src/",
|
|
23
|
+
"native/",
|
|
24
|
+
"binding.gyp"
|
|
25
|
+
],
|
|
26
|
+
"optionalDependencies": {
|
|
27
|
+
"@audio/host-clap-darwin-arm64": ">=0.1.0",
|
|
28
|
+
"@audio/host-clap-darwin-x64": ">=0.1.0",
|
|
29
|
+
"@audio/host-clap-linux-x64": ">=0.1.0",
|
|
30
|
+
"@audio/host-clap-linux-arm64": ">=0.1.0",
|
|
31
|
+
"@audio/host-clap-win32-x64": ">=0.1.0"
|
|
32
|
+
},
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"tst": "^9.4.0"
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
}
|
|
40
|
+
}
|
package/src/addon.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NAPI addon loader — tries prebuilt, then local build
|
|
3
|
+
*/
|
|
4
|
+
import { createRequire } from 'node:module'
|
|
5
|
+
import { join, dirname } from 'node:path'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
import { arch, platform } from 'node:os'
|
|
8
|
+
|
|
9
|
+
const require = createRequire(import.meta.url)
|
|
10
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
11
|
+
const plat = `${platform()}-${arch()}`
|
|
12
|
+
|
|
13
|
+
let addon
|
|
14
|
+
const loaders = [
|
|
15
|
+
() => require(`@audio/host-clap-${plat}`),
|
|
16
|
+
() => require(join(root, 'packages', `host-clap-${plat}`, 'host_clap.node')),
|
|
17
|
+
() => require(join(root, 'prebuilds', plat, 'host_clap.node')),
|
|
18
|
+
() => require(join(root, 'build', 'Release', 'host_clap.node')),
|
|
19
|
+
() => require(join(root, 'build', 'Debug', 'host_clap.node')),
|
|
20
|
+
]
|
|
21
|
+
for (const load of loaders) {
|
|
22
|
+
try { addon = load(); break } catch {}
|
|
23
|
+
}
|
|
24
|
+
if (!addon) throw new Error('host-clap addon not found — run `npm run build` or install @audio/host-clap-' + plat)
|
|
25
|
+
|
|
26
|
+
export default addon
|