1
2
3
4
5
6
7
8
9
10
11
12
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
63
64
65
66
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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
| #### RemiAudio
#### Copyright (C) 2024 Remilia Scarlet <remilia@posteo.jp>
####
#### This program is free software: you can redistribute it and/or modify it
#### under the terms of the GNU Affero General Public License as published by
#### the Free Software Foundation, either version 3 of the License, or (at your
#### option) any later version.
####
#### This program is distributed in the hope that it will be useful, but WITHOUT
#### ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
#### FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
#### License for more details.
####
#### You should have received a copy of the GNU Affero General Public License
#### along with this program. If not, see <https://www.gnu.org/licenses/>.
require "./ao-lib"
####
#### libao Device
####
module RemiAudio::Drivers::Ao
# The `AoDevice` class is used to playback audio using libao.
#
# As libao does not support output to Float32 directly, this class will
# convert the audio to the requested bit rate on-the-fly. It does this
# without any dithering. Additionally, only 8-bit, 16-bit, and 24-bit
# output is supported by this device.
class AoDevice < RemiAudio::Drivers::AudioDevice
# The output type of the underlying libao driver.
enum DriverType
# Live audio output.
Live = 1
# Output to a file.
File = 2
end
# The endianness of the samples.
enum Format
# Little endian samples.
Little = 1
# Big endian samples.
Big = 2
# Native endianness samples.
Native = 4
end
##
## Fields that can be set
##
# The driver ID that will be requested when `#start` is called. This
# cannot be changed once `#start` has been called. You can use
# `AoDevice.getDriverId` to get the numeric ID for a driver.
#
# # You can read more about libao drivers
# [here](https://www.xiph.org/ao/doc/drivers.html).
getter driverID : Int32 = 0
# The layout of the channels. This cannot be changed once `#start` has
# been called.
getter matrix : String = "L,R"
##
## Informational Fields
##
# The output type of the underlying libao driver.
getter type : DriverType = DriverType::Live
# The short name of the driver. This is populated after calling `#start`.
getter name : String = ""
# The short name of the driver. This is populated after calling `#start`.
getter shortName : String = ""
# The author of the driver. This is populated after calling `#start`.
getter author : String = ""
# A comment about the driver. This is populated after calling `#start`.
getter comment : String = ""
# The preferred byte ordering (endianness) for samples. Ideally,
# samples sent to `#writeBuffer` will be in the same format, but they
# don't have to be. Using samples of a different endianness may result
# in slightly higher CPU and memory usage by libao. This is populated
# after calling `#start`.
getter preferredByteFormat : Format = Format::Native
# A positive integer ranking how likely it is for this driver to be the default.
getter priority : Int32 = 0
# The possible options for thsi driver. This isn't used directly by
# RemiAudio at the moment, and is purely informational. This is
# populated after calling `#start`.
getter availableOptions : Array(String) = [] of String
##
## Internal Fields
##
@devicePtr : Libao::PDevice = Libao::PDevice.null
@foreignBuf : Array(UInt8)|Array(Int16) = Array(UInt8).new(0, 0u8)
@sampleMult : Int32 = 0
# Creates a new `AoDevice` instance.
#
# Note that only 8-bit, 16-bit, and 24-bit samples are supported, so
# **newBitDepth** must be one of these values.
def initialize(newSampleRate : Int, newBitDepth : Int, newChannels : Int)
@sampleRate = newSampleRate.to_u32.as(UInt32)
@bitDepth = newBitDepth.to_u8.as(UInt8)
@channels = newChannels.to_u8.as(UInt8)
@expectedBufferSize = @bufferSize * @channels
@outputSize = @expectedBufferSize * sizeof(Float32)
if @expectedBufferSize > Int32::MAX - 128
raise AudioDeviceError.new("Buffer size way too big")
end
end
# Attempts to lookup the numeric driver ID for a given driver. **name**
# should be the driver's short name. If the driver is not known, this
# returns nil.
#
# You can read more about libao drivers
# [here](https://www.xiph.org/ao/doc/drivers.html).
def self.getDriverID(name : String) : Int32?
ret = Libao.ao_driver_id(name.to_unsafe)
ret == -1 ? nil : ret
end
# Sets the numeric driver ID that will be used when `#start` is called.
# This cannot be changed after `#start` is called.
#
# # You can read more about libao drivers
# [here](https://www.xiph.org/ao/doc/drivers.html).
def driverID=(id : Int32) : Nil
if @started
raise AudioDeviceError.new("Cannot change the driver ID after the device has been started")
end
@driverID = value
end
# Sets the channel layout. This cannot be changed once `#start` has
# been called.
#
# Some common examples of channel orderings:
#
# * `"L,R"`: Stereo ordering in virtually all file formats
# * `"L,R,BL,BR"`: Quadraphonic ordering for most file formats
# * `"L,R,C,LFE,BR,BL"`: channel order of a 5.1 WAV or FLAC file
# * `"L,R,C,LFE,BR,BL,SL,SR"`: channel order of a 7.1 WAV or FLAC file
# * `"L,C,R,BR,BL,LFE"`: channel order of a six channel (5.1) Vorbis I file
# * `"L,C,R,BR,BL,SL,SR,LFE"`: channel order of an eight channel (7.1) Vorbis file
# * `"L,CL,C,R,RC,BC"`: channel order of a six channel AIFF[-C] file
#
# See also: [https://www.xiph.org/ao/doc/ao_sample_format.html]()
def matrix=(value : String) : Nil
if @started
raise AudioDeviceError.new("Cannot change the matrix after the device has been started")
end
@matrix = value
end
# :inherit:
def start : Nil
raise AudioDeviceError.new("Attempted to start a device twice") if @started
Libao.init
id = driverID || Libao.defaultDriverID
info : Pointer(Libao::AoInfo) = Libao.driverInfo(id)
@driverID = id
@type = DriverType.from_value?(info[0].typ) ||
raise AudioDeviceError.new("Could not determine the driver type for driver ID #{id}")
@name = String.new(info[0].name)
@shortName = String.new(info[0].shortName)
@author = String.new(info[0].author)
@comment = String.new(info[0].comment)
@preferredByteFormat = Format.from_value?(info[0].preferredByteFormat) ||
raise AudioDeviceError.new("Could not determine preferred byte format for " \
"driver ID #{id}")
@priority = info[0].priority
info[0].optionCount.times do |idx|
name = String.new(info[0].options[idx])
@availableOptions << name
end
case @bitDepth
when 8
@sampleMult = Int8::MAX.to_i32!
when 16
@sampleMult = Int16::MAX.to_i32!
when 24
@sampleMult = 8388607 # (1- (expt 2 23))
else raise AudioDeviceError.new("AoDevice: Bit depth is unsupported for this library: #{@bitDepth}")
end
spec : Libao::AoSampleFormat = Libao::AoSampleFormat.new
spec.bits = @bitDepth
spec.rate = @sampleRate.to_i32
spec.channels = @channels
spec.byteFormat = @preferredByteFormat.value
spec.matrix = @matrix.to_unsafe
@devicePtr = Libao.openLive(@driverID, pointerof(spec), Pointer(Libao::AoOption).null)
@started = true
end
# :inherit:
def stop : Nil
unless @devicePtr.null?
Libao.close(@devicePtr)
@devicePtr = Libao::PDevice.null
end
Libao.shutdown if @started
@started = false
end
# Fills the internal buffer with audio from *buf* by converting it to the
# proper format.
@[AlwaysInline]
private def fillBuf(buf : Array(Float32)|Slice(Float32)) : UInt32
len : Int32 = buf.size
foreignLen : Int32 = len
neededLen : Int32 = len
i : Int32 = 0
case @bitDepth
when 8
if @foreignBuf.size != neededLen
@foreignBuf = Array(UInt8).new(neededLen, 0u8)
end
fbuf = @foreignBuf.to_unsafe.unsafe_as(Pointer(UInt8))
while i < len
fbuf[i] = (buf.unsafe_fetch(i) * @sampleMult).to_u8!
i = i &+ 1
end
when 16
foreignLen = len * 2
if @foreignBuf.size != neededLen
@foreignBuf = Array(Int16).new(neededLen, 0i16)
end
fbuf16 = @foreignBuf.to_unsafe.unsafe_as(Pointer(Int16))
while i < len
fbuf16[i] = (buf.unsafe_fetch(i) * @sampleMult).to_i16!
i = i &+ 1
end
when 24
foreignLen = len * 3
neededLen = foreignLen
if @foreignBuf.size != neededLen
@foreignBuf = Array(UInt8).new(neededLen, 0u8)
end
di : Int32 = 0
smp : Float32 = 0.0f32
fbuf = @foreignBuf.to_unsafe.unsafe_as(Pointer(UInt8))
while i < len
smp = buf.unsafe_fetch(i)
fbuf[di ] = ((smp * @sampleMult).to_i32! & 0x0000FF).to_u8!
fbuf[di + 1] = ((smp * @sampleMult).to_i32! & 0x00FF00).unsafe_shr(8).to_u8!
fbuf[di + 2] = ((smp * @sampleMult).to_i32! & 0xFF0000).unsafe_shr(16).to_u8!
di = di &+ 3
i = i &+ 1
end
else raise AudioDeviceError.new("Bad bit depth")
end
foreignLen.to_u32!
end
# Plays back the audio in *buf* by sending it to the underlying backend.
#
# You MUST ALWAYS pass the correct buffer size to `#writeBuffer`, as defined
# by the value of `#bufferSize` multiplied by the number of `#channels`.
#
# Note: if `-Dremiaudio_wd40` is used at compile time, then the size of
# *buf* is not checked for a valid size, and it is not checked if the
# device has been started.
def writeBuffer(buf : Array(Float32)|Slice(Float32)) : Nil
{% unless flag?(:remiaudio_wd40) %}
raise AudioDeviceError.new("Device not started") unless @started
unless buf.size == @expectedBufferSize
raise AudioDeviceError.new("Buffer was the incorrect size: #{buf.size} != #{@expectedBufferSize}")
end
{% end %}
len : UInt32 = fillBuf(buf)
Libao.play(@devicePtr, @foreignBuf.to_unsafe.unsafe_as(Pointer(UInt8)), len)
end
# :ditto:
def writeBuffer(buf : Array(Int16)|Slice(Int16)) : Nil
if @bitDepth == 16
{% unless flag?(:remiaudio_wd40) %}
raise AudioDeviceError.new("Device not started") unless @started
unless buf.size == @expectedBufferSize
raise AudioDeviceError.new("Buffer was the incorrect size: #{buf.size} != #{@expectedBufferSize}")
end
{% end %}
# We can just send the buffer straight to libao.
Libao.play(@devicePtr, buf.to_unsafe.unsafe_as(Pointer(UInt8)), buf.size * sizeof(Int16))
else
super(buf)
end
end
end
end
|