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.
Files changed (66) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +42 -0
  3. data/CHANGELOG.md +5 -0
  4. data/Gemfile +10 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +388 -0
  7. data/Rakefile +8 -0
  8. data/examples/host_api_info.rb +16 -0
  9. data/examples/level_meter.rb +39 -0
  10. data/examples/list_devices.rb +17 -0
  11. data/examples/passthrough.rb +39 -0
  12. data/examples/record_to_file.rb +42 -0
  13. data/examples/sine_wave.rb +46 -0
  14. data/examples/sine_wave_blocking.rb +45 -0
  15. data/lib/portaudio/blocking_stream.rb +182 -0
  16. data/lib/portaudio/buffer.rb +61 -0
  17. data/lib/portaudio/c/constants.rb +53 -0
  18. data/lib/portaudio/c/enums.rb +84 -0
  19. data/lib/portaudio/c/functions.rb +93 -0
  20. data/lib/portaudio/c/host_api/alsa.rb +40 -0
  21. data/lib/portaudio/c/host_api/asio.rb +51 -0
  22. data/lib/portaudio/c/host_api/core_audio.rb +54 -0
  23. data/lib/portaudio/c/host_api/direct_sound.rb +26 -0
  24. data/lib/portaudio/c/host_api/jack.rb +28 -0
  25. data/lib/portaudio/c/host_api/pulse_audio.rb +28 -0
  26. data/lib/portaudio/c/host_api/wasapi.rb +238 -0
  27. data/lib/portaudio/c/host_api/wave_format.rb +105 -0
  28. data/lib/portaudio/c/host_api/wdm_ks.rb +73 -0
  29. data/lib/portaudio/c/host_api/wmme.rb +54 -0
  30. data/lib/portaudio/c/library.rb +122 -0
  31. data/lib/portaudio/c/structs.rb +66 -0
  32. data/lib/portaudio/configuration.rb +20 -0
  33. data/lib/portaudio/device.rb +87 -0
  34. data/lib/portaudio/error.rb +131 -0
  35. data/lib/portaudio/host_api.rb +89 -0
  36. data/lib/portaudio/stream.rb +177 -0
  37. data/lib/portaudio/stream_parameters_builder.rb +59 -0
  38. data/lib/portaudio/version.rb +5 -0
  39. data/lib/portaudio.rb +79 -0
  40. data/spec/integration/blocking_playback_spec.rb +21 -0
  41. data/spec/integration/callback_playback_spec.rb +26 -0
  42. data/spec/integration/device_enumeration_spec.rb +11 -0
  43. data/spec/integration/recording_spec.rb +25 -0
  44. data/spec/portaudio_spec.rb +43 -0
  45. data/spec/spec_helper.rb +17 -0
  46. data/spec/unit/blocking_stream_spec.rb +66 -0
  47. data/spec/unit/buffer_spec.rb +25 -0
  48. data/spec/unit/c/constants_spec.rb +13 -0
  49. data/spec/unit/c/enums_spec.rb +18 -0
  50. data/spec/unit/c/functions_spec.rb +14 -0
  51. data/spec/unit/c/host_api/asio_spec.rb +14 -0
  52. data/spec/unit/c/host_api/core_audio_spec.rb +18 -0
  53. data/spec/unit/c/host_api/direct_sound_spec.rb +9 -0
  54. data/spec/unit/c/host_api/jack_spec.rb +9 -0
  55. data/spec/unit/c/host_api/pulse_audio_spec.rb +9 -0
  56. data/spec/unit/c/host_api/wasapi_spec.rb +34 -0
  57. data/spec/unit/c/host_api/wave_format_spec.rb +17 -0
  58. data/spec/unit/c/host_api/wdm_ks_spec.rb +13 -0
  59. data/spec/unit/c/host_api/wmme_spec.rb +16 -0
  60. data/spec/unit/c/library_spec.rb +18 -0
  61. data/spec/unit/device_spec.rb +54 -0
  62. data/spec/unit/error_spec.rb +32 -0
  63. data/spec/unit/host_api_spec.rb +53 -0
  64. data/spec/unit/stream_parameters_builder_spec.rb +32 -0
  65. data/spec/unit/stream_spec.rb +76 -0
  66. 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
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ## [0.1.0] - 2026-03-05
4
+
5
+ - Initial release.
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec
6
+
7
+ gem "ffi", ">= 1.15"
8
+ gem "irb"
9
+ gem "rake", "~> 13.0"
10
+ gem "rspec", "~> 3.0"
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,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -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