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
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
|