File src/remiaudio/drivers/port-bindings.cr from the latest check-in


     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
   580
   581
   582
   583
   584
   585
   586
   587
   588
   589
   590
   591
   592
   593
   594
   595
   596
   597
   598
   599
   600
   601
   602
   603
   604
   605
   606
   607
   608
   609
   610
   611
   612
   613
   614
   615
   616
   617
   618
   619
   620
   621
   622
   623
   624
#### 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 "./port-lib"

module RemiAudio::Drivers::PortAudio
  # :nodoc:
  module PaMappable(T)
    def initialize(ptr : T)
      {% begin %}
        {% for ivar in @type.instance_vars %}
          @{{ivar.id}} = {% if ivar.type.resolve < Enum %}
                               {{ivar.type.id}}.new(ptr.value.{{ivar.id}}.value)
          {% elsif ivar.type == String %}
            String.new(ptr.value.{{ivar.id}})
          {% else %}
            ptr.value.{{ivar.id}}
          {% end %}
        {% end %}
      {% end %}
    end
  end

  # :nodoc:
  protected def self.raiseIfError(&)
    err = yield
    if err.is_a?(Number)
      if err >= -10000 && err <= -9972
        raise PaError.new("#{LibPortAudio::PaError.new(err)}", err.to_i64)
      else
        return err
      end
    elsif err != LibPortAudio::PaError::NoError
      raise PaError.new("PortAudio error: #{err}", err.to_i64)
    end
    0
  end

  class PaError < AudioDeviceError
    getter errCode : Int64 = 0

    def initialize(@message : String?)
    end

    def initialize(@message : String?, @errCode)
    end
  end

  enum HostApiType
    InDevelopment = 0
    DirectSound
    Mme
    Asio
    SoundManager
    CoreAudio
    Oss
    Alsa
    Al
    BeOs
    Wdmks
    Jack
    Wasapi
    AudioScienceHpi

    # Returns the runtine host API index associated with the `HostApiType`.
    def index : Int
      PortAudio.raiseIfError do
        LibPortAudio.Pa_HostApiTypeIdToHostApiIndex(self.value)
      end
    end
  end

  @[Flags]
  enum SampleFormat : UInt64
    Float32 = 0x00000001
    Int32   = 0x00000002
    Int24   = 0x00000004
    Int16   = 0x00000008
    Int8    = 0x00000010
    UInt8   = 0x00000020
    #CustomFormat = 0x00010000    # Not supported
    #NonInterleaved = 0x80000000  # Not supported

    # Retrieves the size of a single sample for this `SampleFormat`.
    def sampleSize : Int
      LibPortAudio.Pa_GetSampleSize(self.value)
    end
  end

  alias SampleRate = Int8|Int16|Int32|Int64|Int128|
                     UInt8|UInt16|UInt32|UInt64|UInt128|
                     Float32|Float64
  alias SampleBuffer = Slice(Float32)|Slice(Int32)|Slice(Int16)|Slice(Int8)|Slice(UInt8)|
                       Array(Float32)|Array(Int32)|Array(Int16)|Array(Int8)|Array(UInt8)

  alias StreamCallbackTimeInfo = LibPortAudio::StreamCallbackTimeInfo
  alias StreamCallbackFlags = LibPortAudio::StreamCallbackFlags
  alias PaCallback = LibPortAudio::PaCallback

  @[Flags]
  enum StreamFlags
    NoFlag = 0
    ClipOff = 0x00000001
    DitherOff = 0x00000002
  end

  struct HostErrorInfo
    include PaMappable(LibPortAudio::PHostErrorInfo)
    getter hostApiTypeID : HostApiType
    getter errorCode : Int64
    getter errorText : String
  end

  struct DeviceInfo
    include PaMappable(LibPortAudio::PDeviceInfo)
    getter structVersion : Int32
    getter name : String
    getter hostApi : Int32
    getter maxInputChannels : Int32
    getter maxOutputChannels : Int32
    getter defaultLowInputLatency : Float64
    getter defaultLowOutputLatency : Float64
    getter defaultHighInputLatency : Float64
    getter defaultHighOutputLatency : Float64
    getter defaultSampleRate : Float64
  end

  struct StreamParameters
    property device : Int32 = 0
    property channelCount : Int32 = 0
    property sampleFormat : SampleFormat = SampleFormat::Float32
    property suggestedLatency : Float64 = 0.0

    def initialize
    end

    def initialize(@device : Int32, @channelCount : Int32, @sampleFormat : SampleFormat, @suggestedLatency : Float64)
    end

    protected def toForeign : LibPortAudio::StreamParameters
      ret = LibPortAudio::StreamParameters.new
      ret.device = @device
      ret.channelCount = @channelCount
      ret.sampleFormat = @sampleFormat.value
      ret.suggestedLatency = @suggestedLatency
      ret.hostApiSpecificStreamInfo = nil
      ret
    end

    protected def toForeignPtr : Pointer(LibPortAudio::StreamParameters)
      ret = Pointer(LibPortAudio::StreamParameters).malloc(1)
      ret.value.device = @device
      ret.value.channelCount = @channelCount
      ret.value.sampleFormat = @sampleFormat.value
      ret.value.suggestedLatency = @suggestedLatency
      ret.value.hostApiSpecificStreamInfo = nil
      ret
    end

    def self.checkFormatSupported(inputParams : StreamParameters?, outputParams : StreamParameters?,
                                  sampleRate : SampleRate) : Bool
      finput = inputParams ? inputParams.toForeignPtr : Pointer(LibPortAudio::StreamParameters).null
      foutput = outputParams ? outputParams.toForeignPtr : Pointer(LibPortAudio::StreamParameters).null

      ret = PortAudio.raiseIfError do
        LibPortAudio.Pa_IsFormatSupported(finput, foutput, sampleRate.to_f64)
      end

      ret == LibPortAudio::FORMAT_IS_SUPPORTED
    end

    def self.isFormatSupported?(inputParams : StreamParameters?, outputParams : StreamParameters?,
                                sampleRate : SampleRate) : Bool
      # ameba:disable Style/RedundantBegin
      begin
        self.checkFormatSupported(inputParams, outputParams, sampleRate)
      rescue PaError
        false
      end
    end

    def self.isInputFormatSupported?(params : StreamParameters, sampleRate : SampleRate) : Bool
      self.isFormatSupported?(params, nil, sampleRate)
    end

    def self.isOutputFormatSupported?(params : StreamParameters, sampleRate : SampleRate) : Bool
      self.isFormatSupported?(nil, params, sampleRate)
    end
  end

  struct HostApiInfo
    include PaMappable(LibPortAudio::PHostApiInfo)
    getter structVersion : Int32
    getter typeId : HostApiType
    getter name : String
    getter deviceCount : Int32
    getter defaultInputDevice : Int32
    getter defaultOutputDevice : Int32
  end

  def self.getLastHostErrorInfo : HostErrorInfo
    HostErrorInfo.new(LibPortAudio.Pa_GetLastHostErrorInfo())
  end

  def self.version : Int
    LibPortAudio.Pa_GetVersion()
  end

  def self.versionStr : String
    String.new(LibPortAudio.Pa_GetVersionText())
  end

  def self.deviceCount : Int
    PortAudio.raiseIfError { LibPortAudio.Pa_GetDeviceCount() }.not_nil!
  end

  def self.defaultOutputDevice : Int
    PortAudio.raiseIfError { LibPortAudio.Pa_GetDefaultOutputDevice() }.not_nil!
  end

  def self.getDeviceInfo(device : Int) : DeviceInfo
    DeviceInfo.new(LibPortAudio.Pa_GetDeviceInfo(device))
  end

  def self.defaultInputDevice() : Int
    PortAudio.raiseIfError { LibPortAudio.Pa_GetDefaultInputDevice() }.not_nil!
  end

  def self.hostApiInfo(apiIndex : Int) : HostApiInfo
    HostApiInfo.new(LibPortAudio.Pa_GetHostApiInfo(apiIndex))
  end

  def self.init
    PortAudio.raiseIfError { LibPortAudio.Pa_Initialize() }
  end

  def self.terminate
    PortAudio.raiseIfError { LibPortAudio.Pa_Terminate() }
  end

  def self.defaultHostApi : Int
    PortAudio.raiseIfError { LibPortAudio.Pa_GetDefaultHostApi() }
  end

  def self.hostApiDeviceIndexToDeviceIndex(hostApi : Int, hostApiDeviceIndex : Int) : Int
    PortAudio.raiseIfError { LibPortAudio.Pa_HostApiDeviceIndexToDeviceIndex(hostApi, hostApiDeviceIndex) }
  end

  def self.sleep(msec : Int64) : Nil
    LibPortAudio.Pa_Sleep(msec)
  end

  # Initializes PortAudio using `PortAudio.start`, then yields.  This
  # ensures that `PortAudio.terminate` is called before this returns.
  def self.withPA(&) : Nil
    init
    yield
  ensure
    terminate
  end

  struct StreamInfo
    include PaMappable(LibPortAudio::PStreamInfo)
    getter structVersion : Int32
    getter inputLatency : Float64
    getter outputLatency : Float64
    getter sampleRate : Float64
  end

  class PaStream
    @handle : LibPortAudio::PPaStream = LibPortAudio::PPaStream.null
    getter inputSampleFormat : SampleFormat?
    getter inputChannels : Int32?
    getter outputSampleFormat : SampleFormat?
    getter outputChannels : Int32?
    getter framesPerBuffer : UInt64

    protected def initialize(@handle, @inputSampleFormat, @inputChannels, @outputSampleFormat, @outputChannels,
                             @framesPerBuffer)
    end

    protected def initialize(@inputSampleFormat, @inputChannels, @outputSampleFormat, @outputChannels,
                             @framesPerBuffer)
    end

    protected def handle : LibPortAudio::PPaStream
      @handle
    end

    def self.openDefaultStream(numInput : Int?, numOutput : Int?, sampleFormat : SampleFormat,
                               sampleRate : SampleRate, framesPerBuffer : Int) : PaStream
      ret : LibPortAudio::PPaStream = LibPortAudio::PPaStream.null

      PortAudio.raiseIfError do
        LibPortAudio.Pa_OpenDefaultStream(pointerof(ret), numInput || 0, numOutput || 0,
                                          sampleFormat.value,
                                          sampleRate.to_f64, framesPerBuffer.to_u64, nil, nil)
      end

      PaStream.new(ret, sampleFormat,
                   (numInput.nil? ? nil : (numInput == 0 ? nil : numInput.to_i32)),
                   sampleFormat,
                   (numOutput.nil? ? nil : (numOutput == 0 ? nil : numOutput.to_i32)),
                   framesPerBuffer.to_u64)
    end


    def self.openDefaultStream(numInput : Int?, numOutput : Int?, sampleFormat : SampleFormat,
                               sampleRate : SampleRate, framesPerBuffer : Int, callback : PaCallback,
                               userData : Pointer(Void)? = nil) : PaStream
      ret : LibPortAudio::PPaStream = LibPortAudio::PPaStream.null

      PortAudio.raiseIfError do
        LibPortAudio.Pa_OpenDefaultStream(pointerof(ret), numInput || 0, numOutput || 0,
                                          sampleFormat.value,
                                          sampleRate.to_f64, framesPerBuffer.to_u64,
                                          callback, userData)
      end

      PaStream.new(ret, sampleFormat,
                   (numInput.nil? ? nil : (numInput == 0 ? nil : numInput)),
                   sampleFormat,
                   (numOutput.nil? ? nil : (numOutput == 0 ? nil : numOutput)),
                   framesPerBuffer.to_u64)
    end

    def self.openDefaultStream(numInput : Int?, numOutput : Int?, sampleFormat : SampleFormat,
                               sampleRate : SampleRate, framesPerBuffer : Int,
                               userData : Pointer(Void)?, &block : PaCallback) : PaStream
      openDefaultStream(numInput, numOutput, sampleFormat, sampleRate, framesPerBuffer, block, userData)
    end

    def self.openStream(inputParams : StreamParameters?, outputParams : StreamParameters?,
                        sampleRate : SampleRate, framesPerBuffer : Int,
                        streamFlags : StreamFlags) : PaStream
      ret = uninitialized LibPortAudio::PaStream
      finput = inputParams ? inputParams.toForeignPtr : Pointer(LibPortAudio::StreamParameters).null
      foutput = outputParams ? outputParams.toForeignPtr : Pointer(LibPortAudio::StreamParameters).null

      PortAudio.raiseIfError do
        LibPortAudio.Pa_OpenStream(out ret, finput, foutput, sampleRate.to_f64,
                                       framesPerBuffer.to_u64,
                                       LibPortAudio::StreamFlags.new(streamFlags.value), nil, nil)
      end

      input = finput.nil? ? nil : inputParams.new(finput)
      output = foutput.nil? ? nil : outputParams.new(foutput)
      PaStream.new(pointerof(ret),
                   input.nil? ? nil : input.sampleFormat,
                   input.nil? ? nil : input.channelCount,
                   output.nil? ? nil : output.sampleFormat,
                   output.nil? ? nil : output.channelCount,
                   framesPerBuffer)
    end

    # Opens the default audio stream using the given options and starts it with
    # `#start`.  This then yields the stream.  This ensures that the stream is
    # both stopped with `#stop` and closed with `#close` before returning.
    def self.withDefaultStream(numInput : Int?, numOutput : Int?,
                               *, sampleFormat : SampleFormat = SampleFormat::Float32,
                               sampleRate : SampleRate = 44100, framesPerBuffer : Int = 1024, &) : Nil
      strm = self.openDefaultStream(numInput, numOutput, sampleFormat, sampleRate, framesPerBuffer.to_u64)
      begin
        strm.start
        yield strm
      ensure
        strm.stop
        strm.close
      end
    end

    # Opens an audio stream using the given options and starts it with `#start`.
    # This then yields the stream.  This ensures that the stream is both stopped
    # with `#stop` and closed with `#close` before returning.
    def self.withStream(inputParams : StreamParameters?, outputParams : StreamParameters?,
                        *, sampleRate : SampleRate = 44100, framesPerBuffer : Int = 1024,
                        streamFlags : StreamFlags = StreamFlags::NoFlag, &) : Nil
      strm = self.openStream(inputParams, outputParams, sampleRate, framesPerBuffer.to_u64, streamFlags)
      begin
        strm.start
        yield strm
      ensure
        strm.stop
        strm.close
      end
    end

    # Commences audio processing
    def start : Nil
      PortAudio.raiseIfError { LibPortAudio.Pa_StartStream(@handle) }
    end

    # Terminates audio processing immediately without waiting for pending
    # buffers to complete.
    def abort : Nil
      PortAudio.raiseIfError { LibPortAudio.Pa_AbortStream(@handle) }
    end

    # Terminates audio processing. It will wait until all pending audio buffers
    # have been played before returning.
    def stop : Nil
      if @handle != LibPortAudio::PPaStream.null
        PortAudio.raiseIfError { LibPortAudio.Pa_StopStream(@handle) }
      end
    end

    def close : Nil
      PortAudio.raiseIfError { LibPortAudio.Pa_CloseStream(@handle) }
      @handle = LibPortAudio::PPaStream.null
    end

    def stopped? : Bool
      case PortAudio.raiseIfError { LibPortAudio.Pa_IsStreamStopped(@handle) }
      when 0 then false
      when 1 then true
      else raise "Unexpected PulseAudio error"
      end
    end

    def active? : Bool
      case PortAudio.raiseIfError { LibPortAudio.Pa_IsStreamActive(@handle) }
      when 0 then false
      when 1 then true
      else raise "Unexpected PulseAudio error"
      end
    end

    def time : Float64
      LibPortAudio.Pa_GetStreamTime(@handle)
    end

    def writeAvailable : Int64
      PortAudio.raiseIfError { LibPortAudio.Pa_GetStreamWriteAvailable(@handle) }
    end

    def readAvailable : Int64
      PortAudio.raiseIfError { LibPortAudio.Pa_GetStreamReadAvailable(@handle) }
    end

    private macro defineReadFn(dataType, sampleType)
      # Attempts to read samples from an input stream into the destination.  The
      # method doesn't return until the entire buffer has been filled - this may
      # involve waiting for the operating system to supply the data.
      #
      # The size of the destination must be less than or equal to the number of
      # frames per buffer multiplied by the channel count.
      #
      # On success, this returns `true`.  If input data was discarded by PortAudio
      # after the previous call and before this call, this returns `false`.
      # Otherwise this will raise a `PaError` if another error occurs.
      def read(dest : {{dataType}}) : Bool
        unless @inputSampleFormat == {{sampleType}}
          raise PaError.new("Attempt to read #{ {{sampleType}} } samples from an stream that does not provide #{ {{sampleType}} } input")
        end

        unless @inputChannels > 0
          raise PaError.new("Attempt to read samples from a stream with no input channels")
        end

        unless dest.size < (@framesPerBuffer * @inputChannels)
          raise PaError.new("Attempt to read samples into a slice that is not large enough")
        end

        begin
          PortAudio.raiseIfError do
            LibPortAudio.Pa_ReadStream(@handle, dest.to_unsafe, @framesPerBuffer)
          end
          true
        rescue err : PaError
          if err.errCode == LibPortAudio::PaError::InputOverflowed
            return false
          else
            raise err
          end
        end
      end

      # Attempts to read samples from an input stream.  The method doesn't
      # return until one entire buffer has been filled - this may involve
      # waiting for the operating system to supply the data. The size of
      # returned data equal to the number of frames per buffer multiplied by the
      # channel count.
      #
      # On success, this returns the batch of data.  Otherwise this will raise a
      # `PaError` if another error occurs.
      def read(destType : {{dataType}}.class) : {{dataType}}
        dest = {{dataType}}.new(@framesPerBuffer * @inputChannels, 0)
        read(dest)
        dest
      end
    end

    defineReadFn(Slice(Float32), SampleFormat::Float32)
    defineReadFn(Array(Float32), SampleFormat::Float32)
    defineReadFn(Slice(Int32), SampleFormat::Int32)
    defineReadFn(Array(Int32), SampleFormat::Int32)
    defineReadFn(Slice(Int32), SampleFormat::Int24)
    defineReadFn(Array(Int32), SampleFormat::Int24)
    defineReadFn(Slice(Int16), SampleFormat::Int16)
    defineReadFn(Array(Int16), SampleFormat::Int16)
    defineReadFn(Slice(Int8), SampleFormat::Int8)
    defineReadFn(Array(Int8), SampleFormat::Int8)
    defineReadFn(Slice(UInt8), SampleFormat::UInt8)
    defineReadFn(Array(UInt8), SampleFormat::UInt8)

    private macro defineSeparationFn(dataType, innerType)
      # A utility function that separates interleaved data into individual
      # channels.
      def self.separateArray(channelCount : Int, data : {{dataType}}) : Array({{innerType}})
        if data.size != channelCount * @framesPerBuffer
          raise PaError.new("Data of size #{data.size} cannot be separated into #{channelCount} channel#{channelCount == 1 ? "" : "s"} when the frames per buffer is #{framesPerBuffer}")
        end

        ret = [] of Array({{innerType}})

        channelCount.times do |chan|
          ret << Array({{innerType}}).new(@framesPerBuffer.size) do |frame|
            data[(frame * channelCount) + channel]
          end
        end

        ret
      end
    end

    defineSeparationFn(Slice(Float32), Float32)
    defineSeparationFn(Slice(Int32), Int32)
    defineSeparationFn(Slice(Int16), Int16)
    defineSeparationFn(Slice(Int8), Int8)
    defineSeparationFn(Slice(UInt8), UInt8)
    defineSeparationFn(Array(Float32), Float32)
    defineSeparationFn(Array(Int32), Int32)
    defineSeparationFn(Array(Int16), Int16)
    defineSeparationFn(Array(Int8), Int8)
    defineSeparationFn(Array(UInt8), UInt8)

    private macro defineMergeFn(dataType)
      # A utility function that merges individual channels into interleaved
      # data.
      def self.mergeArrays(channelCount : Int, *data : {{dataType}}) : {{dataType}}
        if data.size != channelCount * @framesPerBuffer
          raise PaError.new("Data of size #{data.size} cannot be separated into #{channelCount} channel#{channelCount == 1 ? "" : "s"} when the frames per buffer is #{framesPerBuffer}")
        end

        frame = 0
        channel = 0
        {{dataType}}.new(channelCount * @framesPerBuffer) do |_|
          ret = data[channel][frame]
          frame += 1
          if frame == @framesPerBuffer
            frame = 0
            channel += 1
          end
          ret
        end
      end
    end

    defineMergeFn(Slice(Float32))
    defineMergeFn(Slice(Int32))
    defineMergeFn(Slice(Int16))
    defineMergeFn(Slice(Int8))
    defineMergeFn(Slice(UInt8))
    defineMergeFn(Array(Float32))
    defineMergeFn(Array(Int32))
    defineMergeFn(Array(Int16))
    defineMergeFn(Array(Int8))
    defineMergeFn(Array(UInt8))

    private macro defineWriteFn(dataType, sampleType)
      # Writes sample data to an output stream.  This function doesn't return
      # until the entire buffer has been consumed - this may involve waiting for
      # the operating system to consume the data. The size of the data must be
      # equal to the frames per buffer multiplied by the number of output
      # channels.
      #
      # This returns `self`.
      def <<(data : {{dataType}}) : PaStream
        unless @outputSampleFormat == {{sampleType}}
          raise PaError.new("Cannot write #{ {{sampleType}} } data to a stream that does not accept #{ {{sampleType}} } data")
        end

        if data.size != (@outputChannels.not_nil! * @framesPerBuffer)
          raise PaError.new("Not enough data to write to to the stream (need #{@outputChannels.not_nil! * @framesPerBuffer}, got #{data.size})")
        end

        PortAudio.raiseIfError do
          LibPortAudio.Pa_WriteStream(@handle, data.to_unsafe, @framesPerBuffer)
        end

        self
      end
    end

    defineWriteFn(Slice(Float32), SampleFormat::Float32)
    defineWriteFn(Array(Float32), SampleFormat::Float32)
    defineWriteFn(Slice(Int32), SampleFormat::Int32)
    defineWriteFn(Array(Int32), SampleFormat::Int32)
    defineWriteFn(Slice(Int32), SampleFormat::Int24)
    defineWriteFn(Array(Int32), SampleFormat::Int24)
    defineWriteFn(Slice(Int16), SampleFormat::Int16)
    defineWriteFn(Array(Int16), SampleFormat::Int16)
    defineWriteFn(Slice(Int8), SampleFormat::Int8)
    defineWriteFn(Array(Int8), SampleFormat::Int8)
    defineWriteFn(Slice(UInt8), SampleFormat::UInt8)
    defineWriteFn(Array(UInt8), SampleFormat::UInt8)

    def info : StreamInfo
      StreamInfo.new(LibPortAudio.Pa_GetStreamInfo(@handle))
    end
  end
end