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 +4 -4
- data/README.md +74 -9
- data/ext/audio/audio.c +289 -65
- data/ext/audio/audio.h +34 -0
- data/ext/audio/delay_node.c +185 -0
- data/ext/audio/delay_node.h +51 -0
- data/ext/audio/reverb_node.c +224 -0
- data/ext/audio/reverb_node.h +63 -0
- data/lib/dummy_audio.rb +45 -0
- data/lib/native_audio.rb +56 -0
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a9a953a3fe6e4bd23e99278c28dbd273bc08268105382a4dae34854e3917aa64
|
|
4
|
+
data.tar.gz: af2a5c84a91fa2957984d4c36637bbe914f38fab7d26903406051eb2456dbc96
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
28
|
+
source = NativeAudio::AudioSource.new(clip)
|
|
29
29
|
|
|
30
30
|
# Play the clip
|
|
31
|
-
|
|
31
|
+
source.play
|
|
32
32
|
|
|
33
33
|
# Pause and resume
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
source.pause
|
|
35
|
+
source.resume
|
|
36
36
|
|
|
37
37
|
# Set pitch (1.0 = normal, 0.5 = octave down, 2.0 = octave up)
|
|
38
|
-
|
|
38
|
+
source.set_pitch(1.5)
|
|
39
39
|
|
|
40
40
|
# Set position relative to listener (angle: 0-360, distance: 0-255)
|
|
41
|
-
|
|
41
|
+
source.set_pos(90, 200) # right side, mid distance
|
|
42
42
|
|
|
43
43
|
# Set volume (0-128)
|
|
44
|
-
|
|
44
|
+
source.set_volume(64)
|
|
45
|
+
|
|
46
|
+
# Loop playback
|
|
47
|
+
source.set_looping(true)
|
|
45
48
|
|
|
46
49
|
# Stop the clip
|
|
47
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
15
|
+
// Global Definitions
|
|
15
16
|
// ============================================================================
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
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
|
-
|
|
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],
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
396
|
+
// Delay Tap Controls
|
|
280
397
|
// ============================================================================
|
|
281
398
|
|
|
282
|
-
|
|
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
|
-
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
295
|
-
|
|
416
|
+
return rb_int2inum(tap_id);
|
|
417
|
+
}
|
|
296
418
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
428
|
+
multi_tap_delay_remove_tap(delay_nodes[channel], tap);
|
|
310
429
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
|
|
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++)
|
|
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
|
+
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
|