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
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
| #### RemiAudio
#### Copyright (C) 2022-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 "./audiofile"
module RemiAudio::Formats
###
### Constants
###
# :nodoc:
AU_MAGIC = ".snd"
# The minimum supported sample rate for Au files.
AU_MIN_SAMPLE_RATE = 8000
# The maximum supported sample rate for Au files.
AU_MAX_SAMPLE_RATE = 352800
# The minimum supported number of channels for Au files.
AU_MIN_CHANNELS = 1
# The maximum supported number of channels for Au files.
AU_MAX_CHANNELS = 2
# An array of all the bit depths in Au files that are supported by this
# library for integer samples.
AU_SUPPORTED_INT_BIT_DEPTHS = [8, 16, 24, 32, 64]
# An array of all the bit depths in Au files that are supported by this
# library for float samples.
AU_SUPPORTED_FLOAT_BIT_DEPTHS = [32, 64]
###
### Enumerations
###
# The supported encodings for an Au file.
enum AuEncoding
# µLaw encoded samples.
MuLaw = 1
# 8-bit signed linear PCM samples
Lpcm8bit
# 16-bit signed linear PCM samples
Lpcm16bit
# 24-bit signed linear PCM samples
Lpcm24bit
# 32-bit signed linear PCM samples
Lpcm32bit
# 32-bit IEEE Float samples
Float32
# 64-bit IEEE Float samples
Float64
# ALaw encoded samples.
ALaw = 27
def self.from_value(value : Int) : self
from_value?(value) || raise UnsupportedEncodingError.new("Unsupported Au encoding")
end
# Gets the `SampleFormat` that describes this encoding. If this is an
# encoding that does not describe a PCM/Float encoding, or an encoding not
# directly supported by RemiAudio, this raises an
# `UnsupportedEncodingError`.
@[AlwaysInline]
def toSampleFormat : SampleFormat
self.toSampleFormat? || raise UnsupportedEncodingError.new("Cannot represents #{self} as a SampleFormat")
end
# Attempts to get the `SampleFormat` that describes this encoding, or
# `nil` if it's a non PCM/Float encoding, or an encoding not directly
# supported by RemiAudio.
@[AlwaysInline]
def toSampleFormat? : SampleFormat?
case self
when .lpcm8bit? then SampleFormat::I8
when .lpcm16bit? then SampleFormat::I16
when .lpcm24bit? then SampleFormat::I24
when .lpcm32bit? then SampleFormat::I32
when .float32? then SampleFormat::F32
when .float64? then SampleFormat::F64
else nil
end
end
# Returns the bit depth for a single sample using this encoding.
@[AlwaysInline]
def getBitDepth : UInt8
case self
in .lpcm8bit?, .mu_law?, .a_law? then 8u8
in .lpcm16bit? then 16u8
in .lpcm24bit? then 24u8
in .lpcm32bit?, .float32? then 32u8
in .float64? then 64u8
end
end
# Returns the size in bytes for a single sample using this encoding.
@[AlwaysInline]
def getByteSize : Int32
case self
in .lpcm8bit?, .mu_law?, .a_law? then 1
in .lpcm16bit? then 2
in .lpcm24bit?, .lpcm32bit?, .float32? then 4
in .float64? then 8
end
end
# Returns the value to use when converting a value in this encoding to a
# `::Float64`, or `nil` if converting from this encoding is not supported.
def getFloat64Div? : Float64?
{% begin %}
case self
when .lpcm8bit? then ::Int8::MAX.to_f64!
when .lpcm16bit? then ::Int16::MAX.to_f64!
when .lpcm24bit? then {{ (2.0 ** 23) - 1 }}
when .lpcm32bit? then ::Int32::MAX.to_f64!
when .float32? then 1.0
when .float64? then 1.0
else nil
end
{% end %}
end
# Returns the value to use when converting a value in this encoding to a
# `::Float64`. This will raise an `UnsupportedEncodingError` if converting
# from this encoding is not supported.
@[AlwaysInline]
def getFloat64Div : Float64
getFloat64Div? ||
raise UnsupportedEncodingError.new("This Au encoding is currently unsupported by this library: #{self}")
end
# Checks that *array* is the correct type for this encoding. If it is, this
# returns `true`, otherwise it returns `false`.
@[AlwaysInline]
def checkArray(array : SampleData) : Bool
case self
in .mu_law?, .a_law? then array.is_a?(Array(::Int8))
in .lpcm8bit? then array.is_a?(Array(::Int8))
in .lpcm16bit? then array.is_a?(Array(::Int16))
in .lpcm24bit? then array.is_a?(Array(::Int32))
in .lpcm32bit? then array.is_a?(Array(::Int32))
in .float32? then array.is_a?(Array(::Float32))
in .float64? then array.is_a?(Array(::Float64))
end
end
# Creates a new array of the given size that is appropriate for this
# encoding.
@[AlwaysInline]
def makeArray(size : Int32) : SampleData
case self
in .mu_law?, .a_law? then Array(UInt8).new(size, 0u8).as(SampleData)
in .lpcm8bit? then Array(Int8).new(size, 0i8).as(SampleData)
in .lpcm16bit? then Array(Int16).new(size, 0i16).as(SampleData)
in .lpcm24bit?, .lpcm32bit? then Array(Int32).new(size, 0i32).as(SampleData)
in .float32? then Array(::Float32).new(size, 0.0f32).as(SampleData)
in .float64? then Array(::Float64).new(size, 0.0f64).as(SampleData)
end
end
end
##############################################################################
###
### AuFile Class
###
# A virtual representation of an Au file (.au/.snd).
#
# Au files originated from Sun and store data in liner PCM, IEEE Float, µLaw,
# or ALaw format. Their data is stored in big endian format.
#
# The official specification supports more encodings, some of which are
# specific to certain platforms. This library does not support these. See
# `AuEncoding` for a list of supported encodings.
class AuFile < AudioFile
# The `AuEncoding` that the samples in this instance uses.
getter encoding : AuEncoding = AuEncoding::Lpcm16bit
# The annotation string for this instance, or `nil` if there is none.
getter note : String?
# Creates a new, blank `AuFile` that is backed by a fresh `::IO::Memory`.
# This will immediately write a skeleton Au header into `#io`
def initialize(*, @sampleRate : UInt32 = 44100u32, @channels : UInt32 = 2u32,
@encoding : AuEncoding = AuEncoding::Lpcm16bit, @note : String? = nil)
@bitDepth = @encoding.getBitDepth
@blockSize = @encoding.getByteSize.to_u16
checkInternalValues
assignFuncs
@io = IO::Memory.new(0)
@origIoPos = 0
# Write the header
writeHeader
end
# Creates a new, blank `AuFile` that is backed by the given `::IO`. This
# will immediately write a skeleton Au header into *io*.
def initialize(@io : IO, *, @sampleRate : UInt32 = 44100u32, @channels : UInt32 = 2u32,
@encoding : AuEncoding = AuEncoding::Lpcm16bit, @note : String? = nil)
@bitDepth = @encoding.getBitDepth
@blockSize = @encoding.getByteSize.to_u16
checkInternalValues
assignFuncs
@origIoPos = @io.pos.to_u64!
# Write the header
writeHeader
end
protected def initialize(*, _fromIo : IO)
@io = _fromIo
@origIoPos = @io.pos.to_u64
readHeader
checkInternalValues
assignFuncs
@io.pos = @dataStartsAt
end
# Creates a new `AuFile` by reading the existing Au data from *io*. Samples
# will be streamed from *io* as needed.
def self.open(io : IO) : AuFile
AuFile.new(_fromIo: io)
end
# Creates a new `AuFile` by reading the existing Au data from *io*, then
# yields it to the block. Samples will be streamed from *io* as needed.
# This will **NOT** automatically close the `AuFile` before returning in
# case you have passed an *io* that cannot does not support writing.
def self.open(io : IO, &)
au = AuFile.open(io)
yield au
end
# Creates a new `AuFile` by reading the existing Au data from the given
# file. The file is always opened with the mode `"r+b"`. Samples will be
# streamed from the file as needed.
def self.open(filename : Path|String) : AuFile
AuFile.new(_fromIo: File.open(filename, "r+b"))
end
# Creates a new `AuFile` by reading the existing Au data from the file, then
# yields it to the block. The file is always opened with the mode `"r+b"`.
# Samples will be streamed from *io* as needed. This **will** automatically
# close the `AuFile` and underlying `::IO` before returning.
def self.open(filename : Path|String, &)
au = AuFile.open(filename)
yield au
ensure
au.try &.close
end
# Creates a new `AuFile` that that is backed by *io* and returns the new
# instance. A skeleton Au header data is immediately written into *io*.
def self.create(io : IO, *, sampleRate : Int = 44100, channels : Int = 2,
encoding : AuEncoding = AuEncoding::Lpcm16bit, note : String? = nil) : AuFile
AuFile.new(io, sampleRate: sampleRate.to_u32, channels: channels.to_u32, encoding: encoding, note: note)
end
# Creates a new `AuFile` that that is backed by *io* and yields the new
# instance to the block. A skeleton Au header data is immediately written
# into *io*. The `AuFile` instance will be flushed and closed once the
# block exits.
def self.create(io : IO, *, sampleRate : Int = 44100, channels : Int = 2,
encoding : AuEncoding = AuEncoding::Lpcm16bit, note : String? = nil, &) : AuFile
au = AuFile.new(io, sampleRate: sampleRate.to_u32, channels: channels.to_u32, encoding: encoding, note: note)
yield au
ensure
au.try &.flush
au.try &.close
end
# Creates a new `AuFile` that is backed by a new file on disk.
#
# This always opens the file with the mode `"w+b"` (i.e. existing files are
# overwritten/truncated). A skeleton Au header data is immediately written
# into the file.
def self.create(filename : Path|String, *, sampleRate : Int = 44100, channels : Int = 2,
encoding : AuEncoding = AuEncoding::Lpcm16bit, note : String? = nil) : AuFile
file = File.open(filename, "w+b")
begin
AuFile.new(file, sampleRate: sampleRate.to_u32, channels: channels.to_u32, encoding: encoding, note: note)
rescue err : Exception
file.try &.close
raise err
end
end
# Creates a new `AuFile` that is backed by a new file on disk, then yields
# it to the block. This automatically closes the `AuFile` and the
# underlying file before the block returns.
#
# This always opens the file with the mode `"w+b"` (i.e. existing files are
# overwritten/truncated). A skeleton Au header data is immediately written
# into the file.
def self.create(filename : Path|String, *, sampleRate : Int = 44100, channels : Int = 2,
encoding : AuEncoding = AuEncoding::Lpcm16bit, note : String? = nil, &)
file = File.open(filename, "w+b")
begin
au = AuFile.new(file, sampleRate: sampleRate.to_u32, channels: channels.to_u32, encoding: encoding, note: note)
begin
yield au
ensure
au.close
end
rescue err : Exception
file.try &.close
raise err
end
end
##############################################################################
# Checks that the internal values are consistent.
protected def checkInternalValues : Nil
case @encoding
when .mu_law?, .a_law?
raise AudioFileError.new("Bad bit depth for µLaw/ALaw encoding") unless @bitDepth == 8
when .lpcm8bit?
raise AudioFileError.new("Bad bit depth for 8-bit LPCM encoding") unless @bitDepth == 8
when .lpcm16bit?
raise AudioFileError.new("Bad bit depth for 16-bit LPCM encoding") unless @bitDepth == 16
when .lpcm24bit?
raise AudioFileError.new("Bad bit depth for 24-bit LPCM encoding") unless @bitDepth == 24
when .lpcm32bit?
raise AudioFileError.new("Bad bit depth for 32-bit LPCM encoding") unless @bitDepth == 32
when .float32?
raise AudioFileError.new("Bad bit depth for 32-bit float encoding") unless @bitDepth == 32
when .float64?
raise AudioFileError.new("Bad bit depth for 64-bit float encoding") unless @bitDepth == 64
else
raise UnsupportedEncodingError.new("This Au encoding is currently unsupported by this library")
end
# Check some values
unless @sampleRate >= AU_MIN_SAMPLE_RATE && @sampleRate <= AU_MAX_SAMPLE_RATE
raise AudioFileError.new("Unsupported sample rate: #{@sampleRate}")
end
unless @channels >= AU_MIN_CHANNELS && @channels <= AU_MAX_CHANNELS
raise AudioFileError.new("Unsupported number of channels: #{@channels}")
end
end
# Assigns the internal functions according to our encoding.
protected def assignFuncs : Nil
case @encoding
in .lpcm8bit?
@sampleReadFn = ->fastReadSampleInt8
@samplesReadFn = ->fastReadSamplesInt8(SampleData)
@sampleWriteFn = ->fastWriteSampleInt8(Sample)
@samplesWriteFn = ->fastWriteSamplesInt8(SampleData)
@sampleToF64Fn = ->int8ToF64(Sample)
@f64ToSampleFn = ->f64ToInt8(Float64)
in .lpcm16bit?
@sampleReadFn = ->fastReadSampleInt16BE
@samplesReadFn = ->fastReadSamplesInt16BE(SampleData)
@sampleWriteFn = ->fastWriteSampleInt16BE(Sample)
@samplesWriteFn = ->fastWriteSamplesInt16BE(SampleData)
@sampleToF64Fn = ->int16ToF64(Sample)
@f64ToSampleFn = ->f64ToInt16(Float64)
in .lpcm24bit?
@sampleReadFn = ->fastReadSampleInt24BEFourByte
@samplesReadFn = ->fastReadSamplesInt24BEFourByte(SampleData)
@sampleWriteFn = ->fastWriteSampleInt24BEFourByte(Sample)
@samplesWriteFn = ->fastWriteSamplesInt24BEFourByte(SampleData)
@sampleToF64Fn = ->int24ToF64(Sample)
@f64ToSampleFn = ->f64ToInt24(Float64)
in .lpcm32bit?
@sampleReadFn = ->fastReadSampleInt32BE
@samplesReadFn = ->fastReadSamplesInt32BE(SampleData)
@sampleWriteFn = ->fastWriteSampleInt32BE(Sample)
@samplesWriteFn = ->fastWriteSamplesInt32BE(SampleData)
@sampleToF64Fn = ->int32ToF64(Sample)
@f64ToSampleFn = ->f64ToInt32(Float64)
in .float32?
@sampleReadFn = ->fastReadSampleFloat32BE
@samplesReadFn = ->fastReadSamplesFloat32BE(SampleData)
@sampleWriteFn = ->fastWriteSampleFloat32BE(Sample)
@samplesWriteFn = ->fastWriteSamplesFloat32BE(SampleData)
@sampleToF64Fn = ->float32ToF64(Sample)
@f64ToSampleFn = ->f64ToFloat32(Float64)
in .float64?
@sampleReadFn = ->fastReadSampleFloat64BE
@samplesReadFn = ->fastReadSamplesFloat64BE(SampleData)
@sampleWriteFn = ->fastWriteSampleFloat64BE(Sample)
@samplesWriteFn = ->fastWriteSamplesFloat64BE(SampleData)
@sampleToF64Fn = ->float64ToF64(Sample)
@f64ToSampleFn = ->f64ToFloat64(Float64)
in .mu_law?, .a_law?
@sampleReadFn = ->fastReadSampleUInt8
@samplesReadFn = ->fastReadSamplesUInt8(SampleData)
@sampleWriteFn = ->fastWriteSampleUInt8(Sample)
@samplesWriteFn = ->fastWriteSamplesUInt8(SampleData)
@sampleToF64Fn = ->AudioFile.cannotConvertToFloat64(Sample)
@f64ToSampleFn = ->AudioFile.cannotConvertToSample(Float64)
end
end
# Reads the header data.
protected def readHeader : Nil
# Read the magic bytes and check that it's an Au file.
unless @io.read_string(AU_MAGIC.size) == AU_MAGIC
raise AudioFileError.new("Not an Au file")
end
# Read the rest of the header information. Note that Au files use big
# endian throughout.
@dataStartsAt = @io.readUInt32.byte_swap.to_u64!
@dataSizeInBytes = @io.readUInt32.byte_swap.to_u64!
@encoding = AuEncoding.from_value(@io.readUInt32.byte_swap)
@sampleRate = @io.readUInt32.byte_swap
@channels = @io.readUInt32.byte_swap
# Set bit depth and get the block size
@bitDepth = @encoding.getBitDepth
@blockSize = @encoding.getByteSize.to_u16
# Read the annotation, if any. If there is no annotation, then this field
# will simply be four zero bytes.
initialChar = @io.readUInt32
unless initialChar == 0
# There is an annotation we need to read. We have the first four bytes
# already. But it may be three bytes or less.
@note = case
when ((initialChar & 0xFF000000) >> 24) == 0
# Odd case, empty string, ignore junk bytes after
""
when ((initialChar & 0x00FF0000) >> 16) == 0
# Single character
String.build { |str| str.write_byte(((initialChar & 0xFF000000) >> 24).to_u8!) }
when ((initialChar & 0x0000FF00) >> 8) == 0
# Two characters
String.build do |str|
str.write_byte(((initialChar & 0xFF000000) >> 24).to_u8!)
str.write_byte(((initialChar & 0x00FF0000) >> 16).to_u8!)
end
when (initialChar & 0x000000FF) == 0
# Three characters
String.build do |str|
str.write_byte(((initialChar & 0xFF000000) >> 24).to_u8!)
str.write_byte(((initialChar & 0x00FF0000) >> 16).to_u8!)
str.write_byte(((initialChar & 0x0000FF00) >> 8).to_u8!)
end
else
# Read a full null-terminated string that is four bytes or more.
String.build do |str|
# Write the first four bytes we already have
str.write_byte(((initialChar & 0xFF000000) >> 24).to_u8!)
str.write_byte(((initialChar & 0x00FF0000) >> 16).to_u8!)
str.write_byte(((initialChar & 0x0000FF00) >> 8).to_u8!)
str.write_byte( (initialChar & 0x000000FF).to_u8!)
# Write more bytes until we hit null.
while (byte = @io.readUInt8) != 0
str.write_byte(byte)
end
end
end
end
# Determine the number of samples we have.
@numSamples = @dataSizeInBytes.to_u64.tdiv(@blockSize)
raise AudioFileError.new("Bad number of samples, some are missing") unless @numSamples % @channels == 0
end
# Writes the header data.
protected def writeHeader : Nil
@io << AU_MAGIC
@io.writeUInt32BE(69u32) # Temporary value for data offset
@io.writeUInt32BE(0xFFFFFFFF_u32) # Temporary value for data size in bytes
@io.writeUInt32BE(@encoding.value.to_u32)
@io.writeUInt32BE(@sampleRate)
@io.writeUInt32BE(@channels.to_u32!)
if @note && !@note.not_nil!.empty?
str = @note.not_nil!
@io << str
if str.size < 4
(4 - str.size).times { |_| @io.write_byte(0) }
else
@io.write_byte(0)
end
else
@io.writeUInt32BE(0u32)
end
@io.flush # Needed for the pos below to work right
@dataStartsAt = @io.pos.to_u64
end
# Flushes the stream, then adjusts the header data with the correct data
# size and offset.
protected def finishHeader : Nil
raise AudioFileError.new("Bad number of samples, some are missing") unless @numSamples % @channels == 0
@io.flush
@io.withExcursion(4) do
@io.writeUInt32BE(@dataStartsAt.to_u32) # Data offset
finalNumBytes : UInt32 = 0u32
begin
finalNumBytes = (@numSamples * @blockSize).to_u32
@io.writeUInt32BE(finalNumBytes) # Data size in bytes
rescue ::OverflowError
raise AudioFileError.new("Too many samples for an Au file")
end
end
end
##############################################################################
# :inherited:
def sampleFormat : SampleFormat
@encoding.toSampleFormat || raise AudioFileError.new("Cannot represent encoding #{@encoding} as a SampleFormat")
end
# :inherited:
def read(dest : Array(Float64)) : Int32
temp : SampleData = @encoding.makeArray(dest.size)
num : Int32 = readSamples(temp)
num.times do |i|
dest[i] = @sampleToF64Fn.call(temp[i])
end
num
end
# :inherited:
def readSamplesToEnd : SampleData
curSample = self.pos
toRead : UInt64 = @numSamples - curSample
ret : SampleData = @encoding.makeArray(toRead.to_i32)
@samplesReadFn.call(ret)
ret
end
# :inherited:
def flush : Nil
return unless @changed
finishHeader
@io.flush
end
def copyTo(dest : IO) : Nil
flush
@io.withExcursion(@origIoPos) do
IO.copy(@io, dest)
end
end
end
end
|