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