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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 646dd4b14196feef5695e9b95a1712f876217bd360876234780ce4276ae08eee
4
- data.tar.gz: dab8e4e10ffa4b1e0195d0d2fd94c8d4da41b33406bdbd453d347674cdf71912
3
+ metadata.gz: 2d09213f5d2dc53028449b52f6fad665fbfc6c82514a5eb7f51f70111960e22c
4
+ data.tar.gz: ee29df7951ff18217a556256f24c1e9e847a631e99e7460572167ab81fd8f2ba
5
5
  SHA512:
6
- metadata.gz: 66a2c9fd9214622524a76dc79f45ea5ef7e49a8cd4bc0c4eb2eacce63ad505b163dd94fe49856e4518746604a0cb94a1660d6515bd5f3e6ea2b27e081db16583
7
- data.tar.gz: 18cf9f7de80f4274ee8a256f20510596692e97556be706af7c5e1d67c552d73076b99b4f0d15cf38aa6a166c81bd98a24faa5d7d916d14d5b26f7bfc13540f2a
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(void)
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
- 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.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.3
50
+ rubygems_version: 4.0.16
51
51
  specification_version: 4
52
52
  summary: A simple audio library for Ruby
53
53
  test_files: []