├── .gitignore ├── .gitattributes ├── examples ├── root.zig └── Gain.zig ├── README.md ├── LICENSE └── src └── clap.zig /.gitignore: -------------------------------------------------------------------------------- 1 | /zig-out 2 | .zig-cache 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | *.zig text=auto eol=lf 3 | *.zon text=auto eol=lf -------------------------------------------------------------------------------- /examples/root.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const clap = @import("clap"); 3 | const assert = std.debug.assert; 4 | 5 | export const clap_entry: clap.Entry = .forFactories(.{ 6 | &clap.PluginFactory.forPlugins(std.heap.c_allocator, &.{ 7 | @import("Gain.zig"), 8 | }), 9 | }); 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CLever Audio Plugin (CLAP) in Zig 2 | 3 | WIP. 4 | 5 | ## Installing Examples 6 | 7 | ```bash 8 | # Windows CMD 9 | zig build --prefix-exe-dir %LOCALAPPDATA%\Programs\Common\CLAP 10 | 11 | # Windows PowerShell 12 | zig build --prefix-exe-dir $env:LOCALAPPDATA\Programs\Common\CLAP 13 | 14 | # Linux 15 | zig build --prefix-exe-dir ~/.clap 16 | 17 | # MacOS 18 | zig build --prefix-exe-dir ~/Library/Audio/Plug-Ins/CLAP 19 | ``` 20 | 21 | ## License 22 | 23 | [MIT](https://github.com/SuperAuguste/zig-clap/blob/main/LICENSE). 24 | 25 | Upstream is also [MIT licensed](https://github.com/free-audio/clap/blob/main/LICENSE). 26 | -------------------------------------------------------------------------------- /examples/Gain.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const clap = @import("clap"); 3 | 4 | const Gain = @This(); 5 | 6 | pub const descriptor: clap.Plugin.Descriptor = .{ 7 | .clap_version = .current, 8 | .id = "zig-clap.examples.gain", 9 | .name = "Gain", 10 | .vendor = "Zig Clap Examples", 11 | .url = "https://github.com/SuperAuguste/zig-clap", 12 | .manual_url = "https://github.com/SuperAuguste/zig-clap", 13 | .support_url = "https://github.com/SuperAuguste/zig-clap/issues", 14 | .version = "0.0.1", 15 | .description = "A simple gain plugin", 16 | .features = null, 17 | }; 18 | 19 | pub const create = clap.Plugin.createFor(@This()); 20 | 21 | pub fn init(gain: *Gain, allocator: std.mem.Allocator) error{}!void { 22 | _ = gain; // autofix 23 | _ = allocator; // autofix 24 | } 25 | 26 | pub fn deinit(gain: *Gain, allocator: std.mem.Allocator) void { 27 | _ = gain; // autofix 28 | _ = allocator; // autofix 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Auguste Rame 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/clap.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const assert = std.debug.assert; 3 | 4 | pub const ClapVersion = extern struct { 5 | pub const current: ClapVersion = .{ .major = 1, .minor = 2, .revision = 6 }; 6 | 7 | major: u32, 8 | minor: u32, 9 | revision: u32, 10 | }; 11 | 12 | pub const Entry = extern struct { 13 | clap_version: ClapVersion, 14 | /// Must be defended with a mutex or counter if complex behavior inside. 15 | initFn: *const fn (plugin_path: [*:0]const u8) callconv(.c) bool, 16 | /// Must be defended with a mutex or counter if complex behavior inside. 17 | deinitFn: *const fn () callconv(.c) void, 18 | /// Must be thread-safe. 19 | getFactoryFn: *const fn (factory: [*:0]const u8) callconv(.c) ?*const anyopaque, 20 | 21 | pub fn forFactories(factories: anytype) Entry { 22 | const Instance = struct { 23 | fn init(plugin_path: [*:0]const u8) callconv(.c) bool { 24 | _ = plugin_path; 25 | return true; 26 | } 27 | 28 | fn deinit() callconv(.c) void {} 29 | 30 | fn getFactory(factory_id_ptr: [*:0]const u8) callconv(.c) ?*const anyopaque { 31 | const factory_id = std.mem.span(factory_id_ptr); 32 | 33 | inline for (factories) |factory_ptr| { 34 | if (!std.mem.eql(u8, factory_id, @TypeOf(factory_ptr.*).id)) { 35 | return factory_ptr; 36 | } 37 | } 38 | 39 | return null; 40 | } 41 | }; 42 | 43 | return .{ 44 | .clap_version = .current, 45 | .initFn = &Instance.init, 46 | .deinitFn = &Instance.deinit, 47 | .getFactoryFn = &Instance.getFactory, 48 | }; 49 | } 50 | }; 51 | 52 | pub const PluginFactory = extern struct { 53 | pub const id = "clap.plugin-factory"; 54 | /// Must be thread-safe. 55 | getPluginCountFn: *const fn (plugin_factory: *const PluginFactory) callconv(.c) u32, 56 | /// Must be thread-safe. 57 | getPluginDescriptorFn: *const fn (plugin_factory: *const PluginFactory, index: u32) callconv(.c) ?*const Plugin.Descriptor, 58 | /// Must be thread-safe. 59 | createPluginFn: *const fn (plugin_factory: *const PluginFactory, host: *const Host, plugin_id: [*:0]const u8) callconv(.c) ?*const Plugin, 60 | 61 | /// PluginFactory given a slice of types with: 62 | /// - A `descriptor: PluginDescriptor` decl 63 | /// - An `fn create(allocator: std.mem.Allocator) error{OutOfMemory}!*const Plugin` decl 64 | pub fn forPlugins( 65 | allocator: std.mem.Allocator, 66 | comptime plugins: []const type, 67 | ) PluginFactory { 68 | const Factory = struct { 69 | fn getPluginCount(plugin_factory: *const PluginFactory) callconv(.c) u32 { 70 | _ = plugin_factory; 71 | return plugins.len; 72 | } 73 | 74 | fn getPluginDescriptor(plugin_factory: *const PluginFactory, index: u32) callconv(.c) ?*const Plugin.Descriptor { 75 | _ = plugin_factory; 76 | inline for (plugins, 0..) |PluginType, i| { 77 | if (i == index) { 78 | return &PluginType.descriptor; 79 | } 80 | } 81 | return null; 82 | } 83 | 84 | fn createPlugin( 85 | plugin_factory: *const PluginFactory, 86 | host: *const Host, 87 | plugin_id_raw: [*:0]const u8, 88 | ) callconv(.c) ?*const Plugin { 89 | _ = plugin_factory; 90 | _ = host; 91 | 92 | const plugin_id = std.mem.span(plugin_id_raw); 93 | 94 | inline for (plugins) |PluginType| { 95 | if (std.mem.eql(u8, comptime std.mem.span(PluginType.descriptor.id), plugin_id)) { 96 | return PluginType.create(allocator) catch null; 97 | } 98 | } 99 | 100 | return null; 101 | } 102 | }; 103 | 104 | return .{ 105 | .getPluginCountFn = Factory.getPluginCount, 106 | .getPluginDescriptorFn = Factory.getPluginDescriptor, 107 | .createPluginFn = Factory.createPlugin, 108 | }; 109 | } 110 | }; 111 | 112 | pub const Host = extern struct { 113 | clap_version: ClapVersion, 114 | 115 | host_data: ?*anyopaque, 116 | name: [*:0]const u8, 117 | vendor: ?[*:0]const u8, 118 | url: ?[*:0]const u8, 119 | version: [*:0]const u8, 120 | 121 | /// Must be thread-safe. 122 | getExtensionFn: *const fn (host: *const Host, extension_id: [*:0]const u8) callconv(.c) ?*const anyopaque, 123 | /// Must be thread-safe. 124 | requestRestartFn: *const fn (host: *const Host) callconv(.c) void, 125 | /// Must be thread-safe. 126 | requestProcessFn: *const fn (host: *const Host) callconv(.c) void, 127 | /// Must be thread-safe. 128 | requestCallbackFn: *const fn (host: *const Host) callconv(.c) void, 129 | 130 | pub fn getExtension(host: *const Host, extension_id: [:0]const u8) ?*const anyopaque { 131 | return host.getExtensionFn(host, extension_id); 132 | } 133 | }; 134 | 135 | pub const Plugin = extern struct { 136 | descriptor: *const Descriptor, 137 | 138 | plugin_data: ?*anyopaque, 139 | 140 | /// Called on main thread. 141 | initFn: *const fn (plugin: *const Plugin) callconv(.c) bool, 142 | /// Called on main thread. Must be deactivated. 143 | destroyFn: *const fn (plugin: *const Plugin) callconv(.c) void, 144 | /// Called on main thread. Must be deactivated. 145 | activateFn: *const fn (plugin: *const Plugin, sample_rate: f64, min_frames_count: u32, max_frames_count: u32) callconv(.c) bool, 146 | /// Called on main thread. Must be activated. 147 | deactivateFn: *const fn (plugin: *const Plugin) callconv(.c) void, 148 | /// Called on audio thread. Must be activated and not processing. 149 | startProcesingFn: *const fn (plugin: *const Plugin) callconv(.c) bool, 150 | /// Called on audio thread. Must be activated and processing. 151 | stopProcesingFn: *const fn (plugin: *const Plugin) callconv(.c) void, 152 | /// Called on audio thread. Must be activated. 153 | resetFn: *const fn (plugin: *const Plugin) callconv(.c) void, 154 | /// Called on audio thread. Must be activated and processing. 155 | processFn: *const fn (plugin: *const Plugin, process: *const Process) callconv(.c) Process.Status, 156 | /// Must be thread-safe. 157 | getExtensionFn: *const fn (plugin: *const Plugin, extension_id: [*:0]const u8) callconv(.c) ?*const anyopaque, 158 | /// Called on main thread after host.request_callback(). 159 | onMainThreadFn: *const fn (plugin: *const Plugin) callconv(.c) void, 160 | 161 | pub const Descriptor = extern struct { 162 | clap_version: ClapVersion, 163 | 164 | id: [*:0]const u8, 165 | name: [*:0]const u8, 166 | vendor: ?[*:0]const u8, 167 | url: ?[*:0]const u8, 168 | manual_url: ?[*:0]const u8, 169 | support_url: ?[*:0]const u8, 170 | version: ?[*:0]const u8, 171 | description: ?[*:0]const u8, 172 | 173 | features: ?[*:null]?[*:0]const u8, 174 | }; 175 | 176 | /// `T` must have a `descriptor: PluginDescriptor` decl 177 | /// 178 | /// The following functions are required on `T`: 179 | /// - `init(plugin: *T, allocator: std.mem.Allocator) error{...}!void` 180 | /// - `deinit(plugin: *T, allocator: std.mem.Allocator) void` (destroy is implemented by `createFor`; you only need to deinit your plugin instance) 181 | /// 182 | /// The following functions are optional on `T`: 183 | /// - `activate(plugin: *T, allocator: std.mem.Allocator, sample_rate: f64, min_frames_count: u32, max_frames_count: u32) error{...}!void` 184 | /// - `deactivate(plugin: *T, allocator: std.mem.Allocator) void` 185 | /// - `startProcesing(plugin: *T) error{...}!void` 186 | /// - `stopProcesing(plugin: *T) void` 187 | /// - `reset(plugin: *T)` 188 | /// - `process(plugin: *T, process: *const Process) Process.Status` 189 | /// - `getExtension(plugin: *T, extension_id: [:0]const u8) ?*const anyopaque` 190 | /// - `onMainThread(plugin: *T, allocator: std.mem.Allocator) void` 191 | pub fn createFor( 192 | comptime T: type, 193 | ) fn (allocator: std.mem.Allocator) error{OutOfMemory}!*const Plugin { 194 | return struct { 195 | const PluginData = struct { 196 | allocator: std.mem.Allocator, 197 | is_initialized: bool, 198 | user_plugin: T, 199 | }; 200 | 201 | fn create(allocator: std.mem.Allocator) error{OutOfMemory}!*const Plugin { 202 | const plugin = try allocator.create(Plugin); 203 | errdefer allocator.destroy(plugin); 204 | 205 | const plugin_data = try allocator.create(PluginData); 206 | errdefer allocator.destroy(plugin_data); 207 | 208 | plugin_data.* = .{ 209 | .allocator = allocator, 210 | .is_initialized = false, 211 | .user_plugin = undefined, 212 | }; 213 | 214 | plugin.* = .{ 215 | .descriptor = &T.descriptor, 216 | .plugin_data = plugin_data, 217 | .initFn = init, 218 | .destroyFn = destroy, 219 | .activateFn = activate, 220 | .deactivateFn = deactivate, 221 | .startProcesingFn = startProcesing, 222 | .stopProcesingFn = stopProcesing, 223 | .resetFn = reset, 224 | .processFn = process, 225 | .getExtensionFn = getExtension, 226 | .onMainThreadFn = onMainThread, 227 | }; 228 | 229 | return plugin; 230 | } 231 | 232 | fn init(plugin: *const Plugin) callconv(.c) bool { 233 | const plugin_data: *PluginData = @alignCast(@ptrCast(plugin.plugin_data)); 234 | 235 | if (plugin_data.is_initialized) { 236 | return true; 237 | } 238 | 239 | T.init(&plugin_data.user_plugin, plugin_data.allocator) catch { 240 | return false; 241 | }; 242 | plugin_data.is_initialized = true; 243 | return true; 244 | } 245 | 246 | fn destroy(plugin: *const Plugin) callconv(.c) void { 247 | const plugin_data: *PluginData = @alignCast(@ptrCast(plugin.plugin_data)); 248 | 249 | if (plugin_data.is_initialized) { 250 | T.deinit(&plugin_data.user_plugin, plugin_data.allocator); 251 | } 252 | 253 | plugin_data.allocator.destroy(plugin_data); 254 | plugin_data.allocator.destroy(plugin); 255 | } 256 | 257 | fn activate(plugin: *const Plugin, sample_rate: f64, min_frames_count: u32, max_frames_count: u32) callconv(.c) bool { 258 | if (!@hasDecl(T, "activate")) { 259 | return false; 260 | } 261 | 262 | // const plugin_data: *PluginData = @alignCast(@ptrCast(plugin.plugin_data)); 263 | 264 | _ = plugin; 265 | _ = sample_rate; 266 | _ = min_frames_count; 267 | _ = max_frames_count; 268 | return true; 269 | } 270 | 271 | fn deactivate(plugin: *const Plugin) callconv(.c) void { 272 | if (!@hasDecl(T, "deactivate")) { 273 | return; 274 | } 275 | 276 | _ = plugin; 277 | } 278 | 279 | fn startProcesing(plugin: *const Plugin) callconv(.c) bool { 280 | if (!@hasDecl(T, "startProcesing")) { 281 | return false; 282 | } 283 | 284 | _ = plugin; 285 | return true; 286 | } 287 | 288 | fn stopProcesing(plugin: *const Plugin) callconv(.c) void { 289 | if (!@hasDecl(T, "stopProcesing")) { 290 | return; 291 | } 292 | 293 | _ = plugin; 294 | } 295 | 296 | fn reset(plugin: *const Plugin) callconv(.c) void { 297 | _ = plugin; // autofix 298 | if (!@hasDecl(T, "reset")) { 299 | return; 300 | } 301 | } 302 | 303 | fn process(plugin: *const Plugin, proc: *const Process) callconv(.c) Process.Status { 304 | if (!@hasDecl(T, "process")) { 305 | return .sleep; 306 | } 307 | _ = plugin; 308 | _ = proc; 309 | } 310 | 311 | fn getExtension(plugin: *const Plugin, extension_id: [*:0]const u8) callconv(.c) ?*const anyopaque { 312 | if (!@hasDecl(T, "getExtension")) { 313 | return null; 314 | } 315 | _ = plugin; 316 | _ = extension_id; 317 | return null; 318 | } 319 | 320 | fn onMainThread(plugin: *const Plugin) callconv(.c) void { 321 | if (!@hasDecl(T, "onMainThread")) { 322 | return; 323 | } 324 | 325 | _ = plugin; 326 | } 327 | }.create; 328 | } 329 | }; 330 | 331 | pub const Process = extern struct { 332 | steady_time: i64, 333 | frames_count: u32, 334 | event_transport: ?*Event.Transport, 335 | audio_inputs: [*]const AudioBuffer, 336 | audio_outputs: [*]AudioBuffer, 337 | audio_inputs_count: u32, 338 | audio_ouputs_count: u32, 339 | /// Sorted in sample order. 340 | input_events: *const Event.InputList, 341 | /// Must be sorted in sample order. 342 | output_events: *const Event.OutputList, 343 | 344 | pub const Status = enum(i32) { 345 | @"error" = 0, 346 | @"continue" = 1, 347 | continue_if_not_quiet = 2, 348 | tail = 3, 349 | sleep = 4, 350 | _, 351 | }; 352 | }; 353 | 354 | pub const AudioBuffer = extern struct { 355 | data_32: [*][*]f32, 356 | data_64: [*][*]f64, 357 | channel_count: u32, 358 | latency: u32, 359 | constant_mask: u64, 360 | }; 361 | 362 | pub const BeatTime = enum(i64) { _ }; 363 | pub const SecTime = enum(i64) { _ }; 364 | pub const Id = enum(u32) { invalid = std.math.maxInt(u32), _ }; 365 | 366 | pub const Event = struct { 367 | /// Sorted in sample order. Owned by host. 368 | pub const InputList = extern struct { 369 | ctx: *anyopaque, 370 | getLenFn: *const fn (input_events: *const InputList) callconv(.c) u32, 371 | atFn: *const fn (input_events: *const InputList, index: u32) callconv(.c) ?*const Event, 372 | 373 | pub fn len(input_events: *const InputList) u32 { 374 | return input_events.getLenFn(); 375 | } 376 | 377 | pub fn at(input_events: *const InputList, index: u32) *const Event { 378 | assert(index < input_events.getSize()); 379 | return input_events.atFn(index).?; 380 | } 381 | }; 382 | 383 | /// Sorted in sample order. Owned by host. 384 | pub const OutputList = extern struct { 385 | ctx: *anyopaque, 386 | tryPushFn: *const fn (output_events: *const OutputList, event: *const Event) callconv(.c) bool, 387 | 388 | pub fn push(output_events: *const OutputList, event: *const Event) error{OutOfMemory}!void { 389 | if (!output_events.tryPushFn(output_events, event)) { 390 | return error.OutOfMemory; 391 | } 392 | } 393 | }; 394 | 395 | pub const Type = enum(u16) { 396 | note_on = 0, 397 | note_off = 1, 398 | note_choke = 2, 399 | note_end = 3, 400 | note_expression = 4, 401 | param_value = 5, 402 | param_mod = 6, 403 | param_gesture_begin = 7, 404 | param_gesture_end = 8, 405 | transport = 9, 406 | midi = 10, 407 | midi_sysex = 11, 408 | midi2 = 12, 409 | _, 410 | }; 411 | 412 | pub const Header = extern struct { 413 | /// Size including header. 414 | size: u32, 415 | time: u32, 416 | space_id: SpaceId, 417 | type: Type, 418 | flags: Flags, 419 | 420 | pub const SpaceId = enum(u16) { 421 | core = 0, 422 | _, 423 | }; 424 | pub const Flags = packed struct(u32) { 425 | is_live: bool, 426 | dont_record: bool, 427 | padding: u30, 428 | }; 429 | }; 430 | 431 | pub const Note = extern struct { 432 | header: Header, 433 | 434 | note_id: i32, 435 | port_index: i16, 436 | channel: i16, 437 | key: i16, 438 | velocity: f64, 439 | }; 440 | 441 | pub const NoteExpression = extern struct { 442 | header: Header, 443 | 444 | expression_id: NoteExpression.Id, 445 | 446 | note_id: i32, 447 | port_index: i16, 448 | channel: i16, 449 | key: i16, 450 | 451 | value: f64, 452 | 453 | pub const Id = enum(i32) { 454 | volume = 0, 455 | pan = 1, 456 | tuning = 2, 457 | vibrato = 3, 458 | expression = 4, 459 | brightness = 5, 460 | pressure = 6, 461 | _, 462 | }; 463 | }; 464 | 465 | pub const ParamValue = extern struct { 466 | header: Header, 467 | 468 | param_id: Id, 469 | cookie: *anyopaque, 470 | 471 | note_id: i32, 472 | port_index: i16, 473 | channel: i16, 474 | key: i16, 475 | 476 | value: f64, 477 | }; 478 | 479 | pub const ParamMod = extern struct { 480 | header: Header, 481 | 482 | param_id: Id, 483 | cookie: *anyopaque, 484 | 485 | note_id: i32, 486 | port_index: i16, 487 | channel: i16, 488 | key: i16, 489 | 490 | amount: f64, 491 | }; 492 | 493 | pub const ParamGesture = extern struct { 494 | header: Header, 495 | 496 | param_id: Id, 497 | }; 498 | 499 | pub const Transport = extern struct { 500 | header: Header, 501 | 502 | flags: Flags, 503 | song_pos_beats: BeatTime, 504 | song_pos_seconds: SecTime, 505 | tempo: f64, 506 | tempo_inc: f64, 507 | loop_start_beats: BeatTime, 508 | loop_end_beats: BeatTime, 509 | loop_start_seconds: SecTime, 510 | loop_end_seconds: SecTime, 511 | bar_start: BeatTime, 512 | bar_number: i32, 513 | tsig_num: u16, 514 | tsig_denom: u16, 515 | 516 | pub const Flags = packed struct(u32) { 517 | has_tempo: bool, 518 | has_beats_timeline: bool, 519 | has_seconds_timeline: bool, 520 | has_time_signature: bool, 521 | is_playing: bool, 522 | is_recording: bool, 523 | is_loop_active: bool, 524 | is_within_pre_roll: bool, 525 | padding: u24, 526 | }; 527 | }; 528 | 529 | pub const Midi = extern struct { 530 | header: Header, 531 | 532 | port_index: u16, 533 | data: [3]u8, 534 | }; 535 | 536 | pub const MidiSysex = extern struct { 537 | header: Header, 538 | 539 | port_index: u16, 540 | buffer_ptr: [*]const u8, 541 | buffer_len: u32, 542 | }; 543 | 544 | pub const Midi2 = extern struct { 545 | header: Header, 546 | 547 | port_index: u16, 548 | data: [4]u32, 549 | }; 550 | }; 551 | --------------------------------------------------------------------------------