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 +4 -4
- data/README.md +74 -9
- data/ext/audio/audio.c +329 -66
- 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 +89 -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: e0dd09de66ecf033b152768c52c807b025e05f1a7a3cbbc5b0b15bab85746a42
|
|
4
|
+
data.tar.gz: 4af9bdb8da0bcb06d75335a69d56f5a513206ce06f5a849c7837f5bf713aa2cf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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,29 @@ VALUE audio_duration(VALUE self, VALUE clip)
|
|
|
129
180
|
// Playback Controls
|
|
130
181
|
// ============================================================================
|
|
131
182
|
|
|
132
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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],
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
435
|
+
// Delay Tap Controls
|
|
280
436
|
// ============================================================================
|
|
281
437
|
|
|
282
|
-
|
|
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
|
-
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
295
|
-
|
|
455
|
+
return rb_int2inum(tap_id);
|
|
456
|
+
}
|
|
296
457
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
467
|
+
multi_tap_delay_remove_tap(delay_nodes[channel], tap);
|
|
310
468
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
|
|
321
|
-
|
|
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++)
|
|
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
|
}
|