native_audio 0.5.7 → 0.5.8
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/ext/audio/audio.c +78 -2
- data/lib/dummy_audio.rb +18 -0
- data/lib/native_audio.rb +47 -21
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cc5fb26b26c23ec775416d61500af52eecaa5670c4ebfd67006ef5612f6d0a92
|
|
4
|
+
data.tar.gz: 8fe318acfed478c3abecc8c3a3e57da8890496efbaca0ba8746cfd3469c14d15
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 50a42f1fd738fc8e47b194604c77bad198e3dee103372607c9b270a92245762970a5733d9e88175a1604e3f6ff5c38109a29f9cadbdd718ed2379d648ab4b218
|
|
7
|
+
data.tar.gz: 02d4cfa1d42e8baade8c6f3149406d79cda582589cc0e49f87e3a68e7154cda609a1eb03da1893dda24ca47c7d3a25a3546b9d292c1a666cae5ebcacaad48e8f
|
data/ext/audio/audio.c
CHANGED
|
@@ -22,6 +22,7 @@ ma_sound *channels[MAX_CHANNELS];
|
|
|
22
22
|
multi_tap_delay_node *delay_nodes[MAX_CHANNELS];
|
|
23
23
|
reverb_node *reverb_nodes[MAX_CHANNELS];
|
|
24
24
|
ma_uint64 drain_until_frame[MAX_CHANNELS];
|
|
25
|
+
static VALUE channel_freed_callback = Qnil;
|
|
25
26
|
int sound_count = 0;
|
|
26
27
|
int engine_initialized = 0;
|
|
27
28
|
int context_initialized = 0;
|
|
@@ -181,19 +182,24 @@ VALUE audio_duration(VALUE self, VALUE clip)
|
|
|
181
182
|
// Playback Controls
|
|
182
183
|
// ============================================================================
|
|
183
184
|
|
|
184
|
-
static void cleanup_finished_channels(
|
|
185
|
+
static void cleanup_finished_channels(int skip_channel)
|
|
185
186
|
{
|
|
186
187
|
ma_uint64 now = ma_engine_get_time_in_pcm_frames(&engine);
|
|
187
188
|
ma_uint32 sample_rate = ma_engine_get_sample_rate(&engine);
|
|
188
189
|
ma_uint64 drain_frames = (ma_uint64)(REVERB_DRAIN_SECONDS * sample_rate);
|
|
189
190
|
|
|
190
191
|
for (int i = 0; i < MAX_CHANNELS; i++) {
|
|
192
|
+
if (i == skip_channel) continue;
|
|
191
193
|
// Phase 1: sound finished - uninit the sound, start drain timer
|
|
192
194
|
if (channels[i] != NULL && ma_sound_at_end(channels[i]) && !ma_sound_is_looping(channels[i])) {
|
|
193
195
|
ma_sound_uninit(channels[i]);
|
|
194
196
|
free(channels[i]);
|
|
195
197
|
channels[i] = NULL;
|
|
196
198
|
drain_until_frame[i] = now + drain_frames;
|
|
199
|
+
|
|
200
|
+
if (channel_freed_callback != Qnil) {
|
|
201
|
+
rb_funcall(channel_freed_callback, rb_intern("call"), 1, INT2NUM(i));
|
|
202
|
+
}
|
|
197
203
|
}
|
|
198
204
|
|
|
199
205
|
// Phase 2: drain timer expired - uninit delay and reverb nodes
|
|
@@ -230,7 +236,7 @@ VALUE audio_play(VALUE self, VALUE channel_id, VALUE clip)
|
|
|
230
236
|
return Qnil;
|
|
231
237
|
}
|
|
232
238
|
|
|
233
|
-
cleanup_finished_channels();
|
|
239
|
+
cleanup_finished_channels(channel);
|
|
234
240
|
|
|
235
241
|
// Cancel any pending drain timer for this channel
|
|
236
242
|
drain_until_frame[channel] = 0;
|
|
@@ -604,6 +610,71 @@ VALUE audio_set_reverb_dry(VALUE self, VALUE channel_id, VALUE dry)
|
|
|
604
610
|
return Qnil;
|
|
605
611
|
}
|
|
606
612
|
|
|
613
|
+
// ============================================================================
|
|
614
|
+
// Channel Query
|
|
615
|
+
// ============================================================================
|
|
616
|
+
|
|
617
|
+
VALUE audio_next_free_channel(VALUE self)
|
|
618
|
+
{
|
|
619
|
+
cleanup_finished_channels(-1);
|
|
620
|
+
|
|
621
|
+
// Prefer fully drained channels to preserve reverb tails
|
|
622
|
+
for (int i = 0; i < MAX_CHANNELS; i++) {
|
|
623
|
+
if (channels[i] == NULL && drain_until_frame[i] == 0) {
|
|
624
|
+
return rb_int2inum(i);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Fall back to draining channels if all else is exhausted
|
|
629
|
+
for (int i = 0; i < MAX_CHANNELS; i++) {
|
|
630
|
+
if (channels[i] == NULL) {
|
|
631
|
+
return rb_int2inum(i);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
return rb_int2inum(-1);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
VALUE audio_reset_all_channels(VALUE self)
|
|
639
|
+
{
|
|
640
|
+
for (int i = 0; i < MAX_CHANNELS; i++) {
|
|
641
|
+
if (channels[i] != NULL) {
|
|
642
|
+
ma_sound_stop(channels[i]);
|
|
643
|
+
ma_sound_uninit(channels[i]);
|
|
644
|
+
free(channels[i]);
|
|
645
|
+
channels[i] = NULL;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
if (delay_nodes[i] != NULL) {
|
|
649
|
+
multi_tap_delay_uninit(delay_nodes[i]);
|
|
650
|
+
free(delay_nodes[i]);
|
|
651
|
+
delay_nodes[i] = NULL;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (reverb_nodes[i] != NULL) {
|
|
655
|
+
reverb_uninit(reverb_nodes[i]);
|
|
656
|
+
free(reverb_nodes[i]);
|
|
657
|
+
reverb_nodes[i] = NULL;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
drain_until_frame[i] = 0;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
return Qnil;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
VALUE audio_on_channel_freed(VALUE self, VALUE callback)
|
|
667
|
+
{
|
|
668
|
+
if (channel_freed_callback != Qnil) {
|
|
669
|
+
rb_gc_unregister_address(&channel_freed_callback);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
channel_freed_callback = callback;
|
|
673
|
+
rb_gc_register_address(&channel_freed_callback);
|
|
674
|
+
|
|
675
|
+
return Qnil;
|
|
676
|
+
}
|
|
677
|
+
|
|
607
678
|
// ============================================================================
|
|
608
679
|
// Ruby Module Setup
|
|
609
680
|
// ============================================================================
|
|
@@ -653,4 +724,9 @@ void Init_audio(void)
|
|
|
653
724
|
rb_define_singleton_method(mAudio, "set_reverb_damping", audio_set_reverb_damping, 2);
|
|
654
725
|
rb_define_singleton_method(mAudio, "set_reverb_wet", audio_set_reverb_wet, 2);
|
|
655
726
|
rb_define_singleton_method(mAudio, "set_reverb_dry", audio_set_reverb_dry, 2);
|
|
727
|
+
|
|
728
|
+
// Channel query
|
|
729
|
+
rb_define_singleton_method(mAudio, "next_free_channel", audio_next_free_channel, 0);
|
|
730
|
+
rb_define_singleton_method(mAudio, "on_channel_freed", audio_on_channel_freed, 1);
|
|
731
|
+
rb_define_singleton_method(mAudio, "reset_all_channels", audio_reset_all_channels, 0);
|
|
656
732
|
}
|
data/lib/dummy_audio.rb
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'set'
|
|
4
|
+
|
|
3
5
|
# Dummy audio backend for CI/testing environments without audio hardware.
|
|
4
6
|
# Has the same interface as the Audio C extension but does nothing.
|
|
5
7
|
module DummyAudio
|
|
6
8
|
@sound_count = 0
|
|
7
9
|
@tap_counts = {}
|
|
10
|
+
@active_channels = Set.new
|
|
11
|
+
@channel_freed_callback = nil
|
|
8
12
|
|
|
9
13
|
def self.init
|
|
10
14
|
nil
|
|
@@ -22,10 +26,12 @@ module DummyAudio
|
|
|
22
26
|
|
|
23
27
|
def self.play(channel, clip)
|
|
24
28
|
@tap_counts[channel] = 0
|
|
29
|
+
@active_channels << channel
|
|
25
30
|
channel
|
|
26
31
|
end
|
|
27
32
|
|
|
28
33
|
def self.stop(channel)
|
|
34
|
+
@active_channels.delete(channel)
|
|
29
35
|
nil
|
|
30
36
|
end
|
|
31
37
|
|
|
@@ -99,4 +105,16 @@ module DummyAudio
|
|
|
99
105
|
def self.set_reverb_dry(channel, dry)
|
|
100
106
|
nil
|
|
101
107
|
end
|
|
108
|
+
|
|
109
|
+
def self.next_free_channel
|
|
110
|
+
(0..1023).find { |i| !@active_channels.include?(i) } || -1
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def self.on_channel_freed(callback)
|
|
114
|
+
@channel_freed_callback = callback
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def self.reset_all_channels
|
|
118
|
+
@active_channels.clear
|
|
119
|
+
end
|
|
102
120
|
end
|
data/lib/native_audio.rb
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
require_relative './audio'
|
|
4
4
|
require_relative './dummy_audio'
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
unless ENV['DUMMY_AUDIO_BACKEND'] == 'true'
|
|
7
|
+
Audio.init
|
|
8
|
+
end
|
|
7
9
|
|
|
8
10
|
module NativeAudio
|
|
9
11
|
def self.audio_driver
|
|
@@ -55,56 +57,74 @@ module NativeAudio
|
|
|
55
57
|
|
|
56
58
|
def initialize(clip)
|
|
57
59
|
@clip = clip
|
|
58
|
-
@channel = AudioSource.channels.count
|
|
59
60
|
@delay_taps = []
|
|
60
61
|
@params = {}
|
|
61
|
-
|
|
62
|
+
@channel = nil
|
|
62
63
|
end
|
|
63
64
|
|
|
64
65
|
def play
|
|
66
|
+
acquire_channel unless @channel
|
|
65
67
|
NativeAudio.audio_driver.play(@channel, @clip.clip)
|
|
66
68
|
apply_params
|
|
67
69
|
end
|
|
68
70
|
|
|
69
71
|
def stop
|
|
72
|
+
return unless @channel
|
|
70
73
|
NativeAudio.audio_driver.stop(@channel)
|
|
74
|
+
self.class.owners.delete(@channel)
|
|
75
|
+
@channel = nil
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def channel_freed
|
|
79
|
+
@channel = nil
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def self.owners
|
|
83
|
+
@owners ||= {}
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def self.setup_channel_freed_callback
|
|
87
|
+
NativeAudio.audio_driver.on_channel_freed(proc { |channel|
|
|
88
|
+
owner = owners.delete(channel)
|
|
89
|
+
owner&.channel_freed
|
|
90
|
+
})
|
|
71
91
|
end
|
|
72
92
|
|
|
73
93
|
def pause
|
|
74
|
-
NativeAudio.audio_driver.pause(@channel)
|
|
94
|
+
NativeAudio.audio_driver.pause(@channel) if @channel
|
|
75
95
|
end
|
|
76
96
|
|
|
77
97
|
def resume
|
|
78
|
-
NativeAudio.audio_driver.resume(@channel)
|
|
98
|
+
NativeAudio.audio_driver.resume(@channel) if @channel
|
|
79
99
|
end
|
|
80
100
|
|
|
81
101
|
def set_pos(angle, distance)
|
|
82
102
|
@params[:pos] = [angle, distance]
|
|
83
|
-
NativeAudio.audio_driver.set_pos(@channel, angle, distance)
|
|
103
|
+
NativeAudio.audio_driver.set_pos(@channel, angle, distance) if @channel
|
|
84
104
|
end
|
|
85
105
|
|
|
86
106
|
def set_pan(pan)
|
|
87
107
|
@params[:pan] = pan
|
|
88
|
-
NativeAudio.audio_driver.set_pan(@channel, pan)
|
|
108
|
+
NativeAudio.audio_driver.set_pan(@channel, pan) if @channel
|
|
89
109
|
end
|
|
90
110
|
|
|
91
111
|
def seek(seconds)
|
|
92
|
-
NativeAudio.audio_driver.seek(@channel, seconds)
|
|
112
|
+
NativeAudio.audio_driver.seek(@channel, seconds) if @channel
|
|
93
113
|
end
|
|
94
114
|
|
|
95
115
|
def set_volume(volume)
|
|
96
116
|
@params[:volume] = volume
|
|
97
|
-
NativeAudio.audio_driver.set_volume(@channel, volume)
|
|
117
|
+
NativeAudio.audio_driver.set_volume(@channel, volume) if @channel
|
|
98
118
|
end
|
|
99
119
|
|
|
100
120
|
def set_pitch(pitch)
|
|
101
121
|
@params[:pitch] = pitch
|
|
102
|
-
NativeAudio.audio_driver.set_pitch(@channel, pitch)
|
|
122
|
+
NativeAudio.audio_driver.set_pitch(@channel, pitch) if @channel
|
|
103
123
|
end
|
|
104
124
|
|
|
105
125
|
def set_looping(looping)
|
|
106
126
|
@params[:looping] = looping
|
|
107
|
-
NativeAudio.audio_driver.set_looping(@channel, looping)
|
|
127
|
+
NativeAudio.audio_driver.set_looping(@channel, looping) if @channel
|
|
108
128
|
end
|
|
109
129
|
|
|
110
130
|
def add_delay_tap(time_ms:, volume:)
|
|
@@ -116,28 +136,32 @@ module NativeAudio
|
|
|
116
136
|
|
|
117
137
|
def enable_reverb(enabled = true)
|
|
118
138
|
@params[:reverb_enabled] = enabled
|
|
119
|
-
NativeAudio.audio_driver.enable_reverb(@channel, enabled)
|
|
139
|
+
NativeAudio.audio_driver.enable_reverb(@channel, enabled) if @channel
|
|
120
140
|
end
|
|
121
141
|
|
|
122
142
|
def set_reverb(room_size: 0.5, damping: 0.3, wet: 0.3, dry: 1.0)
|
|
123
143
|
@params[:reverb] = { room_size: room_size, damping: damping, wet: wet, dry: dry }
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
144
|
+
if @channel
|
|
145
|
+
NativeAudio.audio_driver.enable_reverb(@channel, true)
|
|
146
|
+
NativeAudio.audio_driver.set_reverb_room_size(@channel, room_size)
|
|
147
|
+
NativeAudio.audio_driver.set_reverb_damping(@channel, damping)
|
|
148
|
+
NativeAudio.audio_driver.set_reverb_wet(@channel, wet)
|
|
149
|
+
NativeAudio.audio_driver.set_reverb_dry(@channel, dry)
|
|
150
|
+
end
|
|
129
151
|
end
|
|
130
152
|
|
|
131
153
|
def delay_taps
|
|
132
154
|
@delay_taps
|
|
133
155
|
end
|
|
134
156
|
|
|
135
|
-
def self.channels
|
|
136
|
-
@channels ||= []
|
|
137
|
-
end
|
|
138
|
-
|
|
139
157
|
private
|
|
140
158
|
|
|
159
|
+
def acquire_channel
|
|
160
|
+
@channel = NativeAudio.audio_driver.next_free_channel
|
|
161
|
+
raise "No free audio channels available" if @channel < 0
|
|
162
|
+
self.class.owners[@channel] = self
|
|
163
|
+
end
|
|
164
|
+
|
|
141
165
|
def apply_params
|
|
142
166
|
NativeAudio.audio_driver.set_volume(@channel, @params[:volume]) if @params.key?(:volume)
|
|
143
167
|
NativeAudio.audio_driver.set_pitch(@channel, @params[:pitch]) if @params.key?(:pitch)
|
|
@@ -161,4 +185,6 @@ module NativeAudio
|
|
|
161
185
|
end
|
|
162
186
|
end
|
|
163
187
|
end
|
|
188
|
+
|
|
189
|
+
AudioSource.setup_channel_freed_callback
|
|
164
190
|
end
|