portaudio 0.1.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 +7 -0
- data/.github/workflows/ci.yml +42 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +388 -0
- data/Rakefile +8 -0
- data/examples/host_api_info.rb +16 -0
- data/examples/level_meter.rb +39 -0
- data/examples/list_devices.rb +17 -0
- data/examples/passthrough.rb +39 -0
- data/examples/record_to_file.rb +42 -0
- data/examples/sine_wave.rb +46 -0
- data/examples/sine_wave_blocking.rb +45 -0
- data/lib/portaudio/blocking_stream.rb +182 -0
- data/lib/portaudio/buffer.rb +61 -0
- data/lib/portaudio/c/constants.rb +53 -0
- data/lib/portaudio/c/enums.rb +84 -0
- data/lib/portaudio/c/functions.rb +93 -0
- data/lib/portaudio/c/host_api/alsa.rb +40 -0
- data/lib/portaudio/c/host_api/asio.rb +51 -0
- data/lib/portaudio/c/host_api/core_audio.rb +54 -0
- data/lib/portaudio/c/host_api/direct_sound.rb +26 -0
- data/lib/portaudio/c/host_api/jack.rb +28 -0
- data/lib/portaudio/c/host_api/pulse_audio.rb +28 -0
- data/lib/portaudio/c/host_api/wasapi.rb +238 -0
- data/lib/portaudio/c/host_api/wave_format.rb +105 -0
- data/lib/portaudio/c/host_api/wdm_ks.rb +73 -0
- data/lib/portaudio/c/host_api/wmme.rb +54 -0
- data/lib/portaudio/c/library.rb +122 -0
- data/lib/portaudio/c/structs.rb +66 -0
- data/lib/portaudio/configuration.rb +20 -0
- data/lib/portaudio/device.rb +87 -0
- data/lib/portaudio/error.rb +131 -0
- data/lib/portaudio/host_api.rb +89 -0
- data/lib/portaudio/stream.rb +177 -0
- data/lib/portaudio/stream_parameters_builder.rb +59 -0
- data/lib/portaudio/version.rb +5 -0
- data/lib/portaudio.rb +79 -0
- data/spec/integration/blocking_playback_spec.rb +21 -0
- data/spec/integration/callback_playback_spec.rb +26 -0
- data/spec/integration/device_enumeration_spec.rb +11 -0
- data/spec/integration/recording_spec.rb +25 -0
- data/spec/portaudio_spec.rb +43 -0
- data/spec/spec_helper.rb +17 -0
- data/spec/unit/blocking_stream_spec.rb +66 -0
- data/spec/unit/buffer_spec.rb +25 -0
- data/spec/unit/c/constants_spec.rb +13 -0
- data/spec/unit/c/enums_spec.rb +18 -0
- data/spec/unit/c/functions_spec.rb +14 -0
- data/spec/unit/c/host_api/asio_spec.rb +14 -0
- data/spec/unit/c/host_api/core_audio_spec.rb +18 -0
- data/spec/unit/c/host_api/direct_sound_spec.rb +9 -0
- data/spec/unit/c/host_api/jack_spec.rb +9 -0
- data/spec/unit/c/host_api/pulse_audio_spec.rb +9 -0
- data/spec/unit/c/host_api/wasapi_spec.rb +34 -0
- data/spec/unit/c/host_api/wave_format_spec.rb +17 -0
- data/spec/unit/c/host_api/wdm_ks_spec.rb +13 -0
- data/spec/unit/c/host_api/wmme_spec.rb +16 -0
- data/spec/unit/c/library_spec.rb +18 -0
- data/spec/unit/device_spec.rb +54 -0
- data/spec/unit/error_spec.rb +32 -0
- data/spec/unit/host_api_spec.rb +53 -0
- data/spec/unit/stream_parameters_builder_spec.rb +32 -0
- data/spec/unit/stream_spec.rb +76 -0
- metadata +122 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 4a1f4649d2439f0148beb887fac7bfa2c5d52f769c246381212a145b7cb26ef1
|
|
4
|
+
data.tar.gz: ddb39d4f70a5e3e7263c38162809955c6b0d88e5b745565b70085e7e011f8f3e
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: '0967e0db0a6e6d00573bba8e39f73c70956f61daaaee09f2ed514c7f2be74d4bb56dd524e5bae22d8488be25dca04ba1f6d304a5f195bcfdf8fa26e9df935a3e'
|
|
7
|
+
data.tar.gz: ccc47eef65f9ef870cf8f0c5e9f178401488dd38a943fd68044e4a3c0546cf7b37ee9efab6a0b70c8602d6e6c01e2c4c28897790aa7979199158a995330ade68
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: [main]
|
|
5
|
+
pull_request:
|
|
6
|
+
jobs:
|
|
7
|
+
test:
|
|
8
|
+
env:
|
|
9
|
+
PORTAUDIO_RUN_INTEGRATION: "1"
|
|
10
|
+
strategy:
|
|
11
|
+
fail-fast: false
|
|
12
|
+
matrix:
|
|
13
|
+
os:
|
|
14
|
+
- ubuntu-latest
|
|
15
|
+
- macos-latest
|
|
16
|
+
ruby:
|
|
17
|
+
- '3.1'
|
|
18
|
+
- '3.2'
|
|
19
|
+
- '3.3'
|
|
20
|
+
- '3.4'
|
|
21
|
+
- '4.0'
|
|
22
|
+
- 'head'
|
|
23
|
+
runs-on: ${{ matrix.os }}
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v6
|
|
26
|
+
with:
|
|
27
|
+
persist-credentials: false
|
|
28
|
+
- name: Set up Ruby
|
|
29
|
+
uses: ruby/setup-ruby@v1
|
|
30
|
+
with:
|
|
31
|
+
ruby-version: ${{ matrix.ruby }}
|
|
32
|
+
bundler-cache: true
|
|
33
|
+
- name: Install PortAudio (Linux)
|
|
34
|
+
if: runner.os == 'Linux'
|
|
35
|
+
run: |
|
|
36
|
+
sudo apt-get update
|
|
37
|
+
sudo apt-get install -y libportaudio2 portaudio19-dev
|
|
38
|
+
- name: Install PortAudio (macOS)
|
|
39
|
+
if: runner.os == 'macOS'
|
|
40
|
+
run: brew install portaudio
|
|
41
|
+
- name: Run test suite
|
|
42
|
+
run: bundle exec rspec
|
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yudai Takada
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
# portaudio
|
|
2
|
+
|
|
3
|
+
`portaudio` is a Ruby FFI binding for PortAudio V19.
|
|
4
|
+
|
|
5
|
+
It exposes the low-level PortAudio API under `PortAudio::C` and a small set of Ruby wrappers for common tasks such as device discovery, host API discovery, blocking streams, callback streams, and buffer conversion.
|
|
6
|
+
|
|
7
|
+
## Public API overview
|
|
8
|
+
|
|
9
|
+
| API | Purpose |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| `PortAudio` | Lifecycle helpers such as `init`, `terminate`, `with_portaudio`, and `check_error!` |
|
|
12
|
+
| `PortAudio::Device` | Enumerate devices, inspect defaults, and search by name |
|
|
13
|
+
| `PortAudio::HostAPI` | Enumerate host APIs and access their devices |
|
|
14
|
+
| `PortAudio::BlockingStream` | Blocking `read` / `write` streams |
|
|
15
|
+
| `PortAudio::Stream` | Callback-based streams |
|
|
16
|
+
| `PortAudio::Buffer` | Convert between raw PortAudio bytes and Ruby float arrays |
|
|
17
|
+
| `PortAudio::C` | Low-level functions, constants, enums, and structs |
|
|
18
|
+
| `PortAudio::C::HostAPI` | Low-level host API extension bindings |
|
|
19
|
+
|
|
20
|
+
`Portaudio` remains available as a compatibility alias for `PortAudio`.
|
|
21
|
+
|
|
22
|
+
## Requirements
|
|
23
|
+
|
|
24
|
+
- Ruby `>= 3.1`
|
|
25
|
+
- `ffi >= 1.15`
|
|
26
|
+
- A PortAudio shared library installed on the system
|
|
27
|
+
|
|
28
|
+
## Install PortAudio
|
|
29
|
+
|
|
30
|
+
Install the runtime and development files first:
|
|
31
|
+
|
|
32
|
+
- macOS: `brew install portaudio`
|
|
33
|
+
- Ubuntu/Debian: `sudo apt-get install -y libportaudio2 portaudio19-dev`
|
|
34
|
+
- Windows (RubyInstaller2 UCRT): `ridk exec pacman -S mingw-w64-ucrt-x86_64-portaudio`
|
|
35
|
+
- Windows (RubyInstaller2 MINGW): `ridk exec pacman -S mingw-w64-x86_64-portaudio`
|
|
36
|
+
|
|
37
|
+
On Windows, make one of these DLLs available on `PATH`:
|
|
38
|
+
|
|
39
|
+
- `portaudio.dll`
|
|
40
|
+
- `libportaudio.dll`
|
|
41
|
+
- `libportaudio-2.dll`
|
|
42
|
+
- `portaudio_x64.dll`
|
|
43
|
+
|
|
44
|
+
You can also point the gem at a specific library file with `PORTAUDIO_LIBRARY_PATH` or `PortAudio::Configuration.library_path`.
|
|
45
|
+
|
|
46
|
+
## Install the gem
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
bundle add portaudio
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
or:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
gem install portaudio
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Configuration
|
|
59
|
+
|
|
60
|
+
The shared library is loaded lazily on first use. If loading fails, the gem raises `PortAudio::LibraryNotLoadedError`.
|
|
61
|
+
|
|
62
|
+
### Override the library path
|
|
63
|
+
|
|
64
|
+
Use an environment variable:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
export PORTAUDIO_LIBRARY_PATH=/custom/path/libportaudio.so
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
or configure it in Ruby:
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
PortAudio::Configuration.library_path = "/custom/path/libportaudio.so"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Set the path before the first PortAudio call when possible.
|
|
77
|
+
|
|
78
|
+
### Warning handling for blocking streams
|
|
79
|
+
|
|
80
|
+
`PortAudio::BlockingStream#read` and `#write` treat overflow/underflow warning codes specially.
|
|
81
|
+
|
|
82
|
+
- `PortAudio::Configuration.strict_warnings = false` is the default
|
|
83
|
+
- When strict mode is off, warning codes are stored in `stream.last_warning`
|
|
84
|
+
- When strict mode is on, warning codes raise normal Ruby exceptions such as `PortAudio::InputOverflowError`
|
|
85
|
+
|
|
86
|
+
You can set this globally:
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
PortAudio::Configuration.strict_warnings = true
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
or per call:
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
chunk = stream.read(256, strict: false)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Lifecycle
|
|
99
|
+
|
|
100
|
+
Use `PortAudio.with_portaudio` for normal code. It initializes PortAudio, yields to your block, and terminates it in `ensure`.
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
PortAudio.with_portaudio do
|
|
104
|
+
puts PortAudio.initialized? # => true
|
|
105
|
+
end
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`PortAudio.init` / `PortAudio.terminate` are reference-counted, so nested usage is safe.
|
|
109
|
+
|
|
110
|
+
## Device and host API discovery
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
PortAudio.with_portaudio do
|
|
114
|
+
puts "Host APIs:"
|
|
115
|
+
PortAudio::HostAPI.all.each do |host_api|
|
|
116
|
+
puts "#{host_api.index}: #{host_api.name} (#{host_api.type_symbol})"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
puts
|
|
120
|
+
puts "Devices:"
|
|
121
|
+
PortAudio::Device.all.each do |device|
|
|
122
|
+
puts [
|
|
123
|
+
"#{device.index}: #{device.name}",
|
|
124
|
+
"host_api=#{device.host_api.name}",
|
|
125
|
+
"in=#{device.max_input_channels}",
|
|
126
|
+
"out=#{device.max_output_channels}",
|
|
127
|
+
"rate=#{device.default_sample_rate}"
|
|
128
|
+
].join(" ")
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
default_input = PortAudio::Device.default_input
|
|
132
|
+
default_output = PortAudio::Device.default_output
|
|
133
|
+
|
|
134
|
+
p default_input
|
|
135
|
+
p default_output
|
|
136
|
+
end
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Useful helpers:
|
|
140
|
+
|
|
141
|
+
- `PortAudio::Device.count`
|
|
142
|
+
- `PortAudio::Device.find_by_name("built-in")`
|
|
143
|
+
- `PortAudio::HostAPI.default`
|
|
144
|
+
- `PortAudio::HostAPI.find_by_type(:paCoreAudio)` or another PortAudio host API type symbol
|
|
145
|
+
|
|
146
|
+
## Stream parameter hashes
|
|
147
|
+
|
|
148
|
+
Both `PortAudio::BlockingStream` and `PortAudio::Stream` accept `input:` and `output:` hashes with the same shape:
|
|
149
|
+
|
|
150
|
+
```ruby
|
|
151
|
+
{
|
|
152
|
+
device: device_or_index,
|
|
153
|
+
channels: 2,
|
|
154
|
+
format: :float32,
|
|
155
|
+
latency: 0.0,
|
|
156
|
+
host_api_stream_info: pointer_or_struct
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Notes:
|
|
161
|
+
|
|
162
|
+
- `device` accepts either a `PortAudio::Device` object or a device index
|
|
163
|
+
- `channels` defaults to `1`
|
|
164
|
+
- `format` defaults to `:float32`
|
|
165
|
+
- `latency` defaults to `0.0`
|
|
166
|
+
- `host_api_stream_info` must be `FFI::Pointer`-compatible
|
|
167
|
+
|
|
168
|
+
Supported sample format symbols:
|
|
169
|
+
|
|
170
|
+
- `:float32`
|
|
171
|
+
- `:int32`
|
|
172
|
+
- `:int24`
|
|
173
|
+
- `:int16`
|
|
174
|
+
- `:int8`
|
|
175
|
+
- `:uint8`
|
|
176
|
+
|
|
177
|
+
## Blocking streams
|
|
178
|
+
|
|
179
|
+
Use `PortAudio::BlockingStream` for explicit reads and writes.
|
|
180
|
+
|
|
181
|
+
### Write audio to the default output device
|
|
182
|
+
|
|
183
|
+
```ruby
|
|
184
|
+
PortAudio.with_portaudio do
|
|
185
|
+
output = PortAudio::Device.default_output
|
|
186
|
+
raise "No default output device" unless output
|
|
187
|
+
|
|
188
|
+
stream = PortAudio::BlockingStream.new(
|
|
189
|
+
output: { device: output, channels: 1, format: :float32 },
|
|
190
|
+
sample_rate: output.default_sample_rate,
|
|
191
|
+
frames_per_buffer: 256
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
samples = Array.new(256, 0.0)
|
|
195
|
+
|
|
196
|
+
stream.start
|
|
197
|
+
100.times { stream.write(samples) }
|
|
198
|
+
stream.stop
|
|
199
|
+
stream.close
|
|
200
|
+
end
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
`write` accepts either:
|
|
204
|
+
|
|
205
|
+
- A packed binary `String`
|
|
206
|
+
- An `Array` of sample values, which is converted according to the configured output format
|
|
207
|
+
|
|
208
|
+
### Read input and decode it with `PortAudio::Buffer`
|
|
209
|
+
|
|
210
|
+
```ruby
|
|
211
|
+
PortAudio.with_portaudio do
|
|
212
|
+
input = PortAudio::Device.default_input
|
|
213
|
+
raise "No default input device" unless input
|
|
214
|
+
|
|
215
|
+
stream = PortAudio::BlockingStream.new(
|
|
216
|
+
input: { device: input, channels: 1, format: :float32 },
|
|
217
|
+
sample_rate: input.default_sample_rate,
|
|
218
|
+
frames_per_buffer: 128
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
stream.start
|
|
222
|
+
raw = stream.read(128)
|
|
223
|
+
stream.stop
|
|
224
|
+
stream.close
|
|
225
|
+
|
|
226
|
+
buffer = PortAudio::Buffer.new(frames: 128, channels: 1, format: :float32)
|
|
227
|
+
buffer.pointer.put_bytes(0, raw)
|
|
228
|
+
|
|
229
|
+
p buffer.to_float_array.first(8)
|
|
230
|
+
end
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
`read` returns raw bytes. `PortAudio::Buffer` is the convenience wrapper for converting those bytes into floats.
|
|
234
|
+
|
|
235
|
+
## Callback streams
|
|
236
|
+
|
|
237
|
+
Use `PortAudio::Stream` when PortAudio should pull audio from a callback.
|
|
238
|
+
|
|
239
|
+
```ruby
|
|
240
|
+
PortAudio.with_portaudio do
|
|
241
|
+
output = PortAudio::Device.default_output
|
|
242
|
+
raise "No default output device" unless output
|
|
243
|
+
remaining_frames = (output.default_sample_rate * 0.2).round
|
|
244
|
+
|
|
245
|
+
stream = PortAudio::Stream.new(
|
|
246
|
+
output: { device: output, channels: 1, format: :float32 },
|
|
247
|
+
sample_rate: output.default_sample_rate,
|
|
248
|
+
frames_per_buffer: 128
|
|
249
|
+
) do |_input_ptr, output_ptr, frame_count, _time_info, _status_flags, _user_data|
|
|
250
|
+
samples = Array.new(frame_count) do
|
|
251
|
+
remaining_frames -= 1 if remaining_frames.positive?
|
|
252
|
+
0.0
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
output_ptr.put_array_of_float(0, samples)
|
|
256
|
+
remaining_frames.zero? ? :complete : :continue
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
stream.on_finished { puts "stream finished" }
|
|
260
|
+
|
|
261
|
+
stream.start
|
|
262
|
+
sleep 0.1 while stream.active?
|
|
263
|
+
stream.close
|
|
264
|
+
|
|
265
|
+
raise stream.callback_error if stream.callback_error
|
|
266
|
+
end
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Callback return values can be either PortAudio symbols or the normalized Ruby aliases:
|
|
270
|
+
|
|
271
|
+
- `:paContinue` or `:continue`
|
|
272
|
+
- `:paComplete` or `:complete`
|
|
273
|
+
- `:paAbort` or `:abort`
|
|
274
|
+
|
|
275
|
+
If the callback raises, the stream aborts and the exception is stored in `stream.callback_error`.
|
|
276
|
+
|
|
277
|
+
### `Stream.open` convenience helper
|
|
278
|
+
|
|
279
|
+
Use `PortAudio::Stream.open` to start a stream inside a block and always close it afterward:
|
|
280
|
+
|
|
281
|
+
```ruby
|
|
282
|
+
PortAudio.with_portaudio do
|
|
283
|
+
output = PortAudio::Device.default_output
|
|
284
|
+
raise "No default output device" unless output
|
|
285
|
+
remaining_frames = (output.default_sample_rate * 0.2).round
|
|
286
|
+
|
|
287
|
+
callback = proc do |_input_ptr, output_ptr, frame_count, *_rest|
|
|
288
|
+
samples = Array.new(frame_count) do
|
|
289
|
+
remaining_frames -= 1 if remaining_frames.positive?
|
|
290
|
+
0.0
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
output_ptr.put_array_of_float(0, samples)
|
|
294
|
+
remaining_frames.zero? ? :complete : :continue
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
PortAudio::Stream.open(
|
|
298
|
+
output: { device: output, channels: 1, format: :float32 },
|
|
299
|
+
sample_rate: output.default_sample_rate,
|
|
300
|
+
frames_per_buffer: 128,
|
|
301
|
+
callback: callback
|
|
302
|
+
) do |stream|
|
|
303
|
+
stream.start
|
|
304
|
+
sleep 0.1 while stream.active?
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
## Buffer helper
|
|
310
|
+
|
|
311
|
+
`PortAudio::Buffer` allocates a PortAudio-sized memory region and exposes a few convenience methods:
|
|
312
|
+
|
|
313
|
+
- `pointer`
|
|
314
|
+
- `bytesize`
|
|
315
|
+
- `write_floats`
|
|
316
|
+
- `to_float_array`
|
|
317
|
+
- `clear`
|
|
318
|
+
|
|
319
|
+
`to_float_array` and `write_floats` currently support `:float32`, `:int16`, and `:int32`.
|
|
320
|
+
|
|
321
|
+
## Errors
|
|
322
|
+
|
|
323
|
+
All negative `PaError` codes are mapped to Ruby exceptions through `PortAudio.check_error!`.
|
|
324
|
+
|
|
325
|
+
Common examples:
|
|
326
|
+
|
|
327
|
+
- `PortAudio::InvalidDeviceError`
|
|
328
|
+
- `PortAudio::FormatNotSupportedError`
|
|
329
|
+
- `PortAudio::DeviceUnavailableError`
|
|
330
|
+
- `PortAudio::InputOverflowError`
|
|
331
|
+
- `PortAudio::OutputUnderflowError`
|
|
332
|
+
- `PortAudio::HostApiError`
|
|
333
|
+
|
|
334
|
+
`PortAudio::HostApiError` also exposes:
|
|
335
|
+
|
|
336
|
+
- `code`
|
|
337
|
+
- `host_error_info`
|
|
338
|
+
|
|
339
|
+
If you call low-level functions directly, use `PortAudio.check_error!(code)` on the return value.
|
|
340
|
+
|
|
341
|
+
## Low-level bindings
|
|
342
|
+
|
|
343
|
+
The gem exposes low-level PortAudio bindings under:
|
|
344
|
+
|
|
345
|
+
- `PortAudio::C::Functions`
|
|
346
|
+
- `PortAudio::C::Constants`
|
|
347
|
+
- `PortAudio::C::Enums`
|
|
348
|
+
- `PortAudio::C::Structs`
|
|
349
|
+
|
|
350
|
+
Platform-specific host API extension bindings are available under `PortAudio::C::HostAPI`:
|
|
351
|
+
|
|
352
|
+
- `ALSA`
|
|
353
|
+
- `ASIO`
|
|
354
|
+
- `CoreAudio`
|
|
355
|
+
- `DirectSound`
|
|
356
|
+
- `JACK`
|
|
357
|
+
- `PulseAudio`
|
|
358
|
+
- `WASAPI`
|
|
359
|
+
- `WDMKS`
|
|
360
|
+
- `WaveFormat`
|
|
361
|
+
- `WMME`
|
|
362
|
+
|
|
363
|
+
If the loaded PortAudio library does not export a specific extension symbol, calling that Ruby method raises `PortAudio::UnsupportedFunctionError`.
|
|
364
|
+
|
|
365
|
+
## Development
|
|
366
|
+
|
|
367
|
+
Install dependencies:
|
|
368
|
+
|
|
369
|
+
```bash
|
|
370
|
+
bundle install
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
Run the default test suite:
|
|
374
|
+
|
|
375
|
+
```bash
|
|
376
|
+
bundle exec rspec
|
|
377
|
+
bundle exec rake
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
Integration specs use real audio hardware and are opt-in:
|
|
381
|
+
|
|
382
|
+
```bash
|
|
383
|
+
PORTAUDIO_RUN_INTEGRATION=1 bundle exec rspec spec/integration
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
## License
|
|
387
|
+
|
|
388
|
+
MIT
|
data/Rakefile
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "portaudio"
|
|
4
|
+
|
|
5
|
+
PortAudio.with_portaudio do
|
|
6
|
+
apis = PortAudio::HostAPI.all
|
|
7
|
+
|
|
8
|
+
if apis.empty?
|
|
9
|
+
warn "No host APIs detected"
|
|
10
|
+
next
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
apis.each do |api|
|
|
14
|
+
puts "#{api.index}: #{api.name} (#{api.type}) devices=#{api.device_count}"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "portaudio"
|
|
4
|
+
|
|
5
|
+
duration_seconds = 1.0
|
|
6
|
+
|
|
7
|
+
PortAudio.with_portaudio do
|
|
8
|
+
input = PortAudio::Device.default_input
|
|
9
|
+
unless input
|
|
10
|
+
warn "No default input device available"
|
|
11
|
+
next
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
frames = 256
|
|
15
|
+
remaining_frames = (input.default_sample_rate * duration_seconds).round
|
|
16
|
+
|
|
17
|
+
stream = PortAudio::BlockingStream.new(
|
|
18
|
+
input: { device: input.index, channels: 1, format: :float32 },
|
|
19
|
+
sample_rate: input.default_sample_rate,
|
|
20
|
+
frames_per_buffer: frames
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
begin
|
|
24
|
+
stream.start
|
|
25
|
+
|
|
26
|
+
while remaining_frames.positive?
|
|
27
|
+
frame_count = [remaining_frames, frames].min
|
|
28
|
+
raw = stream.read(frame_count)
|
|
29
|
+
samples = raw.unpack("e*")
|
|
30
|
+
peak = samples.map(&:abs).max || 0.0
|
|
31
|
+
bars = (peak * 50).round
|
|
32
|
+
puts format("%0.3f %s", peak, "#" * bars)
|
|
33
|
+
remaining_frames -= frame_count
|
|
34
|
+
end
|
|
35
|
+
ensure
|
|
36
|
+
stream.stop if stream.active?
|
|
37
|
+
stream.close unless stream.closed?
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "portaudio"
|
|
4
|
+
|
|
5
|
+
PortAudio.with_portaudio do
|
|
6
|
+
devices = PortAudio::Device.all
|
|
7
|
+
|
|
8
|
+
if devices.empty?
|
|
9
|
+
warn "No audio devices detected"
|
|
10
|
+
next
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
devices.each do |device|
|
|
14
|
+
puts format("%3d %-40s in=%2d out=%2d rate=%0.1f", device.index, device.name, device.max_input_channels,
|
|
15
|
+
device.max_output_channels, device.default_sample_rate)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "portaudio"
|
|
4
|
+
|
|
5
|
+
duration_seconds = 1.0
|
|
6
|
+
|
|
7
|
+
PortAudio.with_portaudio do
|
|
8
|
+
input = PortAudio::Device.default_input
|
|
9
|
+
output = PortAudio::Device.default_output
|
|
10
|
+
unless input && output
|
|
11
|
+
warn "No default input/output device available"
|
|
12
|
+
next
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
sample_rate = [input.default_sample_rate, output.default_sample_rate].min
|
|
16
|
+
frames = 256
|
|
17
|
+
remaining_frames = (sample_rate * duration_seconds).round
|
|
18
|
+
|
|
19
|
+
stream = PortAudio::BlockingStream.new(
|
|
20
|
+
input: { device: input.index, channels: 1, format: :float32 },
|
|
21
|
+
output: { device: output.index, channels: 1, format: :float32 },
|
|
22
|
+
sample_rate: sample_rate,
|
|
23
|
+
frames_per_buffer: frames
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
begin
|
|
27
|
+
stream.start
|
|
28
|
+
|
|
29
|
+
while remaining_frames.positive?
|
|
30
|
+
frame_count = [remaining_frames, frames].min
|
|
31
|
+
data = stream.read(frame_count)
|
|
32
|
+
stream.write(data, frame_count)
|
|
33
|
+
remaining_frames -= frame_count
|
|
34
|
+
end
|
|
35
|
+
ensure
|
|
36
|
+
stream.stop if stream.active?
|
|
37
|
+
stream.close unless stream.closed?
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "portaudio"
|
|
4
|
+
|
|
5
|
+
duration_seconds = 1.0
|
|
6
|
+
output_path = "recording.raw.f32"
|
|
7
|
+
|
|
8
|
+
PortAudio.with_portaudio do
|
|
9
|
+
input = PortAudio::Device.default_input
|
|
10
|
+
unless input
|
|
11
|
+
warn "No default input device available"
|
|
12
|
+
next
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
sample_rate = input.default_sample_rate
|
|
16
|
+
frames = 256
|
|
17
|
+
remaining_frames = (sample_rate * duration_seconds).round
|
|
18
|
+
|
|
19
|
+
stream = PortAudio::BlockingStream.new(
|
|
20
|
+
input: { device: input.index, channels: 1, format: :float32 },
|
|
21
|
+
sample_rate: sample_rate,
|
|
22
|
+
frames_per_buffer: frames
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
raw = +""
|
|
26
|
+
|
|
27
|
+
begin
|
|
28
|
+
stream.start
|
|
29
|
+
|
|
30
|
+
while remaining_frames.positive?
|
|
31
|
+
frame_count = [remaining_frames, frames].min
|
|
32
|
+
raw << stream.read(frame_count)
|
|
33
|
+
remaining_frames -= frame_count
|
|
34
|
+
end
|
|
35
|
+
ensure
|
|
36
|
+
stream.stop if stream.active?
|
|
37
|
+
stream.close unless stream.closed?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
File.binwrite(output_path, raw)
|
|
41
|
+
puts "Wrote #{output_path} (#{raw.bytesize} bytes)"
|
|
42
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "portaudio"
|
|
4
|
+
|
|
5
|
+
frequency = 440.0
|
|
6
|
+
phase = 0.0
|
|
7
|
+
duration_ms = 2_000
|
|
8
|
+
|
|
9
|
+
PortAudio.with_portaudio do
|
|
10
|
+
output = PortAudio::Device.default_output
|
|
11
|
+
unless output
|
|
12
|
+
warn "No default output device available"
|
|
13
|
+
next
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
sample_rate = output.default_sample_rate
|
|
17
|
+
remaining_frames = (sample_rate * (duration_ms / 1000.0)).round
|
|
18
|
+
|
|
19
|
+
stream = PortAudio::Stream.new(
|
|
20
|
+
output: { device: output.index, channels: 1, format: :float32 },
|
|
21
|
+
sample_rate: sample_rate,
|
|
22
|
+
frames_per_buffer: 256
|
|
23
|
+
) do |_in_ptr, out_ptr, frame_count, _time_info, _status, _user_data|
|
|
24
|
+
samples = Array.new(frame_count) do
|
|
25
|
+
next 0.0 unless remaining_frames.positive?
|
|
26
|
+
|
|
27
|
+
value = Math.sin(phase * 2.0 * Math::PI)
|
|
28
|
+
phase += frequency / sample_rate
|
|
29
|
+
phase -= 1.0 if phase >= 1.0
|
|
30
|
+
remaining_frames -= 1
|
|
31
|
+
value
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
out_ptr.put_array_of_float(0, samples)
|
|
35
|
+
remaining_frames.zero? ? :paComplete : :paContinue
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
begin
|
|
39
|
+
stream.start
|
|
40
|
+
sleep 0.1 while stream.active?
|
|
41
|
+
raise stream.callback_error if stream.callback_error
|
|
42
|
+
ensure
|
|
43
|
+
stream.abort if stream.active?
|
|
44
|
+
stream.close unless stream.closed?
|
|
45
|
+
end
|
|
46
|
+
end
|