native_audio 0.5.7 → 0.5.9
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 +79 -2
- data/lib/dummy_audio.rb +18 -0
- data/lib/native_audio.rb +47 -21
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2d09213f5d2dc53028449b52f6fad665fbfc6c82514a5eb7f51f70111960e22c
|
|
4
|
+
data.tar.gz: ee29df7951ff18217a556256f24c1e9e847a631e99e7460572167ab81fd8f2ba
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dac3d8730c25b971ee92c337ab27cba7ed2ba2f629a027ec4597e165bc1ebc96dff7048233dd0e72d096db26c965ca807f52c3ffe57bd292ad42c822539f4990
|
|
7
|
+
data.tar.gz: 0ce1c4bed90ef8605987ebbc41e9cf3cf545bd185229eb1a66ccc99894fc293016c60e78e4203e08c7b5a3d6cbf993fcb6ef7260fbe437c331feb71f1041dd1b
|
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;
|
|
@@ -101,6 +102,7 @@ VALUE audio_init(VALUE self)
|
|
|
101
102
|
|
|
102
103
|
ma_engine_config config = ma_engine_config_init();
|
|
103
104
|
config.listenerCount = 1;
|
|
105
|
+
config.channels = 2;
|
|
104
106
|
|
|
105
107
|
if (use_null) {
|
|
106
108
|
ma_backend backends[] = { ma_backend_null };
|
|
@@ -181,19 +183,24 @@ VALUE audio_duration(VALUE self, VALUE clip)
|
|
|
181
183
|
// Playback Controls
|
|
182
184
|
// ============================================================================
|
|
183
185
|
|
|
184
|
-
static void cleanup_finished_channels(
|
|
186
|
+
static void cleanup_finished_channels(int skip_channel)
|
|
185
187
|
{
|
|
186
188
|
ma_uint64 now = ma_engine_get_time_in_pcm_frames(&engine);
|
|
187
189
|
ma_uint32 sample_rate = ma_engine_get_sample_rate(&engine);
|
|
188
190
|
ma_uint64 drain_frames = (ma_uint64)(REVERB_DRAIN_SECONDS * sample_rate);
|
|
189
191
|
|
|
190
192
|
for (int i = 0; i < MAX_CHANNELS; i++) {
|
|
193
|
+
if (i == skip_channel) continue;
|
|
191
194
|
// Phase 1: sound finished - uninit the sound, start drain timer
|
|
192
195
|
if (channels[i] != NULL && ma_sound_at_end(channels[i]) && !ma_sound_is_looping(channels[i])) {
|
|
193
196
|
ma_sound_uninit(channels[i]);
|
|
194
197
|
free(channels[i]);
|
|
195
198
|
channels[i] = NULL;
|
|
196
199
|
drain_until_frame[i] = now + drain_frames;
|
|
200
|
+
|
|
201
|
+
if (channel_freed_callback != Qnil) {
|
|
202
|
+
rb_funcall(channel_freed_callback, rb_intern("call"), 1, INT2NUM(i));
|
|
203
|
+
}
|
|
197
204
|
}
|
|
198
205
|
|
|
199
206
|
// Phase 2: drain timer expired - uninit delay and reverb nodes
|
|
@@ -230,7 +237,7 @@ VALUE audio_play(VALUE self, VALUE channel_id, VALUE clip)
|
|
|
230
237
|
return Qnil;
|
|
231
238
|
}
|
|
232
239
|
|
|
233
|
-
cleanup_finished_channels();
|
|
240
|
+
cleanup_finished_channels(channel);
|
|
234
241
|
|
|
235
242
|
// Cancel any pending drain timer for this channel
|
|
236
243
|
drain_until_frame[channel] = 0;
|
|
@@ -604,6 +611,71 @@ VALUE audio_set_reverb_dry(VALUE self, VALUE channel_id, VALUE dry)
|
|
|
604
611
|
return Qnil;
|
|
605
612
|
}
|
|
606
613
|
|
|
614
|
+
// ============================================================================
|
|
615
|
+
// Channel Query
|
|
616
|
+
// ============================================================================
|
|
617
|
+
|
|
618
|
+
VALUE audio_next_free_channel(VALUE self)
|
|
619
|
+
{
|
|
620
|
+
cleanup_finished_channels(-1);
|
|
621
|
+
|
|
622
|
+
// Prefer fully drained channels to preserve reverb tails
|
|
623
|
+
for (int i = 0; i < MAX_CHANNELS; i++) {
|
|
624
|
+
if (channels[i] == NULL && drain_until_frame[i] == 0) {
|
|
625
|
+
return rb_int2inum(i);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Fall back to draining channels if all else is exhausted
|
|
630
|
+
for (int i = 0; i < MAX_CHANNELS; i++) {
|
|
631
|
+
if (channels[i] == NULL) {
|
|
632
|
+
return rb_int2inum(i);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
return rb_int2inum(-1);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
VALUE audio_reset_all_channels(VALUE self)
|
|
640
|
+
{
|
|
641
|
+
for (int i = 0; i < MAX_CHANNELS; i++) {
|
|
642
|
+
if (channels[i] != NULL) {
|
|
643
|
+
ma_sound_stop(channels[i]);
|
|
644
|
+
ma_sound_uninit(channels[i]);
|
|
645
|
+
free(channels[i]);
|
|
646
|
+
channels[i] = NULL;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if (delay_nodes[i] != NULL) {
|
|
650
|
+
multi_tap_delay_uninit(delay_nodes[i]);
|
|
651
|
+
free(delay_nodes[i]);
|
|
652
|
+
delay_nodes[i] = NULL;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
if (reverb_nodes[i] != NULL) {
|
|
656
|
+
reverb_uninit(reverb_nodes[i]);
|
|
657
|
+
free(reverb_nodes[i]);
|
|
658
|
+
reverb_nodes[i] = NULL;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
drain_until_frame[i] = 0;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
return Qnil;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
VALUE audio_on_channel_freed(VALUE self, VALUE callback)
|
|
668
|
+
{
|
|
669
|
+
if (channel_freed_callback != Qnil) {
|
|
670
|
+
rb_gc_unregister_address(&channel_freed_callback);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
channel_freed_callback = callback;
|
|
674
|
+
rb_gc_register_address(&channel_freed_callback);
|
|
675
|
+
|
|
676
|
+
return Qnil;
|
|
677
|
+
}
|
|
678
|
+
|
|
607
679
|
// ============================================================================
|
|
608
680
|
// Ruby Module Setup
|
|
609
681
|
// ============================================================================
|
|
@@ -653,4 +725,9 @@ void Init_audio(void)
|
|
|
653
725
|
rb_define_singleton_method(mAudio, "set_reverb_damping", audio_set_reverb_damping, 2);
|
|
654
726
|
rb_define_singleton_method(mAudio, "set_reverb_wet", audio_set_reverb_wet, 2);
|
|
655
727
|
rb_define_singleton_method(mAudio, "set_reverb_dry", audio_set_reverb_dry, 2);
|
|
728
|
+
|
|
729
|
+
// Channel query
|
|
730
|
+
rb_define_singleton_method(mAudio, "next_free_channel", audio_next_free_channel, 0);
|
|
731
|
+
rb_define_singleton_method(mAudio, "on_channel_freed", audio_on_channel_freed, 1);
|
|
732
|
+
rb_define_singleton_method(mAudio, "reset_all_channels", audio_reset_all_channels, 0);
|
|
656
733
|
}
|
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
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: native_audio
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.5.
|
|
4
|
+
version: 0.5.9
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Max Hatfull
|
|
@@ -47,7 +47,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
47
47
|
- !ruby/object:Gem::Version
|
|
48
48
|
version: '0'
|
|
49
49
|
requirements: []
|
|
50
|
-
rubygems_version: 4.0.
|
|
50
|
+
rubygems_version: 4.0.16
|
|
51
51
|
specification_version: 4
|
|
52
52
|
summary: A simple audio library for Ruby
|
|
53
53
|
test_files: []
|