#### 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 "./common"
# The `RemiAudio::Drivers` module provides an abstract way to access various
# audio backends. Note that the set of backends depends on two things: the
# target platform (Linux, BSD, etc.), and the compile time flags.
#
# The following backends are available on Linux:
#
# * PulseAudio (disable with `-Dremiaudio_no_pulseaudio`)
# * PortAudio (disable with `-Dremiaudio_no_portaudio`)
# * libao (disable with `-Dremiaudio_no_libao`)
module RemiAudio::Drivers
# Base class for exceptions that occur within this library's code.
class AudioDeviceError < RemiAudioError
end
# The `AudioDevice` class is a virtual representation of an audio output
# device. Subclasses exist for various "driver" backends, such as libao,
# PortAudio, etc.
#
# Whenever possible, the implementing class attempts to work entirely using
# 32-bit floating point audio, and the `#writeBuffer` method only accepts
# 32-bit floating point. However, some backends (e.g. libao) do not support
# floating point audio natively, so the corresponding subclass will convert
# audio automatically on the fly. See the documentation for these subclasses
# for more information.
abstract class AudioDevice
# Returns the sample rate that this device will request from the underlying
# backend when started. The audio data sent to `#writeBuffer` should match
# this sample rate.
getter sampleRate : UInt32 = 44100u32
# Returns the bit depth that this device will request from the underlying
# backend when started.
getter bitDepth : UInt8 = 16u8
# Returns the number of output channels that this device will request from
# the underlying backend when started. The audio data sent to
# `#writeBuffer` should match this.
getter channels : UInt8 = 2u8
# The size of the audio buffers that will get written. This is per-channel,
# so if this is 2048 and there are two channels, then you need an array of
# 4096 elements for a buffer.
#
# This MUST NOT change after the driver has been started, and you MUST
# ALWAYS pass the correct buffer size to `#writeBuffer`.
getter bufferSize : UInt32 = 2048u32
# The expected size of the audio buffers that you send to `#writeBuffer`.
# This value changes depending on what you set for `#bufferSize`.
getter expectedBufferSize : UInt32 = 4096u32
# Returns `true` if the device has been started, or `false` otherwise.
getter? started : Bool = false
@outputSize : UInt32 = 4096u32 * sizeof(Float32)
@f32ConvBuf : Array(Float32)? = nil
# Sets the buffer size. This dictates what size of array you must pass into
# `#writeBuffer`. This is per-channel, so if this is 2048 and there are two
# channels, then you need an array of 4096 elements for a buffer.
#
# This MUST NOT change after the driver has been started, and you MUST
# ALWAYS pass the correct buffer size to `#writeBuffer`. Any attempt to
# change this after starting the device will raise an `AudioDeviceError`.
def bufferSize=(value : Int) : Nil
if @started
raise AudioDeviceError.new("Cannot change the buffer size after the device has been started")
end
@bufferSize = value.to_u32
@expectedBufferSize = @bufferSize * @channels
@outputSize = @expectedBufferSize * sizeof(Float32)
end
# Creates a new device instance with the requested parameters.
#abstract def initialize(newSampleRate : Int, newBitDepth : Int, newChannels : Int)
# Opens the audio stream. This must be called before `#writeBuffer` is
# called.
abstract def start : Nil
# Closes the audio stream and frees resources. This must be called when you
# are finished using the instance to ensure that the resources are properly
# freed and the audio device is properly closed.
abstract def stop : Nil
# 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`.
abstract def writeBuffer(buf : Array(Float32)|Slice(Float32)) : Nil
# :ditto::
@[AlwaysInline]
def <<(buf : Array(Float32)|Slice(Float32)) : Nil
writeBuffer(buf)
end
{% begin %}
{% for size in [8, 16, 32] %}
{% typ = "Int#{size}".id %}
# Plays back the audio in *buf* by sending it to the underlying backend.
# This variation will first convert audio to Float32 internally.
#
# You MUST ALWAYS pass the correct buffer size to `#writeBuffer`, as defined
# by the value of `#bufferSize` multiplied by the number of `#channels`.
def writeBuffer(buf : Array({{typ}})|Slice({{typ}})) : Nil
f32Buf = @f32ConvBuf
if f32Buf.nil? || f32Buf.size != buf.size
@f32ConvBuf = f32Buf = Array(Float32).new(buf.size, 0.0f32)
end
f32Buf.size.times do |i|
f32Buf.unsafe_put(i, buf.unsafe_fetch(i) * INT{{size}}_INV_F32)
end
self.writeBuffer(f32Buf)
end
# :ditto::
@[AlwaysInline]
def <<(buf : Array({{typ}})|Slice({{typ}})) : Nil
writeBuffer(buf)
end
{% end %}
{% end %}
# Plays back the audio in *buf* by sending it to the underlying backend.
# This variation will first convert 24-bit signed integer audio to Float32
# internally.
#
# You MUST ALWAYS pass the correct buffer size to `#writeBuffer`, as defined
# by the value of `#bufferSize` multiplied by the number of `#channels`.
def writeBufferI24(buf : Array(Int32)|Slice(Int32)) : Nil
f32Buf = @f32ConvBuf
if f32Buf.nil? || f32Buf.size != buf.size
@f32ConvBuf = f32Buf = Array(Float32).new(buf.size, 0.0f32)
end
f32Buf.size.times do |i|
f32Buf.unsafe_put(i, buf.unsafe_fetch(i) * INT24_INV_F32)
end
self.writeBuffer(f32Buf)
end
# Plays back the audio in *buf* by sending it to the underlying backend.
# This variation will first convert audio to Float32 internally.
#
# You MUST ALWAYS pass the correct buffer size to `#writeBuffer`, as defined
# by the value of `#bufferSize` multiplied by the number of `#channels`.
def writeBuffer(buf : Array(UInt8)|Slice(UInt8)) : Nil
f32Buf = @f32ConvBuf
if f32Buf.nil? || f32Buf.size != buf.size
@f32ConvBuf = f32Buf = Array(Float32).new(buf.size, 0.0f32)
end
f32Buf.size.times do |i|
f32Buf.unsafe_put(i, (buf.unsafe_fetch(i).to_i16! - 128) * INT8_INV_F32)
end
self.writeBuffer(f32Buf)
end
# :ditto::
@[AlwaysInline]
def <<(buf : Array(UInt8)|Slice(UInt8)) : Nil
writeBuffer(buf)
end
end
# Creates a new `AudioDevice` instance, then yields it to the block. This
# ensures that `AudioDevice#stop` is called once the block exits.
#
# **T** must be a subclass of `RemiAudio::Drivers::AudioDevice` except
# **`RemiAudio::Drivers::TcpDevice`.
def self.withDevice(typ : T.class, sampleRate : Int, bitDepth : Int, channels : Int, &) : Nil forall T
{% begin %}
{% unless T < ::RemiAudio::Drivers::AudioDevice %}
{% raise "Must use a subclass of RemiAudio::Drivers::AudioDevice" %}
{% end %}
{% end %}
dev = T.new(sampleRate, bitDepth, channels)
yield dev
ensure
dev.try &.stop
end
end
{% begin %}
{% unless flag?(:remiaudio_no_libao) %}3
require "./drivers/ao"
{% end %}
{% unless flag?(:remiaudio_no_portaudio) %}3
require "./drivers/portaudio"
{% end %}
{% unless flag?(:remiaudio_no_pulseaudio) %}3
require "./drivers/pulse-simple"
{% end %}
{% end %}
require "./drivers/tcp"