native_audio 0.4.0 → 0.5.1

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: e0dd09de66ecf033b152768c52c807b025e05f1a7a3cbbc5b0b15bab85746a42
4
+ data.tar.gz: 4af9bdb8da0bcb06d75335a69d56f5a513206ce06f5a849c7837f5bf713aa2cf
5
5
  SHA512:
6
- metadata.gz: cb6eb51f84784da896c21258f32393014845d5dcf47109e0ddc83f972c43e3353431c0addb42a8b9c14fed5397f9a8aaf1ce372549e7a321961a715afa577ceb
7
- data.tar.gz: 82fe331ea3a1adb7a6281afe2978c62f7b9fc37c9836ed7ededda965034237af0a7acd54ca313973bb12ed1b22127d504981c70391ce0377ccf51898da99399a
6
+ metadata.gz: 84abd6f60b5f33320866d5ad74aac58c604236322fe9d62c1894502ca531b203148631588dcd2d41afc63e4da08aa75805145eb951ce839dcc1e5eeaa8f18577
7
+ data.tar.gz: f37a3d01e2fba67d3b34c9b830ff53907b02a9a64d47df968c97365866d8b58a1870baa672cf0c553739bb58120646b16b68ecadec5224056c1d3384a90c4415
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,29 @@ 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
183
+ static void cleanup_finished_channels(void)
184
+ {
185
+ for (int i = 0; i < MAX_CHANNELS; i++) {
186
+ if (channels[i] != NULL && ma_sound_at_end(channels[i])) {
187
+ ma_sound_uninit(channels[i]);
188
+ free(channels[i]);
189
+ channels[i] = NULL;
190
+
191
+ if (delay_nodes[i] != NULL) {
192
+ multi_tap_delay_uninit(delay_nodes[i]);
193
+ free(delay_nodes[i]);
194
+ delay_nodes[i] = NULL;
195
+ }
196
+
197
+ if (reverb_nodes[i] != NULL) {
198
+ reverb_uninit(reverb_nodes[i]);
199
+ free(reverb_nodes[i]);
200
+ reverb_nodes[i] = NULL;
201
+ }
202
+ }
203
+ }
204
+ }
205
+
133
206
  VALUE audio_play(VALUE self, VALUE channel_id, VALUE clip)
134
207
  {
135
208
  int channel = NUM2INT(channel_id);
@@ -145,7 +218,9 @@ VALUE audio_play(VALUE self, VALUE channel_id, VALUE clip)
145
218
  return Qnil;
146
219
  }
147
220
 
148
- // Clean up existing sound on this channel
221
+ cleanup_finished_channels();
222
+
223
+ // Clean up existing resources on this channel
149
224
  if (channels[channel] != NULL) {
150
225
  ma_sound_stop(channels[channel]);
151
226
  ma_sound_uninit(channels[channel]);
@@ -153,27 +228,89 @@ VALUE audio_play(VALUE self, VALUE channel_id, VALUE clip)
153
228
  channels[channel] = NULL;
154
229
  }
155
230
 
156
- // Create a copy of the sound for playback
231
+ if (delay_nodes[channel] != NULL) {
232
+ multi_tap_delay_uninit(delay_nodes[channel]);
233
+ free(delay_nodes[channel]);
234
+ delay_nodes[channel] = NULL;
235
+ }
236
+
237
+ if (reverb_nodes[channel] != NULL) {
238
+ reverb_uninit(reverb_nodes[channel]);
239
+ free(reverb_nodes[channel]);
240
+ reverb_nodes[channel] = NULL;
241
+ }
242
+
243
+ // Create sound copy for playback
157
244
  ma_sound *playback = (ma_sound *)malloc(sizeof(ma_sound));
158
245
  if (playback == NULL) {
159
246
  rb_raise(rb_eRuntimeError, "Failed to allocate memory for playback");
160
247
  return Qnil;
161
248
  }
162
249
 
163
- ma_result result = ma_sound_init_copy(&engine, sounds[clip_id], 0, NULL, playback);
250
+ ma_result result = ma_sound_init_copy(&engine, sounds[clip_id], MA_SOUND_FLAG_NO_DEFAULT_ATTACHMENT, NULL, playback);
164
251
  if (result != MA_SUCCESS) {
165
252
  free(playback);
166
253
  rb_raise(rb_eRuntimeError, "Failed to create sound copy for playback");
167
254
  return Qnil;
168
255
  }
169
256
 
257
+ // Create delay node
258
+ ma_uint32 sampleRate = ma_engine_get_sample_rate(&engine);
259
+ ma_uint32 numChannels = ma_engine_get_channels(&engine);
260
+
261
+ multi_tap_delay_node *delayNode = (multi_tap_delay_node *)malloc(sizeof(multi_tap_delay_node));
262
+ if (delayNode == NULL) {
263
+ ma_sound_uninit(playback);
264
+ free(playback);
265
+ rb_raise(rb_eRuntimeError, "Failed to allocate memory for delay node");
266
+ return Qnil;
267
+ }
268
+
269
+ result = multi_tap_delay_init(delayNode, ma_engine_get_node_graph(&engine), sampleRate, numChannels);
270
+ if (result != MA_SUCCESS) {
271
+ free(delayNode);
272
+ ma_sound_uninit(playback);
273
+ free(playback);
274
+ rb_raise(rb_eRuntimeError, "Failed to initialize delay node");
275
+ return Qnil;
276
+ }
277
+
278
+ // Create reverb node
279
+ reverb_node *reverbNode = (reverb_node *)malloc(sizeof(reverb_node));
280
+ if (reverbNode == NULL) {
281
+ multi_tap_delay_uninit(delayNode);
282
+ free(delayNode);
283
+ ma_sound_uninit(playback);
284
+ free(playback);
285
+ rb_raise(rb_eRuntimeError, "Failed to allocate memory for reverb node");
286
+ return Qnil;
287
+ }
288
+
289
+ result = reverb_init(reverbNode, ma_engine_get_node_graph(&engine), sampleRate, numChannels);
290
+ if (result != MA_SUCCESS) {
291
+ free(reverbNode);
292
+ multi_tap_delay_uninit(delayNode);
293
+ free(delayNode);
294
+ ma_sound_uninit(playback);
295
+ free(playback);
296
+ rb_raise(rb_eRuntimeError, "Failed to initialize reverb node");
297
+ return Qnil;
298
+ }
299
+
300
+ // Route: sound -> delay_node -> reverb_node -> endpoint
301
+ ma_node *endpoint = ma_engine_get_endpoint(&engine);
302
+ ma_node_attach_output_bus(&reverbNode->base, 0, endpoint, 0);
303
+ ma_node_attach_output_bus(&delayNode->base, 0, &reverbNode->base, 0);
304
+ ma_node_attach_output_bus((ma_node *)playback, 0, &delayNode->base, 0);
305
+
306
+ delay_nodes[channel] = delayNode;
307
+ reverb_nodes[channel] = reverbNode;
170
308
  channels[channel] = playback;
171
309
  ma_sound_start(playback);
172
310
 
173
311
  return rb_int2inum(channel);
174
312
  }
175
313
 
176
- // Audio.stop(channel) - Stop playback and rewind
177
314
  VALUE audio_stop(VALUE self, VALUE channel_id)
178
315
  {
179
316
  int channel = NUM2INT(channel_id);
@@ -183,12 +320,25 @@ VALUE audio_stop(VALUE self, VALUE channel_id)
183
320
  }
184
321
 
185
322
  ma_sound_stop(channels[channel]);
186
- ma_sound_seek_to_pcm_frame(channels[channel], 0);
323
+ ma_sound_uninit(channels[channel]);
324
+ free(channels[channel]);
325
+ channels[channel] = NULL;
326
+
327
+ if (delay_nodes[channel] != NULL) {
328
+ multi_tap_delay_uninit(delay_nodes[channel]);
329
+ free(delay_nodes[channel]);
330
+ delay_nodes[channel] = NULL;
331
+ }
332
+
333
+ if (reverb_nodes[channel] != NULL) {
334
+ reverb_uninit(reverb_nodes[channel]);
335
+ free(reverb_nodes[channel]);
336
+ reverb_nodes[channel] = NULL;
337
+ }
187
338
 
188
339
  return Qnil;
189
340
  }
190
341
 
191
- // Audio.pause(channel) - Pause playback
192
342
  VALUE audio_pause(VALUE self, VALUE channel_id)
193
343
  {
194
344
  int channel = NUM2INT(channel_id);
@@ -202,7 +352,6 @@ VALUE audio_pause(VALUE self, VALUE channel_id)
202
352
  return Qnil;
203
353
  }
204
354
 
205
- // Audio.resume(channel) - Resume playback
206
355
  VALUE audio_resume(VALUE self, VALUE channel_id)
207
356
  {
208
357
  int channel = NUM2INT(channel_id);
@@ -217,10 +366,9 @@ VALUE audio_resume(VALUE self, VALUE channel_id)
217
366
  }
218
367
 
219
368
  // ============================================================================
220
- // Audio Effects
369
+ // Sound Effects
221
370
  // ============================================================================
222
371
 
223
- // Audio.set_volume(channel, volume) - Set volume (0-128)
224
372
  VALUE audio_set_volume(VALUE self, VALUE channel_id, VALUE volume)
225
373
  {
226
374
  int channel = NUM2INT(channel_id);
@@ -230,13 +378,11 @@ VALUE audio_set_volume(VALUE self, VALUE channel_id, VALUE volume)
230
378
  return Qnil;
231
379
  }
232
380
 
233
- float normalized_volume = vol / 128.0f;
234
- ma_sound_set_volume(channels[channel], normalized_volume);
381
+ ma_sound_set_volume(channels[channel], vol / 128.0f);
235
382
 
236
383
  return Qnil;
237
384
  }
238
385
 
239
- // Audio.set_pitch(channel, pitch) - Set pitch (1.0 = normal)
240
386
  VALUE audio_set_pitch(VALUE self, VALUE channel_id, VALUE pitch)
241
387
  {
242
388
  int channel = NUM2INT(channel_id);
@@ -251,9 +397,6 @@ VALUE audio_set_pitch(VALUE self, VALUE channel_id, VALUE pitch)
251
397
  return Qnil;
252
398
  }
253
399
 
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
400
  VALUE audio_set_pos(VALUE self, VALUE channel_id, VALUE angle, VALUE distance)
258
401
  {
259
402
  int channel = NUM2INT(channel_id);
@@ -264,7 +407,6 @@ VALUE audio_set_pos(VALUE self, VALUE channel_id, VALUE angle, VALUE distance)
264
407
  return Qnil;
265
408
  }
266
409
 
267
- // Convert polar to cartesian
268
410
  float rad = ang * (MA_PI / 180.0f);
269
411
  float normalized_dist = dist / 255.0f;
270
412
  float x = normalized_dist * sinf(rad);
@@ -275,54 +417,157 @@ VALUE audio_set_pos(VALUE self, VALUE channel_id, VALUE angle, VALUE distance)
275
417
  return Qnil;
276
418
  }
277
419
 
420
+ VALUE audio_set_looping(VALUE self, VALUE channel_id, VALUE looping)
421
+ {
422
+ int channel = NUM2INT(channel_id);
423
+ ma_bool32 loop = RTEST(looping) ? MA_TRUE : MA_FALSE;
424
+
425
+ if (channel < 0 || channel >= MAX_CHANNELS || channels[channel] == NULL) {
426
+ return Qnil;
427
+ }
428
+
429
+ ma_sound_set_looping(channels[channel], loop);
430
+
431
+ return Qnil;
432
+ }
433
+
278
434
  // ============================================================================
279
- // Engine Initialization
435
+ // Delay Tap Controls
280
436
  // ============================================================================
281
437
 
282
- // Audio.init - Initialize the audio engine
283
- VALUE audio_init(VALUE self)
438
+ VALUE audio_add_delay_tap(VALUE self, VALUE channel_id, VALUE time_ms, VALUE volume)
284
439
  {
285
- if (engine_initialized) {
440
+ int channel = NUM2INT(channel_id);
441
+ float ms = (float)NUM2DBL(time_ms);
442
+ float vol = (float)NUM2DBL(volume);
443
+
444
+ if (channel < 0 || channel >= MAX_CHANNELS || delay_nodes[channel] == NULL) {
445
+ rb_raise(rb_eArgError, "Invalid channel or no delay node: %d", channel);
286
446
  return Qnil;
287
447
  }
288
448
 
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);
449
+ int tap_id = multi_tap_delay_add_tap(delay_nodes[channel], ms, vol);
450
+ if (tap_id < 0) {
451
+ rb_raise(rb_eRuntimeError, "Failed to add delay tap (max taps reached)");
452
+ return Qnil;
453
+ }
293
454
 
294
- ma_engine_config config = ma_engine_config_init();
295
- config.listenerCount = 1;
455
+ return rb_int2inum(tap_id);
456
+ }
296
457
 
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;
458
+ VALUE audio_remove_delay_tap(VALUE self, VALUE channel_id, VALUE tap_id)
459
+ {
460
+ int channel = NUM2INT(channel_id);
461
+ int tap = NUM2INT(tap_id);
462
+
463
+ if (channel < 0 || channel >= MAX_CHANNELS || delay_nodes[channel] == NULL) {
464
+ return Qnil;
307
465
  }
308
466
 
309
- ma_result result = ma_engine_init(&config, &engine);
467
+ multi_tap_delay_remove_tap(delay_nodes[channel], tap);
310
468
 
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");
469
+ return Qnil;
470
+ }
471
+
472
+ VALUE audio_set_delay_tap_volume(VALUE self, VALUE channel_id, VALUE tap_id, VALUE volume)
473
+ {
474
+ int channel = NUM2INT(channel_id);
475
+ int tap = NUM2INT(tap_id);
476
+ float vol = (float)NUM2DBL(volume);
477
+
478
+ if (channel < 0 || channel >= MAX_CHANNELS || delay_nodes[channel] == NULL) {
317
479
  return Qnil;
318
480
  }
319
481
 
320
- engine_initialized = 1;
321
- rb_set_end_proc(cleanup_audio, Qnil);
482
+ multi_tap_delay_set_volume(delay_nodes[channel], tap, vol);
483
+
484
+ return Qnil;
485
+ }
486
+
487
+ VALUE audio_set_delay_tap_time(VALUE self, VALUE channel_id, VALUE tap_id, VALUE time_ms)
488
+ {
489
+ int channel = NUM2INT(channel_id);
490
+ int tap = NUM2INT(tap_id);
491
+ float ms = (float)NUM2DBL(time_ms);
492
+
493
+ if (channel < 0 || channel >= MAX_CHANNELS || delay_nodes[channel] == NULL) {
494
+ return Qnil;
495
+ }
496
+
497
+ multi_tap_delay_set_time(delay_nodes[channel], tap, ms);
322
498
 
323
499
  return Qnil;
324
500
  }
325
501
 
502
+ // ============================================================================
503
+ // Reverb Controls
504
+ // ============================================================================
505
+
506
+ VALUE audio_enable_reverb(VALUE self, VALUE channel_id, VALUE enabled)
507
+ {
508
+ int channel = NUM2INT(channel_id);
509
+ ma_bool32 en = RTEST(enabled) ? MA_TRUE : MA_FALSE;
510
+
511
+ if (channel < 0 || channel >= MAX_CHANNELS || reverb_nodes[channel] == NULL) {
512
+ return Qnil;
513
+ }
514
+
515
+ reverb_set_enabled(reverb_nodes[channel], en);
516
+ return Qnil;
517
+ }
518
+
519
+ VALUE audio_set_reverb_room_size(VALUE self, VALUE channel_id, VALUE size)
520
+ {
521
+ int channel = NUM2INT(channel_id);
522
+ float s = (float)NUM2DBL(size);
523
+
524
+ if (channel < 0 || channel >= MAX_CHANNELS || reverb_nodes[channel] == NULL) {
525
+ return Qnil;
526
+ }
527
+
528
+ reverb_set_room_size(reverb_nodes[channel], s);
529
+ return Qnil;
530
+ }
531
+
532
+ VALUE audio_set_reverb_damping(VALUE self, VALUE channel_id, VALUE damp)
533
+ {
534
+ int channel = NUM2INT(channel_id);
535
+ float d = (float)NUM2DBL(damp);
536
+
537
+ if (channel < 0 || channel >= MAX_CHANNELS || reverb_nodes[channel] == NULL) {
538
+ return Qnil;
539
+ }
540
+
541
+ reverb_set_damping(reverb_nodes[channel], d);
542
+ return Qnil;
543
+ }
544
+
545
+ VALUE audio_set_reverb_wet(VALUE self, VALUE channel_id, VALUE wet)
546
+ {
547
+ int channel = NUM2INT(channel_id);
548
+ float w = (float)NUM2DBL(wet);
549
+
550
+ if (channel < 0 || channel >= MAX_CHANNELS || reverb_nodes[channel] == NULL) {
551
+ return Qnil;
552
+ }
553
+
554
+ reverb_set_wet(reverb_nodes[channel], w);
555
+ return Qnil;
556
+ }
557
+
558
+ VALUE audio_set_reverb_dry(VALUE self, VALUE channel_id, VALUE dry)
559
+ {
560
+ int channel = NUM2INT(channel_id);
561
+ float d = (float)NUM2DBL(dry);
562
+
563
+ if (channel < 0 || channel >= MAX_CHANNELS || reverb_nodes[channel] == NULL) {
564
+ return Qnil;
565
+ }
566
+
567
+ reverb_set_dry(reverb_nodes[channel], d);
568
+ return Qnil;
569
+ }
570
+
326
571
  // ============================================================================
327
572
  // Ruby Module Setup
328
573
  // ============================================================================
@@ -330,7 +575,11 @@ VALUE audio_init(VALUE self)
330
575
  void Init_audio(void)
331
576
  {
332
577
  for (int i = 0; i < MAX_SOUNDS; i++) sounds[i] = NULL;
333
- for (int i = 0; i < MAX_CHANNELS; i++) channels[i] = NULL;
578
+ for (int i = 0; i < MAX_CHANNELS; i++) {
579
+ channels[i] = NULL;
580
+ delay_nodes[i] = NULL;
581
+ reverb_nodes[i] = NULL;
582
+ }
334
583
 
335
584
  VALUE mAudio = rb_define_module("Audio");
336
585
 
@@ -351,4 +600,18 @@ void Init_audio(void)
351
600
  rb_define_singleton_method(mAudio, "set_volume", audio_set_volume, 2);
352
601
  rb_define_singleton_method(mAudio, "set_pitch", audio_set_pitch, 2);
353
602
  rb_define_singleton_method(mAudio, "set_pos", audio_set_pos, 3);
603
+ rb_define_singleton_method(mAudio, "set_looping", audio_set_looping, 2);
604
+
605
+ // Delay taps
606
+ rb_define_singleton_method(mAudio, "add_delay_tap", audio_add_delay_tap, 3);
607
+ rb_define_singleton_method(mAudio, "remove_delay_tap", audio_remove_delay_tap, 2);
608
+ rb_define_singleton_method(mAudio, "set_delay_tap_volume", audio_set_delay_tap_volume, 3);
609
+ rb_define_singleton_method(mAudio, "set_delay_tap_time", audio_set_delay_tap_time, 3);
610
+
611
+ // Reverb
612
+ rb_define_singleton_method(mAudio, "enable_reverb", audio_enable_reverb, 2);
613
+ rb_define_singleton_method(mAudio, "set_reverb_room_size", audio_set_reverb_room_size, 2);
614
+ rb_define_singleton_method(mAudio, "set_reverb_damping", audio_set_reverb_damping, 2);
615
+ rb_define_singleton_method(mAudio, "set_reverb_wet", audio_set_reverb_wet, 2);
616
+ rb_define_singleton_method(mAudio, "set_reverb_dry", audio_set_reverb_dry, 2);
354
617
  }