Artifact 514b504357cfe92c8bf02671a8369955998b04b6478abc61e44f14e92f89ea11:

  • File src/remiaudio/drivers/pulse-simple.cr — part of check-in [04abdd75b6] at 2024-09-05 06:05:57 on branch trunk — Fix some symbol names (user: alexa size: 5703)

#### 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