Artifact ced8e9a6d527533079bb9a04e163c1ecfd6b80ba7b4abae94db3b6a51c7a0d36:

  • File src/remiaudio/drivers.cr — part of check-in [9b3d491963] at 2024-10-29 05:09:17 on branch trunk — When using libao, send Int16 buffers straight to it. Also expand the convenience methods to UInt8 and Int24 samples. (user: alexa size: 8582)

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