#### 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 "./pulse-simple-lib"
####
#### "Simple" PulseAudio Device
####
module RemiAudio::Drivers::PulseAudio
# Returns the version of the underlying PulseAudio library.
def self.version : String
String.new(LibPulseSimple.pa_get_library_version)
end
# The `PulseDevice` class is used to playback audio using PulseAudio.
#
# PulseAudio supports the output of Float32 audio natively, so the
# `#bitDepth` field is ignored by this class and is always set to `32`.
class PulseDevice < AudioDevice
# The informational name of the program. This may be used by software
# such as volume controls to show the name of the program the stream
# belongs to.
#
# This cannot be changed once `#start` is called.
getter programName : String = "RemiAudio Program"
# The informational name of the audio stream. This may be used by
# software such as volume controls to show the name of the audio stream.
#
# This cannot be changed once `#start` is called.
getter streamName : String = "RemiAudio Stream"
@streamPtr : LibPulseSimple::PaSimple = LibPulseSimple::PaSimple.null
# Creates a new `PulseDevice` instance.
#
# PulseAudio supports the output of Float32 audio natively, so the
# `#bitDepth` field is ignored by this class and is always set to `32`.
def initialize(newSampleRate : Int, newBitDepth : Int, newChannels : Int)
@sampleRate = newSampleRate.to_u32.as(UInt32)
@bitDepth = 32u8 # newBitDepth is ignored by PulseDevice
@channels = newChannels.to_u8.as(UInt8)
@expectedBufferSize = @bufferSize * @channels
@outputSize = @expectedBufferSize * sizeof(Float32)
end
# Sets the informational name of the program. This may be used by
# software such as volume controls to show the name of the program the
# stream belongs to.
#
# This cannot be changed once `#start` is called.
def programName=(value : String) : Nil
if @started
raise AudioDeviceError.new("Cannot change the program name after the device has been started")
end
@programName = value
end
# Sets the informational name of the audio stream. This may be used by
# software such as volume controls to show the name of the audio stream.
#
# This cannot be changed once `#start` is called.
def streamName=(value : String) : Nil
if @started
raise AudioDeviceError.new("Cannot change the stream name after the device has been started")
end
@streamName = value
end
# :inherit:
def start : Nil
raise AudioDeviceError.new("Attempted to start a device twice") if @started
spec = LibPulseSimple::PaSampleSpec.new
spec.format = LibPulseSimple::PaSampleFormat::PA_SAMPLE_FLOAT32LE
spec.channels = @channels
spec.rate = @sampleRate
@streamPtr = LibPulseSimple.pa_simple_new(nil,
programName,
LibPulseSimple::PaStreamDirection::PA_STREAM_PLAYBACK,
nil, # default device
streamName,
pointerof(spec),
nil, # Default channel map
nil, # Defaiult buffering attributes
out err)
if @streamPtr.null?
errStr : String = String.new(LibPulseSimple.pa_strerror(err))
raise AudioDeviceError.new("Could not initialize PulseAudio: #{errStr}")
end
@started = true
end
# :inherit:
def stop : Nil
LibPulseSimple.pa_simple_free(@streamPtr) unless @streamPtr.null?
@started = false
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 %}
result = LibPulseSimple.pa_simple_write(@streamPtr, buf.to_unsafe, @outputSize, out writeError)
unless result == 0
errStr = String.new(LibPulseSimple.pa_strerror(writeError))
raise AudioDeviceError.new("Could not write data to PulseAudio: #{errStr}")
end
end
end
end