native_audio 0.2.0 → 0.4.0

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.
Files changed (160) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +21 -0
  3. data/README.md +87 -0
  4. data/ext/audio/audio.c +354 -0
  5. data/ext/audio/extconf.rb +17 -0
  6. data/ext/audio/miniaudio.h +95844 -0
  7. data/lib/dummy_audio.rb +49 -0
  8. data/lib/native_audio.rb +65 -50
  9. metadata +9 -154
  10. data/assets/include/GL/glew.h +0 -26427
  11. data/assets/include/GLES2/gl2.h +0 -656
  12. data/assets/include/GLES2/gl2ext.h +0 -3949
  13. data/assets/include/GLES2/gl2ext_angle.h +0 -701
  14. data/assets/include/GLES2/gl2platform.h +0 -27
  15. data/assets/include/GLES3/gl3.h +0 -1192
  16. data/assets/include/GLES3/gl31.h +0 -1507
  17. data/assets/include/GLES3/gl32.h +0 -1808
  18. data/assets/include/GLES3/gl3platform.h +0 -27
  19. data/assets/include/KHR/khrplatform.h +0 -290
  20. data/assets/include/SDL2/SDL.h +0 -233
  21. data/assets/include/SDL2/SDL_assert.h +0 -326
  22. data/assets/include/SDL2/SDL_atomic.h +0 -415
  23. data/assets/include/SDL2/SDL_audio.h +0 -1500
  24. data/assets/include/SDL2/SDL_bits.h +0 -126
  25. data/assets/include/SDL2/SDL_blendmode.h +0 -198
  26. data/assets/include/SDL2/SDL_clipboard.h +0 -141
  27. data/assets/include/SDL2/SDL_config.h +0 -61
  28. data/assets/include/SDL2/SDL_config_android.h +0 -194
  29. data/assets/include/SDL2/SDL_config_emscripten.h +0 -218
  30. data/assets/include/SDL2/SDL_config_iphoneos.h +0 -217
  31. data/assets/include/SDL2/SDL_config_macosx.h +0 -277
  32. data/assets/include/SDL2/SDL_config_minimal.h +0 -95
  33. data/assets/include/SDL2/SDL_config_ngage.h +0 -89
  34. data/assets/include/SDL2/SDL_config_os2.h +0 -207
  35. data/assets/include/SDL2/SDL_config_pandora.h +0 -141
  36. data/assets/include/SDL2/SDL_config_windows.h +0 -331
  37. data/assets/include/SDL2/SDL_config_wingdk.h +0 -253
  38. data/assets/include/SDL2/SDL_config_winrt.h +0 -220
  39. data/assets/include/SDL2/SDL_config_xbox.h +0 -235
  40. data/assets/include/SDL2/SDL_copying.h +0 -20
  41. data/assets/include/SDL2/SDL_cpuinfo.h +0 -594
  42. data/assets/include/SDL2/SDL_egl.h +0 -2352
  43. data/assets/include/SDL2/SDL_endian.h +0 -348
  44. data/assets/include/SDL2/SDL_error.h +0 -163
  45. data/assets/include/SDL2/SDL_events.h +0 -1166
  46. data/assets/include/SDL2/SDL_filesystem.h +0 -149
  47. data/assets/include/SDL2/SDL_gamecontroller.h +0 -1074
  48. data/assets/include/SDL2/SDL_gesture.h +0 -117
  49. data/assets/include/SDL2/SDL_guid.h +0 -100
  50. data/assets/include/SDL2/SDL_haptic.h +0 -1341
  51. data/assets/include/SDL2/SDL_hidapi.h +0 -451
  52. data/assets/include/SDL2/SDL_hints.h +0 -2569
  53. data/assets/include/SDL2/SDL_image.h +0 -2173
  54. data/assets/include/SDL2/SDL_joystick.h +0 -1066
  55. data/assets/include/SDL2/SDL_keyboard.h +0 -353
  56. data/assets/include/SDL2/SDL_keycode.h +0 -358
  57. data/assets/include/SDL2/SDL_loadso.h +0 -115
  58. data/assets/include/SDL2/SDL_locale.h +0 -103
  59. data/assets/include/SDL2/SDL_log.h +0 -404
  60. data/assets/include/SDL2/SDL_main.h +0 -275
  61. data/assets/include/SDL2/SDL_messagebox.h +0 -193
  62. data/assets/include/SDL2/SDL_metal.h +0 -113
  63. data/assets/include/SDL2/SDL_misc.h +0 -79
  64. data/assets/include/SDL2/SDL_mixer.h +0 -2784
  65. data/assets/include/SDL2/SDL_mouse.h +0 -465
  66. data/assets/include/SDL2/SDL_mutex.h +0 -471
  67. data/assets/include/SDL2/SDL_name.h +0 -33
  68. data/assets/include/SDL2/SDL_opengl.h +0 -2132
  69. data/assets/include/SDL2/SDL_opengl_glext.h +0 -13209
  70. data/assets/include/SDL2/SDL_opengles.h +0 -39
  71. data/assets/include/SDL2/SDL_opengles2.h +0 -52
  72. data/assets/include/SDL2/SDL_opengles2_gl2.h +0 -656
  73. data/assets/include/SDL2/SDL_opengles2_gl2ext.h +0 -4033
  74. data/assets/include/SDL2/SDL_opengles2_gl2platform.h +0 -27
  75. data/assets/include/SDL2/SDL_opengles2_khrplatform.h +0 -311
  76. data/assets/include/SDL2/SDL_pixels.h +0 -644
  77. data/assets/include/SDL2/SDL_platform.h +0 -261
  78. data/assets/include/SDL2/SDL_power.h +0 -88
  79. data/assets/include/SDL2/SDL_quit.h +0 -58
  80. data/assets/include/SDL2/SDL_rect.h +0 -376
  81. data/assets/include/SDL2/SDL_render.h +0 -1919
  82. data/assets/include/SDL2/SDL_revision.h +0 -6
  83. data/assets/include/SDL2/SDL_rwops.h +0 -841
  84. data/assets/include/SDL2/SDL_scancode.h +0 -438
  85. data/assets/include/SDL2/SDL_sensor.h +0 -322
  86. data/assets/include/SDL2/SDL_shape.h +0 -155
  87. data/assets/include/SDL2/SDL_stdinc.h +0 -830
  88. data/assets/include/SDL2/SDL_surface.h +0 -997
  89. data/assets/include/SDL2/SDL_system.h +0 -623
  90. data/assets/include/SDL2/SDL_syswm.h +0 -386
  91. data/assets/include/SDL2/SDL_test.h +0 -69
  92. data/assets/include/SDL2/SDL_test_assert.h +0 -105
  93. data/assets/include/SDL2/SDL_test_common.h +0 -236
  94. data/assets/include/SDL2/SDL_test_compare.h +0 -69
  95. data/assets/include/SDL2/SDL_test_crc32.h +0 -124
  96. data/assets/include/SDL2/SDL_test_font.h +0 -168
  97. data/assets/include/SDL2/SDL_test_fuzzer.h +0 -386
  98. data/assets/include/SDL2/SDL_test_harness.h +0 -134
  99. data/assets/include/SDL2/SDL_test_images.h +0 -78
  100. data/assets/include/SDL2/SDL_test_log.h +0 -67
  101. data/assets/include/SDL2/SDL_test_md5.h +0 -129
  102. data/assets/include/SDL2/SDL_test_memory.h +0 -63
  103. data/assets/include/SDL2/SDL_test_random.h +0 -115
  104. data/assets/include/SDL2/SDL_thread.h +0 -464
  105. data/assets/include/SDL2/SDL_timer.h +0 -222
  106. data/assets/include/SDL2/SDL_touch.h +0 -150
  107. data/assets/include/SDL2/SDL_ttf.h +0 -2316
  108. data/assets/include/SDL2/SDL_types.h +0 -29
  109. data/assets/include/SDL2/SDL_version.h +0 -204
  110. data/assets/include/SDL2/SDL_video.h +0 -2150
  111. data/assets/include/SDL2/SDL_vulkan.h +0 -215
  112. data/assets/include/SDL2/begin_code.h +0 -187
  113. data/assets/include/SDL2/close_code.h +0 -40
  114. data/assets/macos/universal/lib/libFLAC.a +0 -0
  115. data/assets/macos/universal/lib/libSDL2.a +0 -0
  116. data/assets/macos/universal/lib/libSDL2_image.a +0 -0
  117. data/assets/macos/universal/lib/libSDL2_mixer.a +0 -0
  118. data/assets/macos/universal/lib/libSDL2_ttf.a +0 -0
  119. data/assets/macos/universal/lib/libmodplug.a +0 -0
  120. data/assets/macos/universal/lib/libmpg123.a +0 -0
  121. data/assets/macos/universal/lib/libogg.a +0 -0
  122. data/assets/macos/universal/lib/libvorbis.a +0 -0
  123. data/assets/macos/universal/lib/libvorbisfile.a +0 -0
  124. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libFLAC.a +0 -0
  125. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libSDL2.a +0 -0
  126. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libSDL2_mixer.a +0 -0
  127. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libglew32.a +0 -0
  128. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libmodplug.a +0 -0
  129. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libmpg123.a +0 -0
  130. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libogg.a +0 -0
  131. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libopus.a +0 -0
  132. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libopusfile.a +0 -0
  133. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libsndfile.a +0 -0
  134. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libssp.a +0 -1
  135. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libstdc++.a +0 -0
  136. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libvorbis.a +0 -0
  137. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libvorbisfile.a +0 -0
  138. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libz.a +0 -0
  139. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libzstd.a +0 -0
  140. data/assets/windows/mingw-w64-x86_64/lib/libFLAC.a +0 -0
  141. data/assets/windows/mingw-w64-x86_64/lib/libSDL2.a +0 -0
  142. data/assets/windows/mingw-w64-x86_64/lib/libSDL2_mixer.a +0 -0
  143. data/assets/windows/mingw-w64-x86_64/lib/libglew32.a +0 -0
  144. data/assets/windows/mingw-w64-x86_64/lib/libmodplug.a +0 -0
  145. data/assets/windows/mingw-w64-x86_64/lib/libmpg123.a +0 -0
  146. data/assets/windows/mingw-w64-x86_64/lib/libogg.a +0 -0
  147. data/assets/windows/mingw-w64-x86_64/lib/libopus.a +0 -0
  148. data/assets/windows/mingw-w64-x86_64/lib/libopusfile.a +0 -0
  149. data/assets/windows/mingw-w64-x86_64/lib/libsndfile.a +0 -0
  150. data/assets/windows/mingw-w64-x86_64/lib/libssp.a +0 -1
  151. data/assets/windows/mingw-w64-x86_64/lib/libstdc++.a +0 -0
  152. data/assets/windows/mingw-w64-x86_64/lib/libvorbis.a +0 -0
  153. data/assets/windows/mingw-w64-x86_64/lib/libvorbisfile.a +0 -0
  154. data/assets/windows/mingw-w64-x86_64/lib/libz.a +0 -0
  155. data/assets/windows/mingw-w64-x86_64/lib/libzstd.a +0 -0
  156. data/ext/audio.c +0 -80
  157. data/ext/extconf.h +0 -3
  158. data/ext/extconf.rb +0 -173
  159. data/lib/audio.bundle +0 -0
  160. data/lib/audio.so +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fa64178bae1a55555de56fc3e8faedcd719472d81d72fe0768672aa69701cb8f
4
- data.tar.gz: 5ae18b3ba3e5f20f09877b261513654afa88cd1a050ed0754b9d198fec0f9c4e
3
+ metadata.gz: 1f5b017594ea0c5b0b94f2e3c36c057ccdd55275c133f5ffc77ef9dfca5a4b7c
4
+ data.tar.gz: a42b15d0bdf8fa4bfd6f673d3b0f638409653d061e640eb559f433bad2ae6f18
5
5
  SHA512:
6
- metadata.gz: c20e84e9d147faa6f8ac31e32a0ad1152a4d66b28f3c8f4868e56743f4b0a68ee9191b69bef0e650d3fb451d4310d79412d5fb1dfda45639af5768b6f6aa861c
7
- data.tar.gz: d46152b36dc8f120b52df2af64b1cca301b06fcf0c2685e72dc3d4cfd14df8020bd83f4b311c5cad0404241f649d290d789a96b33088b05cea5d6f48f9f066c8
6
+ metadata.gz: cb6eb51f84784da896c21258f32393014845d5dcf47109e0ddc83f972c43e3353431c0addb42a8b9c14fed5397f9a8aaf1ce372549e7a321961a715afa577ceb
7
+ data.tar.gz: 82fe331ea3a1adb7a6281afe2978c62f7b9fc37c9836ed7ededda965034237af0a7acd54ca313973bb12ed1b22127d504981c70391ce0377ccf51898da99399a
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Max Hatfull
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,87 @@
1
+ # What is native_audio?
2
+
3
+ Native audio is a thin wrapper around miniaudio for simple audio playback from Ruby.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'native_audio'
11
+ ```
12
+ And then execute:
13
+
14
+ $ bundle
15
+
16
+ ## Usage
17
+
18
+ ```ruby
19
+ require 'native_audio'
20
+
21
+ # Load a clip
22
+ clip = NativeAudio::Clip.new('path/to/sound.wav')
23
+
24
+ # Check duration
25
+ clip.duration # => seconds
26
+
27
+ # Create an audio source to manage playback
28
+ audio_source = NativeAudio::AudioSource.new(clip)
29
+
30
+ # Play the clip
31
+ audio_source.play
32
+
33
+ # Pause and resume
34
+ audio_source.pause
35
+ audio_source.resume
36
+
37
+ # Set pitch (1.0 = normal, 0.5 = octave down, 2.0 = octave up)
38
+ audio_source.set_pitch(1.5)
39
+
40
+ # Set position relative to listener (angle: 0-360, distance: 0-255)
41
+ audio_source.set_pos(90, 200) # right side, mid distance
42
+
43
+ # Set volume (0-128)
44
+ audio_source.set_volume(64)
45
+
46
+ # Stop the clip
47
+ audio_source.stop
48
+ ```
49
+
50
+ ## Environment Variables
51
+
52
+ ### `NATIVE_AUDIO_DRIVER`
53
+
54
+ Set `NATIVE_AUDIO_DRIVER=null` to use miniaudio's null backend. This initializes the audio engine but outputs no sound - useful for CI/testing on systems without audio hardware.
55
+
56
+ ```bash
57
+ NATIVE_AUDIO_DRIVER=null ruby your_script.rb
58
+ ```
59
+
60
+ ### `DUMMY_AUDIO_BACKEND`
61
+
62
+ Set `DUMMY_AUDIO_BACKEND=true` to bypass miniaudio entirely and use a pure Ruby dummy backend. The C extension still loads (validating it compiles correctly), but no audio engine is initialized and all audio calls are no-ops.
63
+
64
+ ```bash
65
+ DUMMY_AUDIO_BACKEND=true ruby your_script.rb
66
+ ```
67
+
68
+ > **Note:** On Windows Server (e.g., GitHub Actions runners), miniaudio's null backend crashes due to threading issues in headless environments. Use `DUMMY_AUDIO_BACKEND=true` instead. macOS and Linux work fine with `NATIVE_AUDIO_DRIVER=null`.
69
+
70
+ ## Development
71
+
72
+ ```bash
73
+ bundle install
74
+ bundle exec rake compile
75
+ ```
76
+
77
+ To test:
78
+
79
+ ```bash
80
+ ruby test_audio.rb
81
+ ```
82
+
83
+ ## Releasing
84
+
85
+ 1. Update the version in `native_audio.gemspec`
86
+ 2. Go to GitHub Actions → "Release to RubyGems"
87
+ 3. Click "Run workflow" → "Run workflow"
data/ext/audio/audio.c ADDED
@@ -0,0 +1,354 @@
1
+ // ============================================================================
2
+ // native_audio - Ruby audio library using miniaudio
3
+ // ============================================================================
4
+
5
+ #include <ruby.h>
6
+ #include <math.h>
7
+ #include <stdlib.h>
8
+ #include <string.h>
9
+
10
+ #define MINIAUDIO_IMPLEMENTATION
11
+ #include "miniaudio.h"
12
+
13
+ // ============================================================================
14
+ // Constants & Globals
15
+ // ============================================================================
16
+
17
+ #define MAX_SOUNDS 1024
18
+ #define MAX_CHANNELS 1024
19
+
20
+ static ma_engine engine;
21
+ static ma_context context;
22
+ static ma_sound *sounds[MAX_SOUNDS]; // Loaded audio clips
23
+ static ma_sound *channels[MAX_CHANNELS]; // Playback instances
24
+ static int sound_count = 0;
25
+ static int engine_initialized = 0;
26
+ static int context_initialized = 0;
27
+ static int using_null_backend = 0;
28
+
29
+ // ============================================================================
30
+ // Cleanup (called on Ruby exit)
31
+ // ============================================================================
32
+
33
+ static void cleanup_audio(VALUE unused)
34
+ {
35
+ (void)unused;
36
+
37
+ if (!engine_initialized) {
38
+ return;
39
+ }
40
+
41
+ // Stop and clean up all channels
42
+ for (int i = 0; i < MAX_CHANNELS; i++) {
43
+ if (channels[i] != NULL) {
44
+ ma_sound_stop(channels[i]);
45
+ ma_sound_uninit(channels[i]);
46
+ free(channels[i]);
47
+ channels[i] = NULL;
48
+ }
49
+ }
50
+
51
+ // Stop and clean up all loaded sounds
52
+ for (int i = 0; i < sound_count; i++) {
53
+ if (sounds[i] != NULL) {
54
+ ma_sound_stop(sounds[i]);
55
+ ma_sound_uninit(sounds[i]);
56
+ free(sounds[i]);
57
+ sounds[i] = NULL;
58
+ }
59
+ }
60
+
61
+ // On Windows, ma_engine_uninit crashes with the null backend
62
+ // Since null backend has no real resources, we can skip cleanup
63
+ #ifdef _WIN32
64
+ if (using_null_backend) {
65
+ engine_initialized = 0;
66
+ context_initialized = 0;
67
+ return;
68
+ }
69
+ #endif
70
+
71
+ ma_engine_uninit(&engine);
72
+ engine_initialized = 0;
73
+
74
+ if (context_initialized) {
75
+ ma_context_uninit(&context);
76
+ context_initialized = 0;
77
+ }
78
+ }
79
+
80
+ // ============================================================================
81
+ // Audio Loading
82
+ // ============================================================================
83
+
84
+ // Audio.load(path) - Load an audio file, returns clip ID
85
+ VALUE audio_load(VALUE self, VALUE file)
86
+ {
87
+ const char *path = StringValueCStr(file);
88
+
89
+ ma_sound *sound = (ma_sound *)malloc(sizeof(ma_sound));
90
+ if (sound == NULL) {
91
+ rb_raise(rb_eRuntimeError, "Failed to allocate memory for sound");
92
+ return Qnil;
93
+ }
94
+
95
+ ma_result result = ma_sound_init_from_file(&engine, path, MA_SOUND_FLAG_DECODE, NULL, NULL, sound);
96
+ if (result != MA_SUCCESS) {
97
+ free(sound);
98
+ rb_raise(rb_eRuntimeError, "Failed to load audio file: %s", path);
99
+ return Qnil;
100
+ }
101
+
102
+ int id = sound_count;
103
+ sounds[id] = sound;
104
+ sound_count++;
105
+
106
+ return rb_int2inum(id);
107
+ }
108
+
109
+ // Audio.duration(clip) - Get duration of clip in seconds
110
+ VALUE audio_duration(VALUE self, VALUE clip)
111
+ {
112
+ int clip_id = NUM2INT(clip);
113
+
114
+ if (clip_id < 0 || clip_id >= sound_count || sounds[clip_id] == NULL) {
115
+ rb_raise(rb_eArgError, "Invalid clip ID: %d", clip_id);
116
+ return Qnil;
117
+ }
118
+
119
+ float length;
120
+ ma_result result = ma_sound_get_length_in_seconds(sounds[clip_id], &length);
121
+ if (result != MA_SUCCESS) {
122
+ return Qnil;
123
+ }
124
+
125
+ return rb_float_new(length);
126
+ }
127
+
128
+ // ============================================================================
129
+ // Playback Controls
130
+ // ============================================================================
131
+
132
+ // Audio.play(channel, clip) - Play a clip on a channel
133
+ VALUE audio_play(VALUE self, VALUE channel_id, VALUE clip)
134
+ {
135
+ int channel = NUM2INT(channel_id);
136
+ int clip_id = NUM2INT(clip);
137
+
138
+ if (clip_id < 0 || clip_id >= sound_count || sounds[clip_id] == NULL) {
139
+ rb_raise(rb_eArgError, "Invalid clip ID: %d", clip_id);
140
+ return Qnil;
141
+ }
142
+
143
+ if (channel < 0 || channel >= MAX_CHANNELS) {
144
+ rb_raise(rb_eArgError, "Invalid channel ID: %d", channel);
145
+ return Qnil;
146
+ }
147
+
148
+ // Clean up existing sound on this channel
149
+ if (channels[channel] != NULL) {
150
+ ma_sound_stop(channels[channel]);
151
+ ma_sound_uninit(channels[channel]);
152
+ free(channels[channel]);
153
+ channels[channel] = NULL;
154
+ }
155
+
156
+ // Create a copy of the sound for playback
157
+ ma_sound *playback = (ma_sound *)malloc(sizeof(ma_sound));
158
+ if (playback == NULL) {
159
+ rb_raise(rb_eRuntimeError, "Failed to allocate memory for playback");
160
+ return Qnil;
161
+ }
162
+
163
+ ma_result result = ma_sound_init_copy(&engine, sounds[clip_id], 0, NULL, playback);
164
+ if (result != MA_SUCCESS) {
165
+ free(playback);
166
+ rb_raise(rb_eRuntimeError, "Failed to create sound copy for playback");
167
+ return Qnil;
168
+ }
169
+
170
+ channels[channel] = playback;
171
+ ma_sound_start(playback);
172
+
173
+ return rb_int2inum(channel);
174
+ }
175
+
176
+ // Audio.stop(channel) - Stop playback and rewind
177
+ VALUE audio_stop(VALUE self, VALUE channel_id)
178
+ {
179
+ int channel = NUM2INT(channel_id);
180
+
181
+ if (channel < 0 || channel >= MAX_CHANNELS || channels[channel] == NULL) {
182
+ return Qnil;
183
+ }
184
+
185
+ ma_sound_stop(channels[channel]);
186
+ ma_sound_seek_to_pcm_frame(channels[channel], 0);
187
+
188
+ return Qnil;
189
+ }
190
+
191
+ // Audio.pause(channel) - Pause playback
192
+ VALUE audio_pause(VALUE self, VALUE channel_id)
193
+ {
194
+ int channel = NUM2INT(channel_id);
195
+
196
+ if (channel < 0 || channel >= MAX_CHANNELS || channels[channel] == NULL) {
197
+ return Qnil;
198
+ }
199
+
200
+ ma_sound_stop(channels[channel]);
201
+
202
+ return Qnil;
203
+ }
204
+
205
+ // Audio.resume(channel) - Resume playback
206
+ VALUE audio_resume(VALUE self, VALUE channel_id)
207
+ {
208
+ int channel = NUM2INT(channel_id);
209
+
210
+ if (channel < 0 || channel >= MAX_CHANNELS || channels[channel] == NULL) {
211
+ return Qnil;
212
+ }
213
+
214
+ ma_sound_start(channels[channel]);
215
+
216
+ return Qnil;
217
+ }
218
+
219
+ // ============================================================================
220
+ // Audio Effects
221
+ // ============================================================================
222
+
223
+ // Audio.set_volume(channel, volume) - Set volume (0-128)
224
+ VALUE audio_set_volume(VALUE self, VALUE channel_id, VALUE volume)
225
+ {
226
+ int channel = NUM2INT(channel_id);
227
+ int vol = NUM2INT(volume);
228
+
229
+ if (channel < 0 || channel >= MAX_CHANNELS || channels[channel] == NULL) {
230
+ return Qnil;
231
+ }
232
+
233
+ float normalized_volume = vol / 128.0f;
234
+ ma_sound_set_volume(channels[channel], normalized_volume);
235
+
236
+ return Qnil;
237
+ }
238
+
239
+ // Audio.set_pitch(channel, pitch) - Set pitch (1.0 = normal)
240
+ VALUE audio_set_pitch(VALUE self, VALUE channel_id, VALUE pitch)
241
+ {
242
+ int channel = NUM2INT(channel_id);
243
+ float p = (float)NUM2DBL(pitch);
244
+
245
+ if (channel < 0 || channel >= MAX_CHANNELS || channels[channel] == NULL) {
246
+ return Qnil;
247
+ }
248
+
249
+ ma_sound_set_pitch(channels[channel], p);
250
+
251
+ return Qnil;
252
+ }
253
+
254
+ // Audio.set_pos(channel, angle, distance) - Set 3D position
255
+ // angle: 0=front, 90=right, 180=back, 270=left
256
+ // distance: 0=close, 255=far
257
+ VALUE audio_set_pos(VALUE self, VALUE channel_id, VALUE angle, VALUE distance)
258
+ {
259
+ int channel = NUM2INT(channel_id);
260
+ int ang = NUM2INT(angle);
261
+ int dist = NUM2INT(distance);
262
+
263
+ if (channel < 0 || channel >= MAX_CHANNELS || channels[channel] == NULL) {
264
+ return Qnil;
265
+ }
266
+
267
+ // Convert polar to cartesian
268
+ float rad = ang * (MA_PI / 180.0f);
269
+ float normalized_dist = dist / 255.0f;
270
+ float x = normalized_dist * sinf(rad);
271
+ float z = -normalized_dist * cosf(rad);
272
+
273
+ ma_sound_set_position(channels[channel], x, 0.0f, z);
274
+
275
+ return Qnil;
276
+ }
277
+
278
+ // ============================================================================
279
+ // Engine Initialization
280
+ // ============================================================================
281
+
282
+ // Audio.init - Initialize the audio engine
283
+ VALUE audio_init(VALUE self)
284
+ {
285
+ if (engine_initialized) {
286
+ return Qnil;
287
+ }
288
+
289
+ // Check for null driver (for CI environments without audio devices)
290
+ // Usage: NATIVE_AUDIO_DRIVER=null ruby script.rb
291
+ const char *driver = getenv("NATIVE_AUDIO_DRIVER");
292
+ int use_null = (driver != NULL && strcmp(driver, "null") == 0);
293
+
294
+ ma_engine_config config = ma_engine_config_init();
295
+ config.listenerCount = 1;
296
+
297
+ if (use_null) {
298
+ ma_backend backends[] = { ma_backend_null };
299
+ ma_result ctx_result = ma_context_init(backends, 1, NULL, &context);
300
+ if (ctx_result != MA_SUCCESS) {
301
+ rb_raise(rb_eRuntimeError, "Failed to initialize null audio context");
302
+ return Qnil;
303
+ }
304
+ context_initialized = 1;
305
+ using_null_backend = 1;
306
+ config.pContext = &context;
307
+ }
308
+
309
+ ma_result result = ma_engine_init(&config, &engine);
310
+
311
+ if (result != MA_SUCCESS) {
312
+ if (context_initialized) {
313
+ ma_context_uninit(&context);
314
+ context_initialized = 0;
315
+ }
316
+ rb_raise(rb_eRuntimeError, "Failed to initialize audio engine");
317
+ return Qnil;
318
+ }
319
+
320
+ engine_initialized = 1;
321
+ rb_set_end_proc(cleanup_audio, Qnil);
322
+
323
+ return Qnil;
324
+ }
325
+
326
+ // ============================================================================
327
+ // Ruby Module Setup
328
+ // ============================================================================
329
+
330
+ void Init_audio(void)
331
+ {
332
+ for (int i = 0; i < MAX_SOUNDS; i++) sounds[i] = NULL;
333
+ for (int i = 0; i < MAX_CHANNELS; i++) channels[i] = NULL;
334
+
335
+ VALUE mAudio = rb_define_module("Audio");
336
+
337
+ // Initialization
338
+ rb_define_singleton_method(mAudio, "init", audio_init, 0);
339
+
340
+ // Loading
341
+ rb_define_singleton_method(mAudio, "load", audio_load, 1);
342
+ rb_define_singleton_method(mAudio, "duration", audio_duration, 1);
343
+
344
+ // Playback
345
+ rb_define_singleton_method(mAudio, "play", audio_play, 2);
346
+ rb_define_singleton_method(mAudio, "stop", audio_stop, 1);
347
+ rb_define_singleton_method(mAudio, "pause", audio_pause, 1);
348
+ rb_define_singleton_method(mAudio, "resume", audio_resume, 1);
349
+
350
+ // Effects
351
+ rb_define_singleton_method(mAudio, "set_volume", audio_set_volume, 2);
352
+ rb_define_singleton_method(mAudio, "set_pitch", audio_set_pitch, 2);
353
+ rb_define_singleton_method(mAudio, "set_pos", audio_set_pos, 3);
354
+ }
@@ -0,0 +1,17 @@
1
+ require 'mkmf'
2
+
3
+ # miniaudio linking requirements per platform:
4
+ # - macOS: CoreFoundation, CoreAudio, AudioToolbox frameworks
5
+ # - Linux: pthread, dl, m (math)
6
+ # - Windows: uses native APIs via runtime linking, no extra libs needed
7
+
8
+ case RUBY_PLATFORM
9
+ when /darwin/
10
+ $LDFLAGS << " -framework CoreFoundation -framework CoreAudio -framework AudioToolbox "
11
+ when /linux/
12
+ $LDFLAGS << " -ldl -lpthread -lm "
13
+ when /mingw|mswin/
14
+ # Windows uses runtime linking to native audio APIs
15
+ end
16
+
17
+ create_makefile('audio')