native_audio 0.5.6 → 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 -22
- 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,8 +3,9 @@
|
|
|
3
3
|
require_relative './audio'
|
|
4
4
|
require_relative './dummy_audio'
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
Audio.init
|
|
6
|
+
unless ENV['DUMMY_AUDIO_BACKEND'] == 'true'
|
|
7
|
+
Audio.init
|
|
8
|
+
end
|
|
8
9
|
|
|
9
10
|
module NativeAudio
|
|
10
11
|
def self.audio_driver
|
|
@@ -56,56 +57,74 @@ module NativeAudio
|
|
|
56
57
|
|
|
57
58
|
def initialize(clip)
|
|
58
59
|
@clip = clip
|
|
59
|
-
@channel = AudioSource.channels.count
|
|
60
60
|
@delay_taps = []
|
|
61
61
|
@params = {}
|
|
62
|
-
|
|
62
|
+
@channel = nil
|
|
63
63
|
end
|
|
64
64
|
|
|
65
65
|
def play
|
|
66
|
+
acquire_channel unless @channel
|
|
66
67
|
NativeAudio.audio_driver.play(@channel, @clip.clip)
|
|
67
68
|
apply_params
|
|
68
69
|
end
|
|
69
70
|
|
|
70
71
|
def stop
|
|
72
|
+
return unless @channel
|
|
71
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
|
+
})
|
|
72
91
|
end
|
|
73
92
|
|
|
74
93
|
def pause
|
|
75
|
-
NativeAudio.audio_driver.pause(@channel)
|
|
94
|
+
NativeAudio.audio_driver.pause(@channel) if @channel
|
|
76
95
|
end
|
|
77
96
|
|
|
78
97
|
def resume
|
|
79
|
-
NativeAudio.audio_driver.resume(@channel)
|
|
98
|
+
NativeAudio.audio_driver.resume(@channel) if @channel
|
|
80
99
|
end
|
|
81
100
|
|
|
82
101
|
def set_pos(angle, distance)
|
|
83
102
|
@params[:pos] = [angle, distance]
|
|
84
|
-
NativeAudio.audio_driver.set_pos(@channel, angle, distance)
|
|
103
|
+
NativeAudio.audio_driver.set_pos(@channel, angle, distance) if @channel
|
|
85
104
|
end
|
|
86
105
|
|
|
87
106
|
def set_pan(pan)
|
|
88
107
|
@params[:pan] = pan
|
|
89
|
-
NativeAudio.audio_driver.set_pan(@channel, pan)
|
|
108
|
+
NativeAudio.audio_driver.set_pan(@channel, pan) if @channel
|
|
90
109
|
end
|
|
91
110
|
|
|
92
111
|
def seek(seconds)
|
|
93
|
-
NativeAudio.audio_driver.seek(@channel, seconds)
|
|
112
|
+
NativeAudio.audio_driver.seek(@channel, seconds) if @channel
|
|
94
113
|
end
|
|
95
114
|
|
|
96
115
|
def set_volume(volume)
|
|
97
116
|
@params[:volume] = volume
|
|
98
|
-
NativeAudio.audio_driver.set_volume(@channel, volume)
|
|
117
|
+
NativeAudio.audio_driver.set_volume(@channel, volume) if @channel
|
|
99
118
|
end
|
|
100
119
|
|
|
101
120
|
def set_pitch(pitch)
|
|
102
121
|
@params[:pitch] = pitch
|
|
103
|
-
NativeAudio.audio_driver.set_pitch(@channel, pitch)
|
|
122
|
+
NativeAudio.audio_driver.set_pitch(@channel, pitch) if @channel
|
|
104
123
|
end
|
|
105
124
|
|
|
106
125
|
def set_looping(looping)
|
|
107
126
|
@params[:looping] = looping
|
|
108
|
-
NativeAudio.audio_driver.set_looping(@channel, looping)
|
|
127
|
+
NativeAudio.audio_driver.set_looping(@channel, looping) if @channel
|
|
109
128
|
end
|
|
110
129
|
|
|
111
130
|
def add_delay_tap(time_ms:, volume:)
|
|
@@ -117,28 +136,32 @@ module NativeAudio
|
|
|
117
136
|
|
|
118
137
|
def enable_reverb(enabled = true)
|
|
119
138
|
@params[:reverb_enabled] = enabled
|
|
120
|
-
NativeAudio.audio_driver.enable_reverb(@channel, enabled)
|
|
139
|
+
NativeAudio.audio_driver.enable_reverb(@channel, enabled) if @channel
|
|
121
140
|
end
|
|
122
141
|
|
|
123
142
|
def set_reverb(room_size: 0.5, damping: 0.3, wet: 0.3, dry: 1.0)
|
|
124
143
|
@params[:reverb] = { room_size: room_size, damping: damping, wet: wet, dry: dry }
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
|
130
151
|
end
|
|
131
152
|
|
|
132
153
|
def delay_taps
|
|
133
154
|
@delay_taps
|
|
134
155
|
end
|
|
135
156
|
|
|
136
|
-
def self.channels
|
|
137
|
-
@channels ||= []
|
|
138
|
-
end
|
|
139
|
-
|
|
140
157
|
private
|
|
141
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
|
+
|
|
142
165
|
def apply_params
|
|
143
166
|
NativeAudio.audio_driver.set_volume(@channel, @params[:volume]) if @params.key?(:volume)
|
|
144
167
|
NativeAudio.audio_driver.set_pitch(@channel, @params[:pitch]) if @params.key?(:pitch)
|
|
@@ -162,4 +185,6 @@ module NativeAudio
|
|
|
162
185
|
end
|
|
163
186
|
end
|
|
164
187
|
end
|
|
188
|
+
|
|
189
|
+
AudioSource.setup_channel_freed_callback
|
|
165
190
|
end
|