Module: OpenAI::LocalAudio
- Defined in:
- lib/openai/helpers/local_audio.rb,
lib/openai/helpers/local_audio/errors.rb,
lib/openai/helpers/local_audio/process.rb,
sig/openai/helpers/local_audio/interface.rbs
Overview
Optional local microphone and speaker helpers. Loading this file opens no devices.
Defined Under Namespace
Classes: DependencyError, DeviceError, Error, FormatError, MediaProcess, PlaybackError, TimeoutError, UnsupportedPlatformError
Class Method Summary collapse
- .capture_device(device, platform: RUBY_PLATFORM) ⇒ Object private
-
.play(source, format: :auto, timeout: nil) ⇒ nil
Play encoded audio, or explicitly selected mono 24 kHz raw PCM16.
- .playback_source(source) ⇒ Object private
-
.record(duration:, device: nil, timeout: nil) ⇒ OpenAI::FilePart
Record a finite mono 24 kHz WAV, ready for a multipart audio upload.
- .wav_header(bytes) ⇒ String private
Class Method Details
.capture_device(device, platform: RUBY_PLATFORM) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 |
# File 'lib/openai/helpers/local_audio.rb', line 120 def capture_device(device, platform: RUBY_PLATFORM) unless device.nil? || device.is_a?(String) || (device.is_a?(Integer) && device >= 0) raise ArgumentError, "device must be a device name or nonnegative index" end raise ArgumentError, "invalid device identifier" if device.to_s.include?("\0") || device == "" case platform when /darwin/ ["avfoundation", "none:#{device || "default"}"] when /linux/ ["alsa", device.is_a?(Integer) ? "hw:#{device}" : (device || "default")] when /mswin|mingw|cygwin/ raise ArgumentError, "Windows capture requires an explicit audio device name" unless device.is_a?(String) ["dshow", "audio=#{device}"] else raise UnsupportedPlatformError, "Local capture is not supported on this platform." end end |
.play(source, format: :auto, timeout: nil) ⇒ nil
Play encoded audio, or explicitly selected mono 24 kHz raw PCM16. The source is consumed from its current position and remains caller-owned.
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
# File 'lib/openai/helpers/local_audio.rb', line 67 def play(source, format: :auto, timeout: nil) raise ArgumentError, "format must be :auto or :pcm" unless [:auto, :pcm].include?(format) MediaProcess.duration(timeout, name: :timeout) unless timeout.nil? io = playback_source(source) reader, writer = IO.pipe(binmode: true) child = nil begin # FFplay can exit zero on decoder/device failure. Its owner drains # error output while feeding input, retaining only a boolean. args = ["ffplay", "-autoexit", "-nodisp", "-nostats", "-loglevel", "error", "-protocol_whitelist", "pipe"] args.concat( format == :pcm ? ["-f", "s16le", "-ar", "24000", "-ch_layout", "mono"] : [ "-format_whitelist", "wav,mp3,ogg,aac,flac" ] ) child = MediaProcess.new( args + ["-i", "pipe:0"], input: reader, output: File::NULL, capture_errors: true, timeout: timeout ) reader.close bytes = 0 while (chunk = child.read(io, stop_on_exit: true)) break if chunk.empty? bytes += chunk.bytesize child.write(writer, chunk) end raise PlaybackError, "Audio input is empty." if bytes.zero? raise FormatError, "PCM input ends with an incomplete sample." if format == :pcm && bytes.odd? writer.close status = child.wait raise PlaybackError, "Audio playback failed." unless status.success? && !child.errors? nil ensure reader.close unless reader.closed? writer.close unless writer.closed? child&.close end end |
.playback_source(source) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
112 113 114 115 116 117 |
# File 'lib/openai/helpers/local_audio.rb', line 112 def playback_source(source) content = source.is_a?(OpenAI::FilePart) ? source.content : source return content if content.is_a?(IO) || content.is_a?(StringIO) return StringIO.new(content) if source.is_a?(OpenAI::FilePart) && content.is_a?(String) raise ArgumentError, "source must be IO, StringIO, or a FilePart containing IO or bytes" end |
.record(duration:, device: nil, timeout: nil) ⇒ OpenAI::FilePart
Record a finite mono 24 kHz WAV, ready for a multipart audio upload.
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
# File 'lib/openai/helpers/local_audio.rb', line 13 def record(duration:, device: nil, timeout: nil) seconds = MediaProcess.duration(duration, name: :duration) MediaProcess.duration(timeout, name: :timeout) unless timeout.nil? format, source = capture_device(device) reader, writer = IO.pipe(binmode: true) child = nil begin child = MediaProcess.new( [ "ffmpeg", "-nostdin", "-hide_banner", "-loglevel", "quiet", "-f", format, "-i", source, "-t", seconds.to_s, "-ar", "24000", "-ac", "1", "-acodec", "pcm_s16le", "-f", "s16le", "pipe:1" ], input: File::NULL, output: writer, timeout: timeout ) writer.close pcm = +"".b while (chunk = child.read(reader)) pcm << chunk end raise DeviceError, "Microphone capture failed." unless child.wait.success? raise DeviceError, "Microphone returned no complete audio samples." if pcm.empty? || pcm.bytesize.odd? audio = StringIO.new(wav_header(pcm.bytesize) << pcm) OpenAI::FilePart.new(audio, filename: "audio.wav", content_type: "audio/wav") ensure reader.close unless reader.closed? writer.close unless writer.closed? child&.close end end |
.wav_header(bytes) ⇒ String
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 |
# File 'lib/openai/helpers/local_audio.rb', line 140 def wav_header(bytes) format = "fmt " + [16, 1, 1, 24_000, 48_000, 2, 16].pack("VvvVVvv") if bytes <= 0xFFFFFFFF - 36 "RIFF" + [bytes + 36].pack("V") + "WAVE" + format + "data" + [bytes].pack("V") else "RF64" + [0xFFFFFFFF].pack("V") + "WAVEds64" + [28].pack("V") + [bytes + 72, bytes, bytes / 2, 0].pack("Q<Q<Q<V") + format + "data" + [0xFFFFFFFF].pack("V") end end |