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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 646dd4b14196feef5695e9b95a1712f876217bd360876234780ce4276ae08eee
4
- data.tar.gz: dab8e4e10ffa4b1e0195d0d2fd94c8d4da41b33406bdbd453d347674cdf71912
3
+ metadata.gz: cc5fb26b26c23ec775416d61500af52eecaa5670c4ebfd67006ef5612f6d0a92
4
+ data.tar.gz: 8fe318acfed478c3abecc8c3a3e57da8890496efbaca0ba8746cfd3469c14d15
5
5
  SHA512:
6
- metadata.gz: 66a2c9fd9214622524a76dc79f45ea5ef7e49a8cd4bc0c4eb2eacce63ad505b163dd94fe49856e4518746604a0cb94a1660d6515bd5f3e6ea2b27e081db16583
7
- data.tar.gz: 18cf9f7de80f4274ee8a256f20510596692e97556be706af7c5e1d67c552d73076b99b4f0d15cf38aa6a166c81bd98a24faa5d7d916d14d5b26f7bfc13540f2a
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(void)
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
- Audio.init unless ENV['DUMMY_AUDIO_BACKEND'] == 'true'
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
- AudioSource.channels << self
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
- NativeAudio.audio_driver.enable_reverb(@channel, true)
125
- NativeAudio.audio_driver.set_reverb_room_size(@channel, room_size)
126
- NativeAudio.audio_driver.set_reverb_damping(@channel, damping)
127
- NativeAudio.audio_driver.set_reverb_wet(@channel, wet)
128
- NativeAudio.audio_driver.set_reverb_dry(@channel, dry)
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.7
4
+ version: 0.5.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Max Hatfull