native_audio 0.3.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 (138) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +21 -0
  3. data/README.md +87 -0
  4. data/ext/audio/audio.c +321 -47
  5. data/ext/audio/extconf.rb +12 -110
  6. data/ext/audio/miniaudio.h +95844 -0
  7. data/lib/dummy_audio.rb +49 -0
  8. data/lib/native_audio.rb +22 -7
  9. metadata +7 -132
  10. data/assets/include/SDL2/SDL.h +0 -233
  11. data/assets/include/SDL2/SDL_assert.h +0 -326
  12. data/assets/include/SDL2/SDL_atomic.h +0 -415
  13. data/assets/include/SDL2/SDL_audio.h +0 -1500
  14. data/assets/include/SDL2/SDL_bits.h +0 -126
  15. data/assets/include/SDL2/SDL_blendmode.h +0 -198
  16. data/assets/include/SDL2/SDL_clipboard.h +0 -141
  17. data/assets/include/SDL2/SDL_config.h +0 -61
  18. data/assets/include/SDL2/SDL_config_android.h +0 -194
  19. data/assets/include/SDL2/SDL_config_emscripten.h +0 -218
  20. data/assets/include/SDL2/SDL_config_iphoneos.h +0 -217
  21. data/assets/include/SDL2/SDL_config_macosx.h +0 -277
  22. data/assets/include/SDL2/SDL_config_minimal.h +0 -95
  23. data/assets/include/SDL2/SDL_config_ngage.h +0 -89
  24. data/assets/include/SDL2/SDL_config_os2.h +0 -207
  25. data/assets/include/SDL2/SDL_config_pandora.h +0 -141
  26. data/assets/include/SDL2/SDL_config_windows.h +0 -331
  27. data/assets/include/SDL2/SDL_config_wingdk.h +0 -253
  28. data/assets/include/SDL2/SDL_config_winrt.h +0 -220
  29. data/assets/include/SDL2/SDL_config_xbox.h +0 -235
  30. data/assets/include/SDL2/SDL_copying.h +0 -20
  31. data/assets/include/SDL2/SDL_cpuinfo.h +0 -594
  32. data/assets/include/SDL2/SDL_egl.h +0 -2352
  33. data/assets/include/SDL2/SDL_endian.h +0 -348
  34. data/assets/include/SDL2/SDL_error.h +0 -163
  35. data/assets/include/SDL2/SDL_events.h +0 -1166
  36. data/assets/include/SDL2/SDL_filesystem.h +0 -149
  37. data/assets/include/SDL2/SDL_gamecontroller.h +0 -1074
  38. data/assets/include/SDL2/SDL_gesture.h +0 -117
  39. data/assets/include/SDL2/SDL_guid.h +0 -100
  40. data/assets/include/SDL2/SDL_haptic.h +0 -1341
  41. data/assets/include/SDL2/SDL_hidapi.h +0 -451
  42. data/assets/include/SDL2/SDL_hints.h +0 -2569
  43. data/assets/include/SDL2/SDL_image.h +0 -2173
  44. data/assets/include/SDL2/SDL_joystick.h +0 -1066
  45. data/assets/include/SDL2/SDL_keyboard.h +0 -353
  46. data/assets/include/SDL2/SDL_keycode.h +0 -358
  47. data/assets/include/SDL2/SDL_loadso.h +0 -115
  48. data/assets/include/SDL2/SDL_locale.h +0 -103
  49. data/assets/include/SDL2/SDL_log.h +0 -404
  50. data/assets/include/SDL2/SDL_main.h +0 -275
  51. data/assets/include/SDL2/SDL_messagebox.h +0 -193
  52. data/assets/include/SDL2/SDL_metal.h +0 -113
  53. data/assets/include/SDL2/SDL_misc.h +0 -79
  54. data/assets/include/SDL2/SDL_mixer.h +0 -2784
  55. data/assets/include/SDL2/SDL_mouse.h +0 -465
  56. data/assets/include/SDL2/SDL_mutex.h +0 -471
  57. data/assets/include/SDL2/SDL_name.h +0 -33
  58. data/assets/include/SDL2/SDL_opengl.h +0 -2132
  59. data/assets/include/SDL2/SDL_opengl_glext.h +0 -13209
  60. data/assets/include/SDL2/SDL_opengles.h +0 -39
  61. data/assets/include/SDL2/SDL_opengles2.h +0 -52
  62. data/assets/include/SDL2/SDL_opengles2_gl2.h +0 -656
  63. data/assets/include/SDL2/SDL_opengles2_gl2ext.h +0 -4033
  64. data/assets/include/SDL2/SDL_opengles2_gl2platform.h +0 -27
  65. data/assets/include/SDL2/SDL_opengles2_khrplatform.h +0 -311
  66. data/assets/include/SDL2/SDL_pixels.h +0 -644
  67. data/assets/include/SDL2/SDL_platform.h +0 -261
  68. data/assets/include/SDL2/SDL_power.h +0 -88
  69. data/assets/include/SDL2/SDL_quit.h +0 -58
  70. data/assets/include/SDL2/SDL_rect.h +0 -376
  71. data/assets/include/SDL2/SDL_render.h +0 -1919
  72. data/assets/include/SDL2/SDL_revision.h +0 -6
  73. data/assets/include/SDL2/SDL_rwops.h +0 -841
  74. data/assets/include/SDL2/SDL_scancode.h +0 -438
  75. data/assets/include/SDL2/SDL_sensor.h +0 -322
  76. data/assets/include/SDL2/SDL_shape.h +0 -155
  77. data/assets/include/SDL2/SDL_stdinc.h +0 -830
  78. data/assets/include/SDL2/SDL_surface.h +0 -997
  79. data/assets/include/SDL2/SDL_system.h +0 -623
  80. data/assets/include/SDL2/SDL_syswm.h +0 -386
  81. data/assets/include/SDL2/SDL_test.h +0 -69
  82. data/assets/include/SDL2/SDL_test_assert.h +0 -105
  83. data/assets/include/SDL2/SDL_test_common.h +0 -236
  84. data/assets/include/SDL2/SDL_test_compare.h +0 -69
  85. data/assets/include/SDL2/SDL_test_crc32.h +0 -124
  86. data/assets/include/SDL2/SDL_test_font.h +0 -168
  87. data/assets/include/SDL2/SDL_test_fuzzer.h +0 -386
  88. data/assets/include/SDL2/SDL_test_harness.h +0 -134
  89. data/assets/include/SDL2/SDL_test_images.h +0 -78
  90. data/assets/include/SDL2/SDL_test_log.h +0 -67
  91. data/assets/include/SDL2/SDL_test_md5.h +0 -129
  92. data/assets/include/SDL2/SDL_test_memory.h +0 -63
  93. data/assets/include/SDL2/SDL_test_random.h +0 -115
  94. data/assets/include/SDL2/SDL_thread.h +0 -464
  95. data/assets/include/SDL2/SDL_timer.h +0 -222
  96. data/assets/include/SDL2/SDL_touch.h +0 -150
  97. data/assets/include/SDL2/SDL_ttf.h +0 -2316
  98. data/assets/include/SDL2/SDL_types.h +0 -29
  99. data/assets/include/SDL2/SDL_version.h +0 -204
  100. data/assets/include/SDL2/SDL_video.h +0 -2150
  101. data/assets/include/SDL2/SDL_vulkan.h +0 -215
  102. data/assets/include/SDL2/begin_code.h +0 -187
  103. data/assets/include/SDL2/close_code.h +0 -40
  104. data/assets/macos/universal/lib/libFLAC.a +0 -0
  105. data/assets/macos/universal/lib/libSDL2.a +0 -0
  106. data/assets/macos/universal/lib/libSDL2_mixer.a +0 -0
  107. data/assets/macos/universal/lib/libmodplug.a +0 -0
  108. data/assets/macos/universal/lib/libmpg123.a +0 -0
  109. data/assets/macos/universal/lib/libogg.a +0 -0
  110. data/assets/macos/universal/lib/libvorbis.a +0 -0
  111. data/assets/macos/universal/lib/libvorbisfile.a +0 -0
  112. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libFLAC.a +0 -0
  113. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libSDL2.a +0 -0
  114. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libSDL2_mixer.a +0 -0
  115. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libmodplug.a +0 -0
  116. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libmpg123.a +0 -0
  117. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libogg.a +0 -0
  118. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libopus.a +0 -0
  119. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libopusfile.a +0 -0
  120. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libsndfile.a +0 -0
  121. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libstdc++.a +0 -0
  122. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libvorbis.a +0 -0
  123. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libvorbisfile.a +0 -0
  124. data/assets/windows/mingw-w64-ucrt-x86_64/lib/libz.a +0 -0
  125. data/assets/windows/mingw-w64-x86_64/lib/libFLAC.a +0 -0
  126. data/assets/windows/mingw-w64-x86_64/lib/libSDL2.a +0 -0
  127. data/assets/windows/mingw-w64-x86_64/lib/libSDL2_mixer.a +0 -0
  128. data/assets/windows/mingw-w64-x86_64/lib/libmodplug.a +0 -0
  129. data/assets/windows/mingw-w64-x86_64/lib/libmpg123.a +0 -0
  130. data/assets/windows/mingw-w64-x86_64/lib/libogg.a +0 -0
  131. data/assets/windows/mingw-w64-x86_64/lib/libopus.a +0 -0
  132. data/assets/windows/mingw-w64-x86_64/lib/libopusfile.a +0 -0
  133. data/assets/windows/mingw-w64-x86_64/lib/libsndfile.a +0 -0
  134. data/assets/windows/mingw-w64-x86_64/lib/libstdc++.a +0 -0
  135. data/assets/windows/mingw-w64-x86_64/lib/libvorbis.a +0 -0
  136. data/assets/windows/mingw-w64-x86_64/lib/libvorbisfile.a +0 -0
  137. data/assets/windows/mingw-w64-x86_64/lib/libz.a +0 -0
  138. data/ext/audio/extconf.h +0 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3bf44a7a337d94bcc47dc2cb5904837cd36984e9ea1b80768d1c5f624cc77dc3
4
- data.tar.gz: ddaece9c35182a2935d4c1d3f1da13d9dbfb7d4f2c0e41b385da8216b2927b05
3
+ metadata.gz: 1f5b017594ea0c5b0b94f2e3c36c057ccdd55275c133f5ffc77ef9dfca5a4b7c
4
+ data.tar.gz: a42b15d0bdf8fa4bfd6f673d3b0f638409653d061e640eb559f433bad2ae6f18
5
5
  SHA512:
6
- metadata.gz: d11e81e4ecd9de4b8eef28dbcc5a9405e6b366117c11971e74bbaa683ea40ce5751be264d23ca04a0f099e02435f5b84eea405cc0fc0f0f0dc26a07d277622a3
7
- data.tar.gz: 71b722055b3d9ed5054bad005470828a691f7d1082723c0237645930a816923d77de97d4a681a89d68bbbb9b8f818cf987b196f761dea9beba874bf7de394839
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 CHANGED
@@ -1,80 +1,354 @@
1
+ // ============================================================================
2
+ // native_audio - Ruby audio library using miniaudio
3
+ // ============================================================================
4
+
1
5
  #include <ruby.h>
2
- #include "extconf.h"
3
- #include <SDL2/SDL.h>
4
- #include <SDL2/SDL_mixer.h>
6
+ #include <math.h>
7
+ #include <stdlib.h>
8
+ #include <string.h>
5
9
 
6
- Mix_Chunk *sounds[1024];
7
- int sound_count = 0;
10
+ #define MINIAUDIO_IMPLEMENTATION
11
+ #include "miniaudio.h"
8
12
 
9
- VALUE audio_play(VALUE self, VALUE channel_id, VALUE clip)
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)
10
34
  {
11
- Mix_Chunk *sound = sounds[NUM2INT(clip)];
12
- int channel = Mix_PlayChannel(NUM2INT(channel_id), sound, 0);
13
- return rb_int2inum(channel);
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
+ }
14
78
  }
15
79
 
80
+ // ============================================================================
81
+ // Audio Loading
82
+ // ============================================================================
83
+
84
+ // Audio.load(path) - Load an audio file, returns clip ID
16
85
  VALUE audio_load(VALUE self, VALUE file)
17
86
  {
18
- Mix_Chunk *sound = NULL;
19
- sound = Mix_LoadWAV(StringValueCStr(file));
20
- if(sound == NULL)
21
- {
22
- fprintf(stderr, "Unable to load WAV file: %s\n", Mix_GetError());
23
- }
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
+ }
24
101
 
25
- sounds[0] = sound;
26
- sound_count++;
102
+ int id = sound_count;
103
+ sounds[id] = sound;
104
+ sound_count++;
27
105
 
28
- return rb_int2inum(sound_count - 1);
106
+ return rb_int2inum(id);
29
107
  }
30
108
 
31
- VALUE audio_set_pos(VALUE self, VALUE channel_id, VALUE angle, VALUE distance)
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)
32
134
  {
33
- Mix_SetPosition(NUM2INT(channel_id), NUM2INT(angle), NUM2INT(distance));
34
- return Qnil;
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);
35
174
  }
36
175
 
176
+ // Audio.stop(channel) - Stop playback and rewind
37
177
  VALUE audio_stop(VALUE self, VALUE channel_id)
38
178
  {
39
- Mix_HaltChannel(NUM2INT(channel_id));
40
- return Qnil;
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;
41
189
  }
42
190
 
191
+ // Audio.pause(channel) - Pause playback
43
192
  VALUE audio_pause(VALUE self, VALUE channel_id)
44
193
  {
45
- Mix_Pause(NUM2INT(channel_id));
46
- return Qnil;
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;
47
203
  }
48
204
 
205
+ // Audio.resume(channel) - Resume playback
49
206
  VALUE audio_resume(VALUE self, VALUE channel_id)
50
207
  {
51
- Mix_Resume(NUM2INT(channel_id));
52
- return Qnil;
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;
53
217
  }
54
218
 
219
+ // ============================================================================
220
+ // Audio Effects
221
+ // ============================================================================
222
+
223
+ // Audio.set_volume(channel, volume) - Set volume (0-128)
55
224
  VALUE audio_set_volume(VALUE self, VALUE channel_id, VALUE volume)
56
225
  {
57
- Mix_Volume(NUM2INT(channel_id), NUM2INT(volume));
58
- return Qnil;
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;
59
237
  }
60
238
 
61
- void Init_audio()
239
+ // Audio.set_pitch(channel, pitch) - Set pitch (1.0 = normal)
240
+ VALUE audio_set_pitch(VALUE self, VALUE channel_id, VALUE pitch)
62
241
  {
63
- if (SDL_Init(SDL_INIT_AUDIO) != 0) { exit(1); }
64
-
65
- int audio_rate = 22050;
66
- Uint16 audio_format = AUDIO_S16SYS;
67
- int audio_channels = 2;
68
- int audio_buffers = 4096;
69
-
70
- if(Mix_OpenAudio(audio_rate, audio_format, audio_channels, audio_buffers) != 0) { exit(1); }
71
-
72
- VALUE mAudio = rb_define_module("Audio");
73
- rb_define_singleton_method(mAudio, "load", audio_load, 1);
74
- rb_define_singleton_method(mAudio, "play", audio_play, 2);
75
- rb_define_singleton_method(mAudio, "set_pos", audio_set_pos, 3);
76
- rb_define_singleton_method(mAudio, "stop", audio_stop, 1);
77
- rb_define_singleton_method(mAudio, "pause", audio_pause, 1);
78
- rb_define_singleton_method(mAudio, "resume", audio_resume, 1);
79
- rb_define_singleton_method(mAudio, "set_volume", audio_set_volume, 2);
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);
80
354
  }
data/ext/audio/extconf.rb CHANGED
@@ -1,115 +1,17 @@
1
1
  require 'mkmf'
2
2
 
3
- # Get the root directory of the gem (two levels up from ext/audio/)
4
- ROOT_DIR = File.expand_path('../..', __dir__)
5
- ASSETS_DIR = File.join(ROOT_DIR, 'assets')
6
-
7
- # Detect platform
8
- PLATFORM = case RUBY_PLATFORM
9
- when /darwin/ then :macos
10
- when /linux/ then :linux
11
- when /mingw/ then :windows
12
- end
13
-
14
- def add_flags(type, flags)
15
- case type
16
- when :c then $CFLAGS << " #{flags} "
17
- when :ld then $LDFLAGS << " #{flags} "
18
- end
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
19
15
  end
20
16
 
21
- def check_sdl
22
- return if have_library('SDL2') && have_library('SDL2_mixer')
23
-
24
- msg = ["SDL2 libraries not found."]
25
-
26
- if PLATFORM == :linux
27
- if system('which apt >/dev/null 2>&1')
28
- msg << "Install with: sudo apt install libsdl2-dev libsdl2-mixer-dev"
29
- elsif system('which dnf >/dev/null 2>&1') || system('which yum >/dev/null 2>&1')
30
- msg << "Install with: sudo dnf install SDL2-devel SDL2_mixer-devel"
31
- elsif system('which pacman >/dev/null 2>&1')
32
- msg << "Install with: sudo pacman -S sdl2 sdl2_mixer"
33
- elsif system('which zypper >/dev/null 2>&1')
34
- msg << "Install with: sudo zypper install libSDL2-devel libSDL2_mixer-devel"
35
- end
36
- end
37
-
38
- abort msg.join("\n")
39
- end
40
-
41
- def set_linux_flags
42
- check_sdl
43
- add_flags(:ld, "-lSDL2 -lSDL2_mixer -lm")
44
- end
45
-
46
- def use_dev_libs
47
- case PLATFORM
48
- when :macos
49
- add_flags(:c, `sdl2-config --cflags`)
50
- add_flags(:c, '-I/opt/homebrew/include')
51
- add_flags(:ld, `sdl2-config --libs`)
52
- add_flags(:ld, '-lSDL2 -lSDL2_mixer')
53
- when :windows
54
- add_flags(:ld, '-lSDL2 -lSDL2_mixer')
55
- when :linux
56
- set_linux_flags
57
- end
58
- end
59
-
60
- def use_bundled_libs
61
- add_flags(:c, '-std=c11')
62
-
63
- case PLATFORM
64
- when :macos
65
- add_flags(:c, "-I#{ASSETS_DIR}/include")
66
- ldir = "#{ASSETS_DIR}/macos/universal/lib"
67
-
68
- add_flags(:ld, "#{ldir}/libSDL2.a #{ldir}/libSDL2_mixer.a")
69
- add_flags(:ld, "#{ldir}/libmpg123.a #{ldir}/libogg.a #{ldir}/libFLAC.a")
70
- add_flags(:ld, "#{ldir}/libvorbis.a #{ldir}/libvorbisfile.a #{ldir}/libmodplug.a")
71
- add_flags(:ld, "-lz -liconv -lstdc++")
72
-
73
- frameworks = %w[Cocoa Carbon GameController ForceFeedback
74
- AudioToolbox CoreAudio IOKit CoreHaptics CoreVideo Metal]
75
- add_flags(:ld, frameworks.map { |f| "-Wl,-framework,#{f}" }.join(' '))
76
-
77
- when :windows
78
- add_flags(:c, "-I#{ASSETS_DIR}/include")
79
-
80
- ldir = if RUBY_PLATFORM =~ /ucrt/
81
- "#{ASSETS_DIR}/windows/mingw-w64-ucrt-x86_64/lib"
82
- else
83
- "#{ASSETS_DIR}/windows/mingw-w64-x86_64/lib"
84
- end
85
-
86
- add_flags(:ld, "-Wl,--start-group")
87
- add_flags(:ld, "#{ldir}/libSDL2.a #{ldir}/libSDL2_mixer.a")
88
- add_flags(:ld, "#{ldir}/libmpg123.a #{ldir}/libFLAC.a #{ldir}/libvorbis.a")
89
- add_flags(:ld, "#{ldir}/libvorbisfile.a #{ldir}/libogg.a #{ldir}/libmodplug.a")
90
- add_flags(:ld, "#{ldir}/libopus.a #{ldir}/libopusfile.a #{ldir}/libsndfile.a")
91
- add_flags(:ld, "#{ldir}/libstdc++.a #{ldir}/libz.a")
92
- add_flags(:ld, '-lmingw32 -lole32 -loleaut32 -limm32')
93
- add_flags(:ld, '-lversion -lwinmm -lrpcrt4 -mwindows -lsetupapi -ldwrite')
94
- add_flags(:ld, "-Wl,--end-group")
95
-
96
- when :linux
97
- set_linux_flags
98
-
99
- else
100
- use_dev_libs
101
- end
102
- end
103
-
104
- # Main configuration
105
- if ARGV.include?('dev')
106
- use_dev_libs
107
- else
108
- use_bundled_libs
109
- end
110
-
111
- $CFLAGS.gsub!(/\n/, ' ')
112
- $LDFLAGS.gsub!(/\n/, ' ')
113
-
114
- create_header
115
17
  create_makefile('audio')