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.
- checksums.yaml +4 -4
- data/LICENSE +21 -0
- data/README.md +87 -0
- data/ext/audio/audio.c +321 -47
- data/ext/audio/extconf.rb +12 -110
- data/ext/audio/miniaudio.h +95844 -0
- data/lib/dummy_audio.rb +49 -0
- data/lib/native_audio.rb +22 -7
- metadata +7 -132
- data/assets/include/SDL2/SDL.h +0 -233
- data/assets/include/SDL2/SDL_assert.h +0 -326
- data/assets/include/SDL2/SDL_atomic.h +0 -415
- data/assets/include/SDL2/SDL_audio.h +0 -1500
- data/assets/include/SDL2/SDL_bits.h +0 -126
- data/assets/include/SDL2/SDL_blendmode.h +0 -198
- data/assets/include/SDL2/SDL_clipboard.h +0 -141
- data/assets/include/SDL2/SDL_config.h +0 -61
- data/assets/include/SDL2/SDL_config_android.h +0 -194
- data/assets/include/SDL2/SDL_config_emscripten.h +0 -218
- data/assets/include/SDL2/SDL_config_iphoneos.h +0 -217
- data/assets/include/SDL2/SDL_config_macosx.h +0 -277
- data/assets/include/SDL2/SDL_config_minimal.h +0 -95
- data/assets/include/SDL2/SDL_config_ngage.h +0 -89
- data/assets/include/SDL2/SDL_config_os2.h +0 -207
- data/assets/include/SDL2/SDL_config_pandora.h +0 -141
- data/assets/include/SDL2/SDL_config_windows.h +0 -331
- data/assets/include/SDL2/SDL_config_wingdk.h +0 -253
- data/assets/include/SDL2/SDL_config_winrt.h +0 -220
- data/assets/include/SDL2/SDL_config_xbox.h +0 -235
- data/assets/include/SDL2/SDL_copying.h +0 -20
- data/assets/include/SDL2/SDL_cpuinfo.h +0 -594
- data/assets/include/SDL2/SDL_egl.h +0 -2352
- data/assets/include/SDL2/SDL_endian.h +0 -348
- data/assets/include/SDL2/SDL_error.h +0 -163
- data/assets/include/SDL2/SDL_events.h +0 -1166
- data/assets/include/SDL2/SDL_filesystem.h +0 -149
- data/assets/include/SDL2/SDL_gamecontroller.h +0 -1074
- data/assets/include/SDL2/SDL_gesture.h +0 -117
- data/assets/include/SDL2/SDL_guid.h +0 -100
- data/assets/include/SDL2/SDL_haptic.h +0 -1341
- data/assets/include/SDL2/SDL_hidapi.h +0 -451
- data/assets/include/SDL2/SDL_hints.h +0 -2569
- data/assets/include/SDL2/SDL_image.h +0 -2173
- data/assets/include/SDL2/SDL_joystick.h +0 -1066
- data/assets/include/SDL2/SDL_keyboard.h +0 -353
- data/assets/include/SDL2/SDL_keycode.h +0 -358
- data/assets/include/SDL2/SDL_loadso.h +0 -115
- data/assets/include/SDL2/SDL_locale.h +0 -103
- data/assets/include/SDL2/SDL_log.h +0 -404
- data/assets/include/SDL2/SDL_main.h +0 -275
- data/assets/include/SDL2/SDL_messagebox.h +0 -193
- data/assets/include/SDL2/SDL_metal.h +0 -113
- data/assets/include/SDL2/SDL_misc.h +0 -79
- data/assets/include/SDL2/SDL_mixer.h +0 -2784
- data/assets/include/SDL2/SDL_mouse.h +0 -465
- data/assets/include/SDL2/SDL_mutex.h +0 -471
- data/assets/include/SDL2/SDL_name.h +0 -33
- data/assets/include/SDL2/SDL_opengl.h +0 -2132
- data/assets/include/SDL2/SDL_opengl_glext.h +0 -13209
- data/assets/include/SDL2/SDL_opengles.h +0 -39
- data/assets/include/SDL2/SDL_opengles2.h +0 -52
- data/assets/include/SDL2/SDL_opengles2_gl2.h +0 -656
- data/assets/include/SDL2/SDL_opengles2_gl2ext.h +0 -4033
- data/assets/include/SDL2/SDL_opengles2_gl2platform.h +0 -27
- data/assets/include/SDL2/SDL_opengles2_khrplatform.h +0 -311
- data/assets/include/SDL2/SDL_pixels.h +0 -644
- data/assets/include/SDL2/SDL_platform.h +0 -261
- data/assets/include/SDL2/SDL_power.h +0 -88
- data/assets/include/SDL2/SDL_quit.h +0 -58
- data/assets/include/SDL2/SDL_rect.h +0 -376
- data/assets/include/SDL2/SDL_render.h +0 -1919
- data/assets/include/SDL2/SDL_revision.h +0 -6
- data/assets/include/SDL2/SDL_rwops.h +0 -841
- data/assets/include/SDL2/SDL_scancode.h +0 -438
- data/assets/include/SDL2/SDL_sensor.h +0 -322
- data/assets/include/SDL2/SDL_shape.h +0 -155
- data/assets/include/SDL2/SDL_stdinc.h +0 -830
- data/assets/include/SDL2/SDL_surface.h +0 -997
- data/assets/include/SDL2/SDL_system.h +0 -623
- data/assets/include/SDL2/SDL_syswm.h +0 -386
- data/assets/include/SDL2/SDL_test.h +0 -69
- data/assets/include/SDL2/SDL_test_assert.h +0 -105
- data/assets/include/SDL2/SDL_test_common.h +0 -236
- data/assets/include/SDL2/SDL_test_compare.h +0 -69
- data/assets/include/SDL2/SDL_test_crc32.h +0 -124
- data/assets/include/SDL2/SDL_test_font.h +0 -168
- data/assets/include/SDL2/SDL_test_fuzzer.h +0 -386
- data/assets/include/SDL2/SDL_test_harness.h +0 -134
- data/assets/include/SDL2/SDL_test_images.h +0 -78
- data/assets/include/SDL2/SDL_test_log.h +0 -67
- data/assets/include/SDL2/SDL_test_md5.h +0 -129
- data/assets/include/SDL2/SDL_test_memory.h +0 -63
- data/assets/include/SDL2/SDL_test_random.h +0 -115
- data/assets/include/SDL2/SDL_thread.h +0 -464
- data/assets/include/SDL2/SDL_timer.h +0 -222
- data/assets/include/SDL2/SDL_touch.h +0 -150
- data/assets/include/SDL2/SDL_ttf.h +0 -2316
- data/assets/include/SDL2/SDL_types.h +0 -29
- data/assets/include/SDL2/SDL_version.h +0 -204
- data/assets/include/SDL2/SDL_video.h +0 -2150
- data/assets/include/SDL2/SDL_vulkan.h +0 -215
- data/assets/include/SDL2/begin_code.h +0 -187
- data/assets/include/SDL2/close_code.h +0 -40
- data/assets/macos/universal/lib/libFLAC.a +0 -0
- data/assets/macos/universal/lib/libSDL2.a +0 -0
- data/assets/macos/universal/lib/libSDL2_mixer.a +0 -0
- data/assets/macos/universal/lib/libmodplug.a +0 -0
- data/assets/macos/universal/lib/libmpg123.a +0 -0
- data/assets/macos/universal/lib/libogg.a +0 -0
- data/assets/macos/universal/lib/libvorbis.a +0 -0
- data/assets/macos/universal/lib/libvorbisfile.a +0 -0
- data/assets/windows/mingw-w64-ucrt-x86_64/lib/libFLAC.a +0 -0
- data/assets/windows/mingw-w64-ucrt-x86_64/lib/libSDL2.a +0 -0
- data/assets/windows/mingw-w64-ucrt-x86_64/lib/libSDL2_mixer.a +0 -0
- data/assets/windows/mingw-w64-ucrt-x86_64/lib/libmodplug.a +0 -0
- data/assets/windows/mingw-w64-ucrt-x86_64/lib/libmpg123.a +0 -0
- data/assets/windows/mingw-w64-ucrt-x86_64/lib/libogg.a +0 -0
- data/assets/windows/mingw-w64-ucrt-x86_64/lib/libopus.a +0 -0
- data/assets/windows/mingw-w64-ucrt-x86_64/lib/libopusfile.a +0 -0
- data/assets/windows/mingw-w64-ucrt-x86_64/lib/libsndfile.a +0 -0
- data/assets/windows/mingw-w64-ucrt-x86_64/lib/libstdc++.a +0 -0
- data/assets/windows/mingw-w64-ucrt-x86_64/lib/libvorbis.a +0 -0
- data/assets/windows/mingw-w64-ucrt-x86_64/lib/libvorbisfile.a +0 -0
- data/assets/windows/mingw-w64-ucrt-x86_64/lib/libz.a +0 -0
- data/assets/windows/mingw-w64-x86_64/lib/libFLAC.a +0 -0
- data/assets/windows/mingw-w64-x86_64/lib/libSDL2.a +0 -0
- data/assets/windows/mingw-w64-x86_64/lib/libSDL2_mixer.a +0 -0
- data/assets/windows/mingw-w64-x86_64/lib/libmodplug.a +0 -0
- data/assets/windows/mingw-w64-x86_64/lib/libmpg123.a +0 -0
- data/assets/windows/mingw-w64-x86_64/lib/libogg.a +0 -0
- data/assets/windows/mingw-w64-x86_64/lib/libopus.a +0 -0
- data/assets/windows/mingw-w64-x86_64/lib/libopusfile.a +0 -0
- data/assets/windows/mingw-w64-x86_64/lib/libsndfile.a +0 -0
- data/assets/windows/mingw-w64-x86_64/lib/libstdc++.a +0 -0
- data/assets/windows/mingw-w64-x86_64/lib/libvorbis.a +0 -0
- data/assets/windows/mingw-w64-x86_64/lib/libvorbisfile.a +0 -0
- data/assets/windows/mingw-w64-x86_64/lib/libz.a +0 -0
- data/ext/audio/extconf.h +0 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1f5b017594ea0c5b0b94f2e3c36c057ccdd55275c133f5ffc77ef9dfca5a4b7c
|
|
4
|
+
data.tar.gz: a42b15d0bdf8fa4bfd6f673d3b0f638409653d061e640eb559f433bad2ae6f18
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
3
|
-
#include <
|
|
4
|
-
#include <
|
|
6
|
+
#include <math.h>
|
|
7
|
+
#include <stdlib.h>
|
|
8
|
+
#include <string.h>
|
|
5
9
|
|
|
6
|
-
|
|
7
|
-
|
|
10
|
+
#define MINIAUDIO_IMPLEMENTATION
|
|
11
|
+
#include "miniaudio.h"
|
|
8
12
|
|
|
9
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
26
|
-
|
|
102
|
+
int id = sound_count;
|
|
103
|
+
sounds[id] = sound;
|
|
104
|
+
sound_count++;
|
|
27
105
|
|
|
28
|
-
|
|
106
|
+
return rb_int2inum(id);
|
|
29
107
|
}
|
|
30
108
|
|
|
31
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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
|
-
|
|
52
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
#
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
when /darwin/
|
|
10
|
-
|
|
11
|
-
when /
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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')
|