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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 64a48d1c26dbc98e6b0231d7ccc72455ad5ee487141ce0dd9eb170ee43b47976
4
- data.tar.gz: 7962a4fd132fa2fe79e1f33eb87b7c06595a6cf8405850dd983b45ba9f2c596b
3
+ metadata.gz: cc5fb26b26c23ec775416d61500af52eecaa5670c4ebfd67006ef5612f6d0a92
4
+ data.tar.gz: 8fe318acfed478c3abecc8c3a3e57da8890496efbaca0ba8746cfd3469c14d15
5
5
  SHA512:
6
- metadata.gz: 3098316f693c450a8f0300f157964a7ddce5f9f1320bafd0447ce1e8707a568e48604fb2c10d5bcb8e1349dc7dca7db419757b9dc91c69f29b6e24e64103fd14
7
- data.tar.gz: 96ae5fd5579fb9132f2a428688453cc9885cd5bfc86edbe3a546e1ed50e8f06ec4fbd251dbfb6dfb58b1e484b7e51294b47bec4812c9938b3b62160425111e3f
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,8 +3,9 @@
3
3
  require_relative './audio'
4
4
  require_relative './dummy_audio'
5
5
 
6
- puts "[NativeAudio] Loading local development version"
7
- Audio.init unless ENV['DUMMY_AUDIO_BACKEND'] == 'true'
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
- AudioSource.channels << self
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
- NativeAudio.audio_driver.enable_reverb(@channel, true)
126
- NativeAudio.audio_driver.set_reverb_room_size(@channel, room_size)
127
- NativeAudio.audio_driver.set_reverb_damping(@channel, damping)
128
- NativeAudio.audio_driver.set_reverb_wet(@channel, wet)
129
- 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
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
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.6
4
+ version: 0.5.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Max Hatfull