native_audio 0.4.0 → 0.5.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1f5b017594ea0c5b0b94f2e3c36c057ccdd55275c133f5ffc77ef9dfca5a4b7c
4
- data.tar.gz: a42b15d0bdf8fa4bfd6f673d3b0f638409653d061e640eb559f433bad2ae6f18
3
+ metadata.gz: a9a953a3fe6e4bd23e99278c28dbd273bc08268105382a4dae34854e3917aa64
4
+ data.tar.gz: af2a5c84a91fa2957984d4c36637bbe914f38fab7d26903406051eb2456dbc96
5
5
  SHA512:
6
- metadata.gz: cb6eb51f84784da896c21258f32393014845d5dcf47109e0ddc83f972c43e3353431c0addb42a8b9c14fed5397f9a8aaf1ce372549e7a321961a715afa577ceb
7
- data.tar.gz: 82fe331ea3a1adb7a6281afe2978c62f7b9fc37c9836ed7ededda965034237af0a7acd54ca313973bb12ed1b22127d504981c70391ce0377ccf51898da99399a
6
+ metadata.gz: '094beb2919f1a5e0b9e18ea7ac5e5776f5f5ed5eb213f9aac5c976ec3e74cb740ba73632f3bab2acb589afc4d2f12a1fe8485b3ddd59b5bc71d8d21fa758f357'
7
+ data.tar.gz: b0c335b765410df6cfbae2a730f921388e5761639f96b329113ef398527dd46f9067680a55e0fb7541cf801784158f07b744f07e9570251ea0485503b573c81f
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # What is native_audio?
2
2
 
3
- Native audio is a thin wrapper around miniaudio for simple audio playback from Ruby.
3
+ Native audio is a thin wrapper around miniaudio for simple audio playback from Ruby, with built-in support for delay and reverb effects.
4
4
 
5
5
  ## Installation
6
6
 
@@ -25,26 +25,91 @@ clip = NativeAudio::Clip.new('path/to/sound.wav')
25
25
  clip.duration # => seconds
26
26
 
27
27
  # Create an audio source to manage playback
28
- audio_source = NativeAudio::AudioSource.new(clip)
28
+ source = NativeAudio::AudioSource.new(clip)
29
29
 
30
30
  # Play the clip
31
- audio_source.play
31
+ source.play
32
32
 
33
33
  # Pause and resume
34
- audio_source.pause
35
- audio_source.resume
34
+ source.pause
35
+ source.resume
36
36
 
37
37
  # Set pitch (1.0 = normal, 0.5 = octave down, 2.0 = octave up)
38
- audio_source.set_pitch(1.5)
38
+ source.set_pitch(1.5)
39
39
 
40
40
  # Set position relative to listener (angle: 0-360, distance: 0-255)
41
- audio_source.set_pos(90, 200) # right side, mid distance
41
+ source.set_pos(90, 200) # right side, mid distance
42
42
 
43
43
  # Set volume (0-128)
44
- audio_source.set_volume(64)
44
+ source.set_volume(64)
45
+
46
+ # Loop playback
47
+ source.set_looping(true)
45
48
 
46
49
  # Stop the clip
47
- audio_source.stop
50
+ source.stop
51
+ ```
52
+
53
+ ## Effects
54
+
55
+ Each audio source has a built-in effects chain:
56
+
57
+ ```
58
+ sound ──▶ delay ──▶ reverb ──▶ output
59
+ ```
60
+
61
+ ### Delay Taps
62
+
63
+ Add discrete echo effects with up to 16 taps per source:
64
+
65
+ ```ruby
66
+ source.play
67
+
68
+ # Add delay taps (returns a DelayTap object)
69
+ tap1 = source.add_delay_tap(time_ms: 200, volume: 0.5)
70
+ tap2 = source.add_delay_tap(time_ms: 400, volume: 0.3)
71
+
72
+ # Modify taps at runtime
73
+ tap1.volume = 0.4
74
+ tap1.time_ms = 250
75
+
76
+ # Remove a tap
77
+ tap2.remove
78
+
79
+ # Query active taps
80
+ source.delay_taps # => [tap1]
81
+ ```
82
+
83
+ ### Reverb
84
+
85
+ Add room ambience with a Schroeder reverb:
86
+
87
+ ```ruby
88
+ source.play
89
+
90
+ # Enable reverb with custom settings
91
+ source.set_reverb(
92
+ room_size: 0.7, # 0.0 = small room, 1.0 = large hall
93
+ damping: 0.5, # 0.0 = bright, 1.0 = muffled
94
+ wet: 0.4, # reverb signal level
95
+ dry: 1.0 # original signal level
96
+ )
97
+
98
+ # Or just enable with defaults
99
+ source.enable_reverb
100
+ source.enable_reverb(false) # disable
101
+ ```
102
+
103
+ ### Combining Effects
104
+
105
+ Delay and reverb work together - each echo gets reverb applied:
106
+
107
+ ```ruby
108
+ source.play
109
+
110
+ # Slapback echo with room reverb
111
+ source.add_delay_tap(time_ms: 150, volume: 0.4)
112
+ source.set_reverb(room_size: 0.5, wet: 0.3, dry: 1.0)
48
113
  ```
49
114
 
50
115
  ## Environment Variables
data/ext/audio/audio.c CHANGED
@@ -1,5 +1,5 @@
1
1
  // ============================================================================
2
- // native_audio - Ruby audio library using miniaudio
2
+ // audio.c - Main entry point and Ruby bindings for native_audio
3
3
  // ============================================================================
4
4
 
5
5
  #include <ruby.h>
@@ -9,22 +9,22 @@
9
9
 
10
10
  #define MINIAUDIO_IMPLEMENTATION
11
11
  #include "miniaudio.h"
12
+ #include "audio.h"
12
13
 
13
14
  // ============================================================================
14
- // Constants & Globals
15
+ // Global Definitions
15
16
  // ============================================================================
16
17
 
17
- #define MAX_SOUNDS 1024
18
- #define MAX_CHANNELS 1024
19
-
20
- static ma_engine engine;
21
- static ma_context context;
22
- static ma_sound *sounds[MAX_SOUNDS]; // Loaded audio clips
23
- static ma_sound *channels[MAX_CHANNELS]; // Playback instances
24
- static int sound_count = 0;
25
- static int engine_initialized = 0;
26
- static int context_initialized = 0;
27
- static int using_null_backend = 0;
18
+ ma_engine engine;
19
+ ma_context context;
20
+ ma_sound *sounds[MAX_SOUNDS];
21
+ ma_sound *channels[MAX_CHANNELS];
22
+ multi_tap_delay_node *delay_nodes[MAX_CHANNELS];
23
+ reverb_node *reverb_nodes[MAX_CHANNELS];
24
+ int sound_count = 0;
25
+ int engine_initialized = 0;
26
+ int context_initialized = 0;
27
+ int using_null_backend = 0;
28
28
 
29
29
  // ============================================================================
30
30
  // Cleanup (called on Ruby exit)
@@ -38,7 +38,6 @@ static void cleanup_audio(VALUE unused)
38
38
  return;
39
39
  }
40
40
 
41
- // Stop and clean up all channels
42
41
  for (int i = 0; i < MAX_CHANNELS; i++) {
43
42
  if (channels[i] != NULL) {
44
43
  ma_sound_stop(channels[i]);
@@ -46,9 +45,20 @@ static void cleanup_audio(VALUE unused)
46
45
  free(channels[i]);
47
46
  channels[i] = NULL;
48
47
  }
48
+
49
+ if (delay_nodes[i] != NULL) {
50
+ multi_tap_delay_uninit(delay_nodes[i]);
51
+ free(delay_nodes[i]);
52
+ delay_nodes[i] = NULL;
53
+ }
54
+
55
+ if (reverb_nodes[i] != NULL) {
56
+ reverb_uninit(reverb_nodes[i]);
57
+ free(reverb_nodes[i]);
58
+ reverb_nodes[i] = NULL;
59
+ }
49
60
  }
50
61
 
51
- // Stop and clean up all loaded sounds
52
62
  for (int i = 0; i < sound_count; i++) {
53
63
  if (sounds[i] != NULL) {
54
64
  ma_sound_stop(sounds[i]);
@@ -58,8 +68,6 @@ static void cleanup_audio(VALUE unused)
58
68
  }
59
69
  }
60
70
 
61
- // On Windows, ma_engine_uninit crashes with the null backend
62
- // Since null backend has no real resources, we can skip cleanup
63
71
  #ifdef _WIN32
64
72
  if (using_null_backend) {
65
73
  engine_initialized = 0;
@@ -77,11 +85,55 @@ static void cleanup_audio(VALUE unused)
77
85
  }
78
86
  }
79
87
 
88
+ // ============================================================================
89
+ // Engine Initialization
90
+ // ============================================================================
91
+
92
+ VALUE audio_init(VALUE self)
93
+ {
94
+ if (engine_initialized) {
95
+ return Qnil;
96
+ }
97
+
98
+ const char *driver = getenv("NATIVE_AUDIO_DRIVER");
99
+ int use_null = (driver != NULL && strcmp(driver, "null") == 0);
100
+
101
+ ma_engine_config config = ma_engine_config_init();
102
+ config.listenerCount = 1;
103
+
104
+ if (use_null) {
105
+ ma_backend backends[] = { ma_backend_null };
106
+ ma_result ctx_result = ma_context_init(backends, 1, NULL, &context);
107
+ if (ctx_result != MA_SUCCESS) {
108
+ rb_raise(rb_eRuntimeError, "Failed to initialize null audio context");
109
+ return Qnil;
110
+ }
111
+ context_initialized = 1;
112
+ using_null_backend = 1;
113
+ config.pContext = &context;
114
+ }
115
+
116
+ ma_result result = ma_engine_init(&config, &engine);
117
+
118
+ if (result != MA_SUCCESS) {
119
+ if (context_initialized) {
120
+ ma_context_uninit(&context);
121
+ context_initialized = 0;
122
+ }
123
+ rb_raise(rb_eRuntimeError, "Failed to initialize audio engine");
124
+ return Qnil;
125
+ }
126
+
127
+ engine_initialized = 1;
128
+ rb_set_end_proc(cleanup_audio, Qnil);
129
+
130
+ return Qnil;
131
+ }
132
+
80
133
  // ============================================================================
81
134
  // Audio Loading
82
135
  // ============================================================================
83
136
 
84
- // Audio.load(path) - Load an audio file, returns clip ID
85
137
  VALUE audio_load(VALUE self, VALUE file)
86
138
  {
87
139
  const char *path = StringValueCStr(file);
@@ -106,7 +158,6 @@ VALUE audio_load(VALUE self, VALUE file)
106
158
  return rb_int2inum(id);
107
159
  }
108
160
 
109
- // Audio.duration(clip) - Get duration of clip in seconds
110
161
  VALUE audio_duration(VALUE self, VALUE clip)
111
162
  {
112
163
  int clip_id = NUM2INT(clip);
@@ -129,7 +180,6 @@ VALUE audio_duration(VALUE self, VALUE clip)
129
180
  // Playback Controls
130
181
  // ============================================================================
131
182
 
132
- // Audio.play(channel, clip) - Play a clip on a channel
133
183
  VALUE audio_play(VALUE self, VALUE channel_id, VALUE clip)
134
184
  {
135
185
  int channel = NUM2INT(channel_id);
@@ -145,7 +195,7 @@ VALUE audio_play(VALUE self, VALUE channel_id, VALUE clip)
145
195
  return Qnil;
146
196
  }
147
197
 
148
- // Clean up existing sound on this channel
198
+ // Clean up existing resources on this channel
149
199
  if (channels[channel] != NULL) {
150
200
  ma_sound_stop(channels[channel]);
151
201
  ma_sound_uninit(channels[channel]);
@@ -153,27 +203,89 @@ VALUE audio_play(VALUE self, VALUE channel_id, VALUE clip)
153
203
  channels[channel] = NULL;
154
204
  }
155
205
 
156
- // Create a copy of the sound for playback
206
+ if (delay_nodes[channel] != NULL) {
207
+ multi_tap_delay_uninit(delay_nodes[channel]);
208
+ free(delay_nodes[channel]);
209
+ delay_nodes[channel] = NULL;
210
+ }
211
+
212
+ if (reverb_nodes[channel] != NULL) {
213
+ reverb_uninit(reverb_nodes[channel]);
214
+ free(reverb_nodes[channel]);
215
+ reverb_nodes[channel] = NULL;
216
+ }
217
+
218
+ // Create sound copy for playback
157
219
  ma_sound *playback = (ma_sound *)malloc(sizeof(ma_sound));
158
220
  if (playback == NULL) {
159
221
  rb_raise(rb_eRuntimeError, "Failed to allocate memory for playback");
160
222
  return Qnil;
161
223
  }
162
224
 
163
- ma_result result = ma_sound_init_copy(&engine, sounds[clip_id], 0, NULL, playback);
225
+ ma_result result = ma_sound_init_copy(&engine, sounds[clip_id], MA_SOUND_FLAG_NO_DEFAULT_ATTACHMENT, NULL, playback);
164
226
  if (result != MA_SUCCESS) {
165
227
  free(playback);
166
228
  rb_raise(rb_eRuntimeError, "Failed to create sound copy for playback");
167
229
  return Qnil;
168
230
  }
169
231
 
232
+ // Create delay node
233
+ ma_uint32 sampleRate = ma_engine_get_sample_rate(&engine);
234
+ ma_uint32 numChannels = ma_engine_get_channels(&engine);
235
+
236
+ multi_tap_delay_node *delayNode = (multi_tap_delay_node *)malloc(sizeof(multi_tap_delay_node));
237
+ if (delayNode == NULL) {
238
+ ma_sound_uninit(playback);
239
+ free(playback);
240
+ rb_raise(rb_eRuntimeError, "Failed to allocate memory for delay node");
241
+ return Qnil;
242
+ }
243
+
244
+ result = multi_tap_delay_init(delayNode, ma_engine_get_node_graph(&engine), sampleRate, numChannels);
245
+ if (result != MA_SUCCESS) {
246
+ free(delayNode);
247
+ ma_sound_uninit(playback);
248
+ free(playback);
249
+ rb_raise(rb_eRuntimeError, "Failed to initialize delay node");
250
+ return Qnil;
251
+ }
252
+
253
+ // Create reverb node
254
+ reverb_node *reverbNode = (reverb_node *)malloc(sizeof(reverb_node));
255
+ if (reverbNode == NULL) {
256
+ multi_tap_delay_uninit(delayNode);
257
+ free(delayNode);
258
+ ma_sound_uninit(playback);
259
+ free(playback);
260
+ rb_raise(rb_eRuntimeError, "Failed to allocate memory for reverb node");
261
+ return Qnil;
262
+ }
263
+
264
+ result = reverb_init(reverbNode, ma_engine_get_node_graph(&engine), sampleRate, numChannels);
265
+ if (result != MA_SUCCESS) {
266
+ free(reverbNode);
267
+ multi_tap_delay_uninit(delayNode);
268
+ free(delayNode);
269
+ ma_sound_uninit(playback);
270
+ free(playback);
271
+ rb_raise(rb_eRuntimeError, "Failed to initialize reverb node");
272
+ return Qnil;
273
+ }
274
+
275
+ // Route: sound -> delay_node -> reverb_node -> endpoint
276
+ ma_node *endpoint = ma_engine_get_endpoint(&engine);
277
+ ma_node_attach_output_bus(&reverbNode->base, 0, endpoint, 0);
278
+ ma_node_attach_output_bus(&delayNode->base, 0, &reverbNode->base, 0);
279
+ ma_node_attach_output_bus((ma_node *)playback, 0, &delayNode->base, 0);
280
+
281
+ delay_nodes[channel] = delayNode;
282
+ reverb_nodes[channel] = reverbNode;
170
283
  channels[channel] = playback;
171
284
  ma_sound_start(playback);
172
285
 
173
286
  return rb_int2inum(channel);
174
287
  }
175
288
 
176
- // Audio.stop(channel) - Stop playback and rewind
177
289
  VALUE audio_stop(VALUE self, VALUE channel_id)
178
290
  {
179
291
  int channel = NUM2INT(channel_id);
@@ -188,7 +300,6 @@ VALUE audio_stop(VALUE self, VALUE channel_id)
188
300
  return Qnil;
189
301
  }
190
302
 
191
- // Audio.pause(channel) - Pause playback
192
303
  VALUE audio_pause(VALUE self, VALUE channel_id)
193
304
  {
194
305
  int channel = NUM2INT(channel_id);
@@ -202,7 +313,6 @@ VALUE audio_pause(VALUE self, VALUE channel_id)
202
313
  return Qnil;
203
314
  }
204
315
 
205
- // Audio.resume(channel) - Resume playback
206
316
  VALUE audio_resume(VALUE self, VALUE channel_id)
207
317
  {
208
318
  int channel = NUM2INT(channel_id);
@@ -217,10 +327,9 @@ VALUE audio_resume(VALUE self, VALUE channel_id)
217
327
  }
218
328
 
219
329
  // ============================================================================
220
- // Audio Effects
330
+ // Sound Effects
221
331
  // ============================================================================
222
332
 
223
- // Audio.set_volume(channel, volume) - Set volume (0-128)
224
333
  VALUE audio_set_volume(VALUE self, VALUE channel_id, VALUE volume)
225
334
  {
226
335
  int channel = NUM2INT(channel_id);
@@ -230,13 +339,11 @@ VALUE audio_set_volume(VALUE self, VALUE channel_id, VALUE volume)
230
339
  return Qnil;
231
340
  }
232
341
 
233
- float normalized_volume = vol / 128.0f;
234
- ma_sound_set_volume(channels[channel], normalized_volume);
342
+ ma_sound_set_volume(channels[channel], vol / 128.0f);
235
343
 
236
344
  return Qnil;
237
345
  }
238
346
 
239
- // Audio.set_pitch(channel, pitch) - Set pitch (1.0 = normal)
240
347
  VALUE audio_set_pitch(VALUE self, VALUE channel_id, VALUE pitch)
241
348
  {
242
349
  int channel = NUM2INT(channel_id);
@@ -251,9 +358,6 @@ VALUE audio_set_pitch(VALUE self, VALUE channel_id, VALUE pitch)
251
358
  return Qnil;
252
359
  }
253
360
 
254
- // Audio.set_pos(channel, angle, distance) - Set 3D position
255
- // angle: 0=front, 90=right, 180=back, 270=left
256
- // distance: 0=close, 255=far
257
361
  VALUE audio_set_pos(VALUE self, VALUE channel_id, VALUE angle, VALUE distance)
258
362
  {
259
363
  int channel = NUM2INT(channel_id);
@@ -264,7 +368,6 @@ VALUE audio_set_pos(VALUE self, VALUE channel_id, VALUE angle, VALUE distance)
264
368
  return Qnil;
265
369
  }
266
370
 
267
- // Convert polar to cartesian
268
371
  float rad = ang * (MA_PI / 180.0f);
269
372
  float normalized_dist = dist / 255.0f;
270
373
  float x = normalized_dist * sinf(rad);
@@ -275,54 +378,157 @@ VALUE audio_set_pos(VALUE self, VALUE channel_id, VALUE angle, VALUE distance)
275
378
  return Qnil;
276
379
  }
277
380
 
381
+ VALUE audio_set_looping(VALUE self, VALUE channel_id, VALUE looping)
382
+ {
383
+ int channel = NUM2INT(channel_id);
384
+ ma_bool32 loop = RTEST(looping) ? MA_TRUE : MA_FALSE;
385
+
386
+ if (channel < 0 || channel >= MAX_CHANNELS || channels[channel] == NULL) {
387
+ return Qnil;
388
+ }
389
+
390
+ ma_sound_set_looping(channels[channel], loop);
391
+
392
+ return Qnil;
393
+ }
394
+
278
395
  // ============================================================================
279
- // Engine Initialization
396
+ // Delay Tap Controls
280
397
  // ============================================================================
281
398
 
282
- // Audio.init - Initialize the audio engine
283
- VALUE audio_init(VALUE self)
399
+ VALUE audio_add_delay_tap(VALUE self, VALUE channel_id, VALUE time_ms, VALUE volume)
284
400
  {
285
- if (engine_initialized) {
401
+ int channel = NUM2INT(channel_id);
402
+ float ms = (float)NUM2DBL(time_ms);
403
+ float vol = (float)NUM2DBL(volume);
404
+
405
+ if (channel < 0 || channel >= MAX_CHANNELS || delay_nodes[channel] == NULL) {
406
+ rb_raise(rb_eArgError, "Invalid channel or no delay node: %d", channel);
286
407
  return Qnil;
287
408
  }
288
409
 
289
- // Check for null driver (for CI environments without audio devices)
290
- // Usage: NATIVE_AUDIO_DRIVER=null ruby script.rb
291
- const char *driver = getenv("NATIVE_AUDIO_DRIVER");
292
- int use_null = (driver != NULL && strcmp(driver, "null") == 0);
410
+ int tap_id = multi_tap_delay_add_tap(delay_nodes[channel], ms, vol);
411
+ if (tap_id < 0) {
412
+ rb_raise(rb_eRuntimeError, "Failed to add delay tap (max taps reached)");
413
+ return Qnil;
414
+ }
293
415
 
294
- ma_engine_config config = ma_engine_config_init();
295
- config.listenerCount = 1;
416
+ return rb_int2inum(tap_id);
417
+ }
296
418
 
297
- if (use_null) {
298
- ma_backend backends[] = { ma_backend_null };
299
- ma_result ctx_result = ma_context_init(backends, 1, NULL, &context);
300
- if (ctx_result != MA_SUCCESS) {
301
- rb_raise(rb_eRuntimeError, "Failed to initialize null audio context");
302
- return Qnil;
303
- }
304
- context_initialized = 1;
305
- using_null_backend = 1;
306
- config.pContext = &context;
419
+ VALUE audio_remove_delay_tap(VALUE self, VALUE channel_id, VALUE tap_id)
420
+ {
421
+ int channel = NUM2INT(channel_id);
422
+ int tap = NUM2INT(tap_id);
423
+
424
+ if (channel < 0 || channel >= MAX_CHANNELS || delay_nodes[channel] == NULL) {
425
+ return Qnil;
307
426
  }
308
427
 
309
- ma_result result = ma_engine_init(&config, &engine);
428
+ multi_tap_delay_remove_tap(delay_nodes[channel], tap);
310
429
 
311
- if (result != MA_SUCCESS) {
312
- if (context_initialized) {
313
- ma_context_uninit(&context);
314
- context_initialized = 0;
315
- }
316
- rb_raise(rb_eRuntimeError, "Failed to initialize audio engine");
430
+ return Qnil;
431
+ }
432
+
433
+ VALUE audio_set_delay_tap_volume(VALUE self, VALUE channel_id, VALUE tap_id, VALUE volume)
434
+ {
435
+ int channel = NUM2INT(channel_id);
436
+ int tap = NUM2INT(tap_id);
437
+ float vol = (float)NUM2DBL(volume);
438
+
439
+ if (channel < 0 || channel >= MAX_CHANNELS || delay_nodes[channel] == NULL) {
317
440
  return Qnil;
318
441
  }
319
442
 
320
- engine_initialized = 1;
321
- rb_set_end_proc(cleanup_audio, Qnil);
443
+ multi_tap_delay_set_volume(delay_nodes[channel], tap, vol);
322
444
 
323
445
  return Qnil;
324
446
  }
325
447
 
448
+ VALUE audio_set_delay_tap_time(VALUE self, VALUE channel_id, VALUE tap_id, VALUE time_ms)
449
+ {
450
+ int channel = NUM2INT(channel_id);
451
+ int tap = NUM2INT(tap_id);
452
+ float ms = (float)NUM2DBL(time_ms);
453
+
454
+ if (channel < 0 || channel >= MAX_CHANNELS || delay_nodes[channel] == NULL) {
455
+ return Qnil;
456
+ }
457
+
458
+ multi_tap_delay_set_time(delay_nodes[channel], tap, ms);
459
+
460
+ return Qnil;
461
+ }
462
+
463
+ // ============================================================================
464
+ // Reverb Controls
465
+ // ============================================================================
466
+
467
+ VALUE audio_enable_reverb(VALUE self, VALUE channel_id, VALUE enabled)
468
+ {
469
+ int channel = NUM2INT(channel_id);
470
+ ma_bool32 en = RTEST(enabled) ? MA_TRUE : MA_FALSE;
471
+
472
+ if (channel < 0 || channel >= MAX_CHANNELS || reverb_nodes[channel] == NULL) {
473
+ return Qnil;
474
+ }
475
+
476
+ reverb_set_enabled(reverb_nodes[channel], en);
477
+ return Qnil;
478
+ }
479
+
480
+ VALUE audio_set_reverb_room_size(VALUE self, VALUE channel_id, VALUE size)
481
+ {
482
+ int channel = NUM2INT(channel_id);
483
+ float s = (float)NUM2DBL(size);
484
+
485
+ if (channel < 0 || channel >= MAX_CHANNELS || reverb_nodes[channel] == NULL) {
486
+ return Qnil;
487
+ }
488
+
489
+ reverb_set_room_size(reverb_nodes[channel], s);
490
+ return Qnil;
491
+ }
492
+
493
+ VALUE audio_set_reverb_damping(VALUE self, VALUE channel_id, VALUE damp)
494
+ {
495
+ int channel = NUM2INT(channel_id);
496
+ float d = (float)NUM2DBL(damp);
497
+
498
+ if (channel < 0 || channel >= MAX_CHANNELS || reverb_nodes[channel] == NULL) {
499
+ return Qnil;
500
+ }
501
+
502
+ reverb_set_damping(reverb_nodes[channel], d);
503
+ return Qnil;
504
+ }
505
+
506
+ VALUE audio_set_reverb_wet(VALUE self, VALUE channel_id, VALUE wet)
507
+ {
508
+ int channel = NUM2INT(channel_id);
509
+ float w = (float)NUM2DBL(wet);
510
+
511
+ if (channel < 0 || channel >= MAX_CHANNELS || reverb_nodes[channel] == NULL) {
512
+ return Qnil;
513
+ }
514
+
515
+ reverb_set_wet(reverb_nodes[channel], w);
516
+ return Qnil;
517
+ }
518
+
519
+ VALUE audio_set_reverb_dry(VALUE self, VALUE channel_id, VALUE dry)
520
+ {
521
+ int channel = NUM2INT(channel_id);
522
+ float d = (float)NUM2DBL(dry);
523
+
524
+ if (channel < 0 || channel >= MAX_CHANNELS || reverb_nodes[channel] == NULL) {
525
+ return Qnil;
526
+ }
527
+
528
+ reverb_set_dry(reverb_nodes[channel], d);
529
+ return Qnil;
530
+ }
531
+
326
532
  // ============================================================================
327
533
  // Ruby Module Setup
328
534
  // ============================================================================
@@ -330,7 +536,11 @@ VALUE audio_init(VALUE self)
330
536
  void Init_audio(void)
331
537
  {
332
538
  for (int i = 0; i < MAX_SOUNDS; i++) sounds[i] = NULL;
333
- for (int i = 0; i < MAX_CHANNELS; i++) channels[i] = NULL;
539
+ for (int i = 0; i < MAX_CHANNELS; i++) {
540
+ channels[i] = NULL;
541
+ delay_nodes[i] = NULL;
542
+ reverb_nodes[i] = NULL;
543
+ }
334
544
 
335
545
  VALUE mAudio = rb_define_module("Audio");
336
546
 
@@ -351,4 +561,18 @@ void Init_audio(void)
351
561
  rb_define_singleton_method(mAudio, "set_volume", audio_set_volume, 2);
352
562
  rb_define_singleton_method(mAudio, "set_pitch", audio_set_pitch, 2);
353
563
  rb_define_singleton_method(mAudio, "set_pos", audio_set_pos, 3);
564
+ rb_define_singleton_method(mAudio, "set_looping", audio_set_looping, 2);
565
+
566
+ // Delay taps
567
+ rb_define_singleton_method(mAudio, "add_delay_tap", audio_add_delay_tap, 3);
568
+ rb_define_singleton_method(mAudio, "remove_delay_tap", audio_remove_delay_tap, 2);
569
+ rb_define_singleton_method(mAudio, "set_delay_tap_volume", audio_set_delay_tap_volume, 3);
570
+ rb_define_singleton_method(mAudio, "set_delay_tap_time", audio_set_delay_tap_time, 3);
571
+
572
+ // Reverb
573
+ rb_define_singleton_method(mAudio, "enable_reverb", audio_enable_reverb, 2);
574
+ rb_define_singleton_method(mAudio, "set_reverb_room_size", audio_set_reverb_room_size, 2);
575
+ rb_define_singleton_method(mAudio, "set_reverb_damping", audio_set_reverb_damping, 2);
576
+ rb_define_singleton_method(mAudio, "set_reverb_wet", audio_set_reverb_wet, 2);
577
+ rb_define_singleton_method(mAudio, "set_reverb_dry", audio_set_reverb_dry, 2);
354
578
  }
data/ext/audio/audio.h ADDED
@@ -0,0 +1,34 @@
1
+ // ============================================================================
2
+ // audio.h - Shared types, constants, and globals for native_audio
3
+ // ============================================================================
4
+
5
+ #ifndef NATIVE_AUDIO_H
6
+ #define NATIVE_AUDIO_H
7
+
8
+ #include "miniaudio.h"
9
+ #include "delay_node.h"
10
+ #include "reverb_node.h"
11
+
12
+ // ============================================================================
13
+ // Constants
14
+ // ============================================================================
15
+
16
+ #define MAX_SOUNDS 1024
17
+ #define MAX_CHANNELS 1024
18
+
19
+ // ============================================================================
20
+ // Globals (defined in audio.c)
21
+ // ============================================================================
22
+
23
+ extern ma_engine engine;
24
+ extern ma_context context;
25
+ extern ma_sound *sounds[MAX_SOUNDS];
26
+ extern ma_sound *channels[MAX_CHANNELS];
27
+ extern multi_tap_delay_node *delay_nodes[MAX_CHANNELS];
28
+ extern reverb_node *reverb_nodes[MAX_CHANNELS];
29
+ extern int sound_count;
30
+ extern int engine_initialized;
31
+ extern int context_initialized;
32
+ extern int using_null_backend;
33
+
34
+ #endif // NATIVE_AUDIO_H
@@ -0,0 +1,185 @@
1
+ // ============================================================================
2
+ // delay_node.c - Multi-tap delay node implementation
3
+ // ============================================================================
4
+
5
+ #include <stdlib.h>
6
+ #include <string.h>
7
+ #include "delay_node.h"
8
+
9
+ // ============================================================================
10
+ // DSP Callback
11
+ // ============================================================================
12
+
13
+ static void multi_tap_delay_process(ma_node *pNode, const float **ppFramesIn,
14
+ ma_uint32 *pFrameCountIn, float **ppFramesOut,
15
+ ma_uint32 *pFrameCountOut)
16
+ {
17
+ multi_tap_delay_node *node = (multi_tap_delay_node *)pNode;
18
+ const float *pFramesIn = ppFramesIn[0];
19
+ float *pFramesOut = ppFramesOut[0];
20
+ ma_uint32 frameCount = *pFrameCountOut;
21
+ ma_uint32 numChannels = node->channels;
22
+
23
+ for (ma_uint32 iFrame = 0; iFrame < frameCount; iFrame++) {
24
+ for (ma_uint32 iChannel = 0; iChannel < numChannels; iChannel++) {
25
+ ma_uint32 sampleIndex = iFrame * numChannels + iChannel;
26
+ float inputSample = pFramesIn[sampleIndex];
27
+
28
+ // Write to circular buffer
29
+ ma_uint32 writeIndex = (node->write_pos * numChannels) + iChannel;
30
+ node->buffer[writeIndex] = inputSample;
31
+
32
+ // Start with dry signal
33
+ float outputSample = inputSample;
34
+
35
+ // Mix in all active taps
36
+ for (ma_uint32 iTap = 0; iTap < MAX_TAPS_PER_CHANNEL; iTap++) {
37
+ if (node->taps[iTap].active) {
38
+ ma_uint32 delayFrames = node->taps[iTap].delay_frames;
39
+ if (delayFrames > 0 && delayFrames <= node->buffer_size) {
40
+ ma_uint32 readPos = (node->write_pos + node->buffer_size - delayFrames) % node->buffer_size;
41
+ ma_uint32 readIndex = (readPos * numChannels) + iChannel;
42
+ outputSample += node->buffer[readIndex] * node->taps[iTap].volume;
43
+ }
44
+ }
45
+ }
46
+
47
+ pFramesOut[sampleIndex] = outputSample;
48
+ }
49
+
50
+ // Advance write position
51
+ node->write_pos = (node->write_pos + 1) % node->buffer_size;
52
+ }
53
+ }
54
+
55
+ static ma_node_vtable g_multi_tap_delay_vtable = {
56
+ multi_tap_delay_process,
57
+ NULL, // onGetRequiredInputFrameCount
58
+ 1, // inputBusCount
59
+ 1, // outputBusCount
60
+ 0 // flags
61
+ };
62
+
63
+ // ============================================================================
64
+ // Lifecycle
65
+ // ============================================================================
66
+
67
+ ma_result multi_tap_delay_init(multi_tap_delay_node *pNode, ma_node_graph *pNodeGraph,
68
+ ma_uint32 sampleRate, ma_uint32 numChannels)
69
+ {
70
+ if (pNode == NULL) {
71
+ return MA_INVALID_ARGS;
72
+ }
73
+
74
+ memset(pNode, 0, sizeof(*pNode));
75
+
76
+ pNode->sample_rate = sampleRate;
77
+ pNode->channels = numChannels;
78
+ pNode->buffer_size = (ma_uint32)(sampleRate * MAX_DELAY_SECONDS);
79
+ pNode->write_pos = 0;
80
+ pNode->tap_count = 0;
81
+
82
+ // Allocate circular buffer (frames * channels)
83
+ pNode->buffer = (float *)calloc(pNode->buffer_size * numChannels, sizeof(float));
84
+ if (pNode->buffer == NULL) {
85
+ return MA_OUT_OF_MEMORY;
86
+ }
87
+
88
+ // Initialize all taps as inactive
89
+ for (int i = 0; i < MAX_TAPS_PER_CHANNEL; i++) {
90
+ pNode->taps[i].active = MA_FALSE;
91
+ pNode->taps[i].delay_frames = 0;
92
+ pNode->taps[i].volume = 0.0f;
93
+ }
94
+
95
+ // Set up node configuration
96
+ ma_uint32 channelsArray[1] = { numChannels };
97
+ ma_node_config nodeConfig = ma_node_config_init();
98
+ nodeConfig.vtable = &g_multi_tap_delay_vtable;
99
+ nodeConfig.pInputChannels = channelsArray;
100
+ nodeConfig.pOutputChannels = channelsArray;
101
+
102
+ return ma_node_init(pNodeGraph, &nodeConfig, NULL, &pNode->base);
103
+ }
104
+
105
+ void multi_tap_delay_uninit(multi_tap_delay_node *pNode)
106
+ {
107
+ if (pNode == NULL) {
108
+ return;
109
+ }
110
+
111
+ ma_node_uninit(&pNode->base, NULL);
112
+
113
+ if (pNode->buffer != NULL) {
114
+ free(pNode->buffer);
115
+ pNode->buffer = NULL;
116
+ }
117
+ }
118
+
119
+ // ============================================================================
120
+ // Tap Management
121
+ // ============================================================================
122
+
123
+ int multi_tap_delay_add_tap(multi_tap_delay_node *pNode, float time_ms, float volume)
124
+ {
125
+ if (pNode == NULL) {
126
+ return -1;
127
+ }
128
+
129
+ // Find first inactive tap slot
130
+ for (int i = 0; i < MAX_TAPS_PER_CHANNEL; i++) {
131
+ if (!pNode->taps[i].active) {
132
+ ma_uint32 delayFrames = (ma_uint32)((time_ms / 1000.0f) * pNode->sample_rate);
133
+ if (delayFrames > pNode->buffer_size) {
134
+ delayFrames = pNode->buffer_size;
135
+ }
136
+ pNode->taps[i].delay_frames = delayFrames;
137
+ pNode->taps[i].volume = volume;
138
+ pNode->taps[i].active = MA_TRUE;
139
+ pNode->tap_count++;
140
+ return i;
141
+ }
142
+ }
143
+
144
+ return -1; // No slots available
145
+ }
146
+
147
+ void multi_tap_delay_remove_tap(multi_tap_delay_node *pNode, int tap_id)
148
+ {
149
+ if (pNode == NULL || tap_id < 0 || tap_id >= MAX_TAPS_PER_CHANNEL) {
150
+ return;
151
+ }
152
+
153
+ if (pNode->taps[tap_id].active) {
154
+ pNode->taps[tap_id].active = MA_FALSE;
155
+ pNode->taps[tap_id].delay_frames = 0;
156
+ pNode->taps[tap_id].volume = 0.0f;
157
+ pNode->tap_count--;
158
+ }
159
+ }
160
+
161
+ void multi_tap_delay_set_volume(multi_tap_delay_node *pNode, int tap_id, float volume)
162
+ {
163
+ if (pNode == NULL || tap_id < 0 || tap_id >= MAX_TAPS_PER_CHANNEL) {
164
+ return;
165
+ }
166
+
167
+ if (pNode->taps[tap_id].active) {
168
+ pNode->taps[tap_id].volume = volume;
169
+ }
170
+ }
171
+
172
+ void multi_tap_delay_set_time(multi_tap_delay_node *pNode, int tap_id, float time_ms)
173
+ {
174
+ if (pNode == NULL || tap_id < 0 || tap_id >= MAX_TAPS_PER_CHANNEL) {
175
+ return;
176
+ }
177
+
178
+ if (pNode->taps[tap_id].active) {
179
+ ma_uint32 delayFrames = (ma_uint32)((time_ms / 1000.0f) * pNode->sample_rate);
180
+ if (delayFrames > pNode->buffer_size) {
181
+ delayFrames = pNode->buffer_size;
182
+ }
183
+ pNode->taps[tap_id].delay_frames = delayFrames;
184
+ }
185
+ }
@@ -0,0 +1,51 @@
1
+ // ============================================================================
2
+ // delay_node.h - Multi-tap delay node for native_audio
3
+ // ============================================================================
4
+
5
+ #ifndef DELAY_NODE_H
6
+ #define DELAY_NODE_H
7
+
8
+ #include "miniaudio.h"
9
+
10
+ // ============================================================================
11
+ // Constants
12
+ // ============================================================================
13
+
14
+ #define MAX_TAPS_PER_CHANNEL 16
15
+ #define MAX_DELAY_SECONDS 2.0f
16
+
17
+ // ============================================================================
18
+ // Types
19
+ // ============================================================================
20
+
21
+ typedef struct {
22
+ ma_uint32 delay_frames;
23
+ float volume;
24
+ ma_bool32 active;
25
+ } delay_tap;
26
+
27
+ typedef struct {
28
+ ma_node_base base;
29
+ float *buffer;
30
+ ma_uint32 buffer_size; // Size in frames
31
+ ma_uint32 write_pos;
32
+ ma_uint32 channels; // Audio channels (stereo = 2)
33
+ delay_tap taps[MAX_TAPS_PER_CHANNEL];
34
+ ma_uint32 tap_count;
35
+ ma_uint32 sample_rate;
36
+ } multi_tap_delay_node;
37
+
38
+ // ============================================================================
39
+ // Public API
40
+ // ============================================================================
41
+
42
+ ma_result multi_tap_delay_init(multi_tap_delay_node *pNode, ma_node_graph *pNodeGraph,
43
+ ma_uint32 sampleRate, ma_uint32 numChannels);
44
+ void multi_tap_delay_uninit(multi_tap_delay_node *pNode);
45
+
46
+ int multi_tap_delay_add_tap(multi_tap_delay_node *pNode, float time_ms, float volume);
47
+ void multi_tap_delay_remove_tap(multi_tap_delay_node *pNode, int tap_id);
48
+ void multi_tap_delay_set_volume(multi_tap_delay_node *pNode, int tap_id, float volume);
49
+ void multi_tap_delay_set_time(multi_tap_delay_node *pNode, int tap_id, float time_ms);
50
+
51
+ #endif // DELAY_NODE_H
@@ -0,0 +1,224 @@
1
+ // ============================================================================
2
+ // reverb_node.c - Schroeder reverb node implementation
3
+ // ============================================================================
4
+
5
+ #include <stdlib.h>
6
+ #include <string.h>
7
+ #include "reverb_node.h"
8
+
9
+ // ============================================================================
10
+ // Delay Line Helpers
11
+ // ============================================================================
12
+
13
+ // Base delay times in seconds (Schroeder-style, prime-ish ratios)
14
+ static const float COMB_DELAYS[NUM_COMBS] = { 0.0297f, 0.0371f, 0.0411f, 0.0437f };
15
+ static const float ALLPASS_DELAYS[NUM_ALLPASSES] = { 0.005f, 0.0017f };
16
+
17
+ static void delay_line_init(delay_line *dl, ma_uint32 size)
18
+ {
19
+ dl->size = size;
20
+ dl->pos = 0;
21
+ dl->buffer = (float *)calloc(size, sizeof(float));
22
+ }
23
+
24
+ static void delay_line_free(delay_line *dl)
25
+ {
26
+ if (dl->buffer) {
27
+ free(dl->buffer);
28
+ dl->buffer = NULL;
29
+ }
30
+ }
31
+
32
+ static inline float delay_line_read(delay_line *dl)
33
+ {
34
+ return dl->buffer[dl->pos];
35
+ }
36
+
37
+ static inline void delay_line_write(delay_line *dl, float value)
38
+ {
39
+ dl->buffer[dl->pos] = value;
40
+ dl->pos = (dl->pos + 1) % dl->size;
41
+ }
42
+
43
+ // ============================================================================
44
+ // Filter Processing
45
+ // ============================================================================
46
+
47
+ // Comb filter: output = buffer[pos], then write input + feedback * output (with damping)
48
+ static inline float comb_process(delay_line *dl, float input, float feedback,
49
+ float damp, float *damp_prev)
50
+ {
51
+ float output = delay_line_read(dl);
52
+ // Low-pass filter on feedback for damping (high freq decay faster)
53
+ *damp_prev = output * (1.0f - damp) + (*damp_prev) * damp;
54
+ delay_line_write(dl, input + feedback * (*damp_prev));
55
+ return output;
56
+ }
57
+
58
+ // Allpass filter: output = -g*input + buffer[pos], write input + g*buffer[pos]
59
+ static inline float allpass_process(delay_line *dl, float input, float feedback)
60
+ {
61
+ float buffered = delay_line_read(dl);
62
+ float output = buffered - feedback * input;
63
+ delay_line_write(dl, input + feedback * buffered);
64
+ return output;
65
+ }
66
+
67
+ // ============================================================================
68
+ // DSP Callback
69
+ // ============================================================================
70
+
71
+ static void reverb_process(ma_node *pNode, const float **ppFramesIn,
72
+ ma_uint32 *pFrameCountIn, float **ppFramesOut,
73
+ ma_uint32 *pFrameCountOut)
74
+ {
75
+ reverb_node *node = (reverb_node *)pNode;
76
+ const float *pFramesIn = ppFramesIn[0];
77
+ float *pFramesOut = ppFramesOut[0];
78
+ ma_uint32 frameCount = *pFrameCountOut;
79
+ ma_uint32 numChannels = node->channels;
80
+
81
+ if (!node->enabled) {
82
+ // Bypass: copy input to output
83
+ memcpy(pFramesOut, pFramesIn, frameCount * numChannels * sizeof(float));
84
+ return;
85
+ }
86
+
87
+ for (ma_uint32 iFrame = 0; iFrame < frameCount; iFrame++) {
88
+ for (ma_uint32 iChannel = 0; iChannel < numChannels && iChannel < 2; iChannel++) {
89
+ ma_uint32 sampleIndex = iFrame * numChannels + iChannel;
90
+ float input = pFramesIn[sampleIndex];
91
+
92
+ // Sum of parallel comb filters
93
+ float combSum = 0.0f;
94
+ for (int c = 0; c < NUM_COMBS; c++) {
95
+ combSum += comb_process(&node->combs[iChannel][c], input,
96
+ node->comb_feedback, node->comb_damp,
97
+ &node->comb_damp_prev[iChannel][c]);
98
+ }
99
+ combSum *= 0.25f; // Average the 4 combs
100
+
101
+ // Series allpass filters
102
+ float allpassOut = combSum;
103
+ for (int a = 0; a < NUM_ALLPASSES; a++) {
104
+ allpassOut = allpass_process(&node->allpasses[iChannel][a],
105
+ allpassOut, node->allpass_feedback);
106
+ }
107
+
108
+ // Mix dry and wet
109
+ pFramesOut[sampleIndex] = input * node->dry + allpassOut * node->wet;
110
+ }
111
+
112
+ // Handle mono->stereo or more channels by copying
113
+ for (ma_uint32 iChannel = 2; iChannel < numChannels; iChannel++) {
114
+ ma_uint32 sampleIndex = iFrame * numChannels + iChannel;
115
+ pFramesOut[sampleIndex] = pFramesIn[sampleIndex];
116
+ }
117
+ }
118
+ }
119
+
120
+ static ma_node_vtable g_reverb_vtable = {
121
+ reverb_process,
122
+ NULL,
123
+ 1,
124
+ 1,
125
+ 0
126
+ };
127
+
128
+ // ============================================================================
129
+ // Lifecycle
130
+ // ============================================================================
131
+
132
+ ma_result reverb_init(reverb_node *pNode, ma_node_graph *pNodeGraph,
133
+ ma_uint32 sampleRate, ma_uint32 numChannels)
134
+ {
135
+ if (pNode == NULL) {
136
+ return MA_INVALID_ARGS;
137
+ }
138
+
139
+ memset(pNode, 0, sizeof(*pNode));
140
+ pNode->sample_rate = sampleRate;
141
+ pNode->channels = numChannels;
142
+ pNode->enabled = MA_FALSE;
143
+
144
+ // Default parameters
145
+ pNode->room_size = 0.5f;
146
+ pNode->comb_feedback = 0.7f;
147
+ pNode->comb_damp = 0.3f;
148
+ pNode->allpass_feedback = 0.5f;
149
+ pNode->wet = 0.3f;
150
+ pNode->dry = 1.0f;
151
+
152
+ // Initialize delay lines for up to 2 audio channels
153
+ ma_uint32 chans = numChannels < 2 ? numChannels : 2;
154
+ for (ma_uint32 ch = 0; ch < chans; ch++) {
155
+ for (int c = 0; c < NUM_COMBS; c++) {
156
+ ma_uint32 delaySize = (ma_uint32)(COMB_DELAYS[c] * pNode->room_size * 2.0f * sampleRate);
157
+ if (delaySize < 1) delaySize = 1;
158
+ delay_line_init(&pNode->combs[ch][c], delaySize);
159
+ }
160
+ for (int a = 0; a < NUM_ALLPASSES; a++) {
161
+ ma_uint32 delaySize = (ma_uint32)(ALLPASS_DELAYS[a] * sampleRate);
162
+ if (delaySize < 1) delaySize = 1;
163
+ delay_line_init(&pNode->allpasses[ch][a], delaySize);
164
+ }
165
+ }
166
+
167
+ // Set up node
168
+ ma_uint32 channelsArray[1] = { numChannels };
169
+ ma_node_config nodeConfig = ma_node_config_init();
170
+ nodeConfig.vtable = &g_reverb_vtable;
171
+ nodeConfig.pInputChannels = channelsArray;
172
+ nodeConfig.pOutputChannels = channelsArray;
173
+
174
+ return ma_node_init(pNodeGraph, &nodeConfig, NULL, &pNode->base);
175
+ }
176
+
177
+ void reverb_uninit(reverb_node *pNode)
178
+ {
179
+ if (pNode == NULL) return;
180
+
181
+ ma_node_uninit(&pNode->base, NULL);
182
+
183
+ for (int ch = 0; ch < 2; ch++) {
184
+ for (int c = 0; c < NUM_COMBS; c++) {
185
+ delay_line_free(&pNode->combs[ch][c]);
186
+ }
187
+ for (int a = 0; a < NUM_ALLPASSES; a++) {
188
+ delay_line_free(&pNode->allpasses[ch][a]);
189
+ }
190
+ }
191
+ }
192
+
193
+ // ============================================================================
194
+ // Parameter Control
195
+ // ============================================================================
196
+
197
+ void reverb_set_enabled(reverb_node *pNode, ma_bool32 enabled)
198
+ {
199
+ if (pNode) pNode->enabled = enabled;
200
+ }
201
+
202
+ void reverb_set_room_size(reverb_node *pNode, float size)
203
+ {
204
+ if (pNode == NULL) return;
205
+ pNode->room_size = size;
206
+ // Note: changing room_size after init would require reallocating buffers
207
+ // For now, this affects feedback calculation
208
+ pNode->comb_feedback = 0.6f + size * 0.35f; // 0.6 to 0.95
209
+ }
210
+
211
+ void reverb_set_damping(reverb_node *pNode, float damp)
212
+ {
213
+ if (pNode) pNode->comb_damp = damp;
214
+ }
215
+
216
+ void reverb_set_wet(reverb_node *pNode, float wet)
217
+ {
218
+ if (pNode) pNode->wet = wet;
219
+ }
220
+
221
+ void reverb_set_dry(reverb_node *pNode, float dry)
222
+ {
223
+ if (pNode) pNode->dry = dry;
224
+ }
@@ -0,0 +1,63 @@
1
+ // ============================================================================
2
+ // reverb_node.h - Schroeder reverb node for native_audio
3
+ // ============================================================================
4
+
5
+ #ifndef REVERB_NODE_H
6
+ #define REVERB_NODE_H
7
+
8
+ #include "miniaudio.h"
9
+
10
+ // ============================================================================
11
+ // Constants
12
+ // ============================================================================
13
+
14
+ #define NUM_COMBS 4
15
+ #define NUM_ALLPASSES 2
16
+
17
+ // ============================================================================
18
+ // Types
19
+ // ============================================================================
20
+
21
+ typedef struct {
22
+ float *buffer;
23
+ ma_uint32 size;
24
+ ma_uint32 pos;
25
+ } delay_line;
26
+
27
+ typedef struct {
28
+ ma_node_base base;
29
+ ma_uint32 channels;
30
+ ma_uint32 sample_rate;
31
+
32
+ // 4 parallel comb filters per audio channel
33
+ delay_line combs[2][NUM_COMBS]; // [audio_channel][comb_index]
34
+ float comb_feedback;
35
+ float comb_damp;
36
+ float comb_damp_prev[2][NUM_COMBS];
37
+
38
+ // 2 series allpass filters per audio channel
39
+ delay_line allpasses[2][NUM_ALLPASSES];
40
+ float allpass_feedback;
41
+
42
+ // Mix control
43
+ float wet;
44
+ float dry;
45
+ float room_size;
46
+ ma_bool32 enabled;
47
+ } reverb_node;
48
+
49
+ // ============================================================================
50
+ // Public API
51
+ // ============================================================================
52
+
53
+ ma_result reverb_init(reverb_node *pNode, ma_node_graph *pNodeGraph,
54
+ ma_uint32 sampleRate, ma_uint32 numChannels);
55
+ void reverb_uninit(reverb_node *pNode);
56
+
57
+ void reverb_set_enabled(reverb_node *pNode, ma_bool32 enabled);
58
+ void reverb_set_room_size(reverb_node *pNode, float size);
59
+ void reverb_set_damping(reverb_node *pNode, float damp);
60
+ void reverb_set_wet(reverb_node *pNode, float wet);
61
+ void reverb_set_dry(reverb_node *pNode, float dry);
62
+
63
+ #endif // REVERB_NODE_H
data/lib/dummy_audio.rb CHANGED
@@ -4,6 +4,7 @@
4
4
  # Has the same interface as the Audio C extension but does nothing.
5
5
  module DummyAudio
6
6
  @sound_count = 0
7
+ @tap_counts = {}
7
8
 
8
9
  def self.init
9
10
  nil
@@ -20,6 +21,7 @@ module DummyAudio
20
21
  end
21
22
 
22
23
  def self.play(channel, clip)
24
+ @tap_counts[channel] = 0
23
25
  channel
24
26
  end
25
27
 
@@ -46,4 +48,47 @@ module DummyAudio
46
48
  def self.set_pos(channel, angle, distance)
47
49
  nil
48
50
  end
51
+
52
+ def self.set_looping(channel, looping)
53
+ nil
54
+ end
55
+
56
+ def self.add_delay_tap(channel, time_ms, volume)
57
+ @tap_counts[channel] ||= 0
58
+ tap_id = @tap_counts[channel]
59
+ @tap_counts[channel] += 1
60
+ tap_id
61
+ end
62
+
63
+ def self.remove_delay_tap(channel, tap_id)
64
+ nil
65
+ end
66
+
67
+ def self.set_delay_tap_volume(channel, tap_id, volume)
68
+ nil
69
+ end
70
+
71
+ def self.set_delay_tap_time(channel, tap_id, time_ms)
72
+ nil
73
+ end
74
+
75
+ def self.enable_reverb(channel, enabled)
76
+ nil
77
+ end
78
+
79
+ def self.set_reverb_room_size(channel, size)
80
+ nil
81
+ end
82
+
83
+ def self.set_reverb_damping(channel, damp)
84
+ nil
85
+ end
86
+
87
+ def self.set_reverb_wet(channel, wet)
88
+ nil
89
+ end
90
+
91
+ def self.set_reverb_dry(channel, dry)
92
+ nil
93
+ end
49
94
  end
data/lib/native_audio.rb CHANGED
@@ -23,10 +23,39 @@ module NativeAudio
23
23
  end
24
24
  end
25
25
 
26
+ class DelayTap
27
+ attr_reader :id, :audio_source, :time_ms, :volume
28
+
29
+ def initialize(audio_source, id, time_ms, volume)
30
+ @audio_source = audio_source
31
+ @id = id
32
+ @time_ms = time_ms
33
+ @volume = volume
34
+ end
35
+
36
+ def volume=(val)
37
+ NativeAudio.audio_driver.set_delay_tap_volume(@audio_source.channel, @id, val)
38
+ @volume = val
39
+ end
40
+
41
+ def time_ms=(val)
42
+ NativeAudio.audio_driver.set_delay_tap_time(@audio_source.channel, @id, val)
43
+ @time_ms = val
44
+ end
45
+
46
+ def remove
47
+ NativeAudio.audio_driver.remove_delay_tap(@audio_source.channel, @id)
48
+ @audio_source.delay_taps.delete(self)
49
+ end
50
+ end
51
+
26
52
  class AudioSource
53
+ attr_reader :channel
54
+
27
55
  def initialize(clip)
28
56
  @clip = clip
29
57
  @channel = AudioSource.channels.count
58
+ @delay_taps = []
30
59
  AudioSource.channels << self
31
60
  end
32
61
 
@@ -58,6 +87,33 @@ module NativeAudio
58
87
  NativeAudio.audio_driver.set_pitch(@channel, pitch)
59
88
  end
60
89
 
90
+ def set_looping(looping)
91
+ NativeAudio.audio_driver.set_looping(@channel, looping)
92
+ end
93
+
94
+ def add_delay_tap(time_ms:, volume:)
95
+ tap_id = NativeAudio.audio_driver.add_delay_tap(@channel, time_ms, volume)
96
+ tap = DelayTap.new(self, tap_id, time_ms, volume)
97
+ @delay_taps << tap
98
+ tap
99
+ end
100
+
101
+ def enable_reverb(enabled = true)
102
+ NativeAudio.audio_driver.enable_reverb(@channel, enabled)
103
+ end
104
+
105
+ def set_reverb(room_size: 0.5, damping: 0.3, wet: 0.3, dry: 1.0)
106
+ NativeAudio.audio_driver.enable_reverb(@channel, true)
107
+ NativeAudio.audio_driver.set_reverb_room_size(@channel, room_size)
108
+ NativeAudio.audio_driver.set_reverb_damping(@channel, damping)
109
+ NativeAudio.audio_driver.set_reverb_wet(@channel, wet)
110
+ NativeAudio.audio_driver.set_reverb_dry(@channel, dry)
111
+ end
112
+
113
+ def delay_taps
114
+ @delay_taps
115
+ end
116
+
61
117
  def self.channels
62
118
  @channels ||= []
63
119
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: native_audio
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Max Hatfull
@@ -20,8 +20,13 @@ files:
20
20
  - LICENSE
21
21
  - README.md
22
22
  - ext/audio/audio.c
23
+ - ext/audio/audio.h
24
+ - ext/audio/delay_node.c
25
+ - ext/audio/delay_node.h
23
26
  - ext/audio/extconf.rb
24
27
  - ext/audio/miniaudio.h
28
+ - ext/audio/reverb_node.c
29
+ - ext/audio/reverb_node.h
25
30
  - lib/dummy_audio.rb
26
31
  - lib/native_audio.rb
27
32
  homepage: https://github.com/rubyrpg/native_audio