#### RemiAudio
#### Copyright (C) 2022-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 "libremiliacr"
require "../common"

# The Formats module contains routines and types for reading and writing various
# formats of audio.  This does not include audio codecs.
module RemiAudio::Formats
  # Base class for all errors related to `AudioFile` descendents.
  class AudioFileError < RemiAudioError
  end

  # Represents an error that occurs when attempting to use an encoding that is
  # not supported by this library.
  class UnsupportedEncodingError < AudioFileError
  end

  # Base class for all PCM- and IEEE Float-based audio file formats.
  abstract class AudioFile
    enum Type
      Au
      Wav
    end

    # The bit depth of this instance.
    getter bitDepth : UInt8 = 16u8

    # The sample rate of this instance.
    getter sampleRate : UInt32 = 44100u32

    # The number of channels this instance has.
    getter channels : UInt32 = 2u32

    # The current number of samples.
    getter numSamples : UInt64 = 0u64

    # The underlying IO.
    getter io : IO

    @changed : Bool = false
    @origIoPos : UInt64 = 0
    @blockSize : UInt16 = 0
    @dataStartsAt : UInt64 = 0
    @dataSizeInBytes : UInt64 = 0

    @sampleReadFn : Proc(Sample) = ->AudioFile.cannotReadSample
    @samplesReadFn : Proc(SampleData, Int32) = ->AudioFile.cannotReadSamplesInto(SampleData)
    @sampleWriteFn : Proc(Sample, Nil) = ->AudioFile.cannotWriteSample(Sample)
    @samplesWriteFn : Proc(SampleData, Nil) = ->AudioFile.cannotWriteSamples(SampleData)
    @sampleToF64Fn : Proc(Sample, Float64) = ->AudioFile.cannotConvertToFloat64(Sample)
    @f64ToSampleFn : Proc(Float64, Sample) = ->AudioFile.cannotConvertToSample(Float64)
    @sampleToF32Fn : Proc(Sample, Float32) = ->AudioFile.cannotConvertToFloat32(Sample)
    @f32ToSampleFn : Proc(Float32, Sample) = ->AudioFile.cannotConvertToSample(Float32)

    private def self.determineFormat(io : IO)
      str = io.read_string(4)
      case str
      when "RIFF"
        io.readUInt32 # Skip chunk size
        str = io.read_string(4)
        if str == "WAVE"
          :wav
        else
          nil
        end
      when ".snd" then :au
      else nil
      end
    rescue Exception
      nil
    end

    def self.open(io : IO) : AudioFile
      oldPos = io.pos
      format = determineFormat(io)
      io.pos = oldPos
      case format
      when :wav then WavFile.open(io)
      when :au then AuFile.open(io)
      else raise AudioFileError.new("Not a recognized audio format")
      end
    end

    def self.open(io : IO, &) : Nil
      file = self.open(io)
      yield file
    end

    def self.open(filename : Path|String) : AudioFile
      format = File.open(filename, "rb") { |io| determineFormat(io) }
      case format
      when :wav then WavFile.open(filename)
      when :au then AuFile.open(filename)
      else raise AudioFileError.new("Not a recognized audio format")
      end
    end

    def self.open(filename : Path|String, &) : Nil
      file = self.open(filename)
      yield file
    end

    def self.test(filename : Path|String) : Type?
      file = self.open(filename)
      case file
      in WavFile then Type::Wav
      in AuFile then Type::Au
      in AudioFile then raise "Unknown type"
      end
    rescue Exception
      nil
    end

    private def initialize
      @io = IO::Memory.new
      @origIoPos = 0u64
      @blockSize = 0u16
      raise "Somehow triggered a private #initialize in an abstract class"
    end

    # Returns a `RemiAudio::SampleFormat` appropriate for this file.
    abstract def sampleFormat : SampleFormat

    # Returns `true` if this is a stereo audio file, or `false` otherwise.
    def stereo? : Bool
      self.channels == 2
    end

    # Returns `true` if this is a monaural audio file, or `false` otherwise.
    def mono? : Bool
      self.channels == 1
    end

    # Returns the current sample position.  This is different than calling
    # `io.pos` in that this is based on samples, not necessarily bytes.
    @[AlwaysInline]
    def pos : UInt64
      (@io.pos.to_u64! - @dataStartsAt).tdiv(@blockSize)
    end

    # Sets the current sample position.  This is different than calling `io.pos
    # = value` in that this is based on samples, not necessarily bytes.
    #
    # This will raise an `::ArgumentError` if *value* is less than zero.
    @[AlwaysInline]
    def pos=(value : Int) : Nil
      if value < 0
        raise ::ArgumentError.new("pos cannot be negative")
      end

      actualPos : UInt64 = @dataStartsAt + (value * @blockSize)
      if actualPos == (@dataStartsAt + @dataSizeInBytes)
        actualPos -= 1
      elsif actualPos > (@dataStartsAt + @dataSizeInBytes)
        raise "Unexpected position"
      end

      @io.pos = actualPos
    end

    # Rewinds the internal read cursor to the beginning of the sample data.
    def rewind : Nil
      self.pos = 0
    end

    # Temporary seeks to the given sample number, yields the block, then returns
    # to the previous sample number.  This returns the final value in the block.
    def withExcursion(newSamplePos : Int32, &)
      oldPos = self.pos
      begin
        self.pos = newSamplePos
        yield
      ensure
        self.pos = oldPos
      end
    end

    # Seeks to the end of the samples.  Writing a sample after this will add a
    # new sample into this instance.
    def toEnd : Nil
      self.pos = @numSamples
    end

    # Loops over all of the samples within this instance, yielding each one to
    # the block as a `::Float64` sample value.
    #
    # This will call `#withExcursion` to set the position to the first sample,
    # so once the block finishes, the internal read cursor will be back at its
    # original position.
    def each(& : Float64 ->) : Nil
      withExcursion(0) do
        while smp = read?
          yield smp
        end
      end
    end

    # Loops over all of the samples within this instance, yielding each one to
    # the block as a `::Float32` sample value.
    #
    # This will call `#withExcursion` to set the position to the first sample,
    # so once the block finishes, the internal read cursor will be back at its
    # original position.
    def eachF32(& : Float32 ->) : Nil
      withExcursion(0) do
        while smp = readF32?
          yield smp
        end
      end
    end

    # Loops over all of the samples within this instance, yielding each one to
    # the block as a `RemiAudio::Sample` value.
    #
    # This will call `#withExcursion` to set the position to the first sample,
    # so once the block finishes, the internal read cursor will be back at its
    # original position.
    def eachSample(& : Sample ->) : Nil
      withExcursion(0) do
        while smp = readSample?
          yield smp
        end
      end
    end

    # Reads a single sample and returns it as a `::Float64` sample value.  If no
    # more samples are available, this raises an `::IO::EOFError`.
    def read : Float64
      self.read? || raise ::IO::EOFError.new("No more samples")
    end

    # Attempts to read a single sample, returning it as a `::Float64` sample
    # value on success, or `nil` if no more samples can be read.
    @[AlwaysInline]
    def read? : Float64?
      readSample?.try do |smp|
        @sampleToF64Fn.call(smp)
      end
    end

    # Reads a single sample and returns it as a `::Float32` sample value.  If no
    # more samples are available, this raises an `::IO::EOFError`.
    def readF32 : Float32
      self.readF32? || raise ::IO::EOFError.new("No more samples")
    end

    # Attempts to read a single sample, returning it as a `::Float32` sample
    # value on success, or `nil` if no more samples can be read.
    @[AlwaysInline]
    def readF32? : Float32?
      readSample?.try do |smp|
        @sampleToF32Fn.call(smp)
      end
    end

    # Reads a single sample and returns it as a `RemiAudio::Sample` sample
    # value.  If no more samples are available, this raises an `::IO::EOFError`.
    def readSample : Sample
      self.readSample? || raise ::IO::EOFError.new("No more samples")
    end

    # Attempts to read a single sample, returning it as a `RemiAudio::Sample`
    # sample value on success, or `nil` if no more samples can be read.
    @[AlwaysInline]
    def readSample? : Sample?
      @sampleReadFn.call
    rescue IO::EOFError
      nil
    end

    # Reads a single sample from an arbitrary point in the file and returns it
    # as a `::Float64` sample value.  This does not change the current read
    # position, thus allowing random access reading.
    def read(pos : Int) : Float64
      withExcursion(pos.to_i32) { read }
    end

    # Reads up to `dest.size` samples as `::Float64` sample values into *dest*.
    # The actual number of samples read is then returned.
    abstract def read(dest : Array(Float64)) : Int32

    # Reads up to `dest.size` samples as `::Float32` sample values into *dest*.
    # The actual number of samples read is then returned.
    abstract def read(dest : Array(Float32)) : Int32

    # Reads a single sample from an arbitrary point in the file.  This does not
    # change the current read position, thus allowing random access reading.
    def readSample(pos : Int) : Sample
      withExcursion(pos.to_i32) { readSample }
    end

    # Reads up to `dest.size` samples as `RemiAudio::Sample` values into *dest*.
    # The actual number of samples read is then returned.
    def readSamples(dest : SampleData) : Int32
      @samplesReadFn.call(dest)
    end

    # Reads all remaining samples, returning them as `::Float64` sample values.
    def readToEnd : Array(Float64)
      curSample = self.pos
      toRead = @numSamples - curSample
      Array(Float64).new(toRead) { |_| read }
    end

    # Reads all remaining samples, returning them as `::Float32` sample values.
    def readToEndF32 : Array(Float32)
      curSample = self.pos
      toRead = @numSamples - curSample
      Array(Float32).new(toRead) { |_| readF32 }
    end

    # Reads all remaining samples, returning them as `RemiAudio::SampleData`.
    abstract def readSamplesToEnd : SampleData

    # Writes a single sample, then returns `self`.
    @[AlwaysInline]
    def write(value : Float64) : self
      writeSample(@f64ToSampleFn.call(value))
      @changed = true
      self
    end

    # Writes a single sample, then returns `self`.
    @[AlwaysInline]
    def write(value : Float32) : self
      writeSample(@f32ToSampleFn.call(value))
      @changed = true
      self
    end

    # :ditto:
    @[AlwaysInline]
    def writeSample(value : Sample) : self
      @sampleWriteFn.call(value)
      @changed = true
      self
    end

    # Writes *samples*, then returns `self`.
    @[AlwaysInline]
    def write(samples : Array(Float64)) : self
      samples.each { |smp| @sampleWriteFn.call(@f64ToSampleFn.call(smp)) }
      @changed = true
      self
    end

    # :ditto:
    @[AlwaysInline]
    def write(samples : Array(Float32)) : self
      samples.each { |smp| @sampleWriteFn.call(@f32ToSampleFn.call(smp)) }
      @changed = true
      self
    end

    # :ditto:
    @[AlwaysInline]
    def writeSamples(samples : SampleData) : self
      samples.each { |smp| @sampleWriteFn.call(smp) }
      @changed = true
      self
    end

    # Flushes the underlying `IO` and updates any header fields.
    abstract def flush : Nil

    # Flushes the underlying `IO` then closes it.
    def close : Nil
      unless @io.closed?
        self.flush
        @io.close
      end
    end

    # Same as `#write`.
    @[AlwaysInline]
    def <<(sample : Float64) : self
      write(sample)
    end

    # :ditto:
    @[AlwaysInline]
    def <<(sample : Float32) : self
      write(sample)
    end

    # Writes the current state of this file to *dest*.  This will automatically
    # call `#flush` before writing.
    abstract def copyTo(dest : IO) : Nil

    ############################################################################

    # Attempts to read a "four CC" string, as used in RIFF formats.  If four
    # bytes cannot be read, this returns `nil`.
    protected def readFourCC : String?
      data = Bytes.new(4)
      numRead = @io.read(data)
      return nil unless numRead == 4
      data.map! do |val|
        unless (32 <= val) && (val <= 126)
          '?'.ord.to_u8
        else
          val
        end
      end
      String.new(data)
    end

    # Attempts to read a RIFF Chunk ID.  The chunk ID must be equal to
    # `chunkType`.  If it is, this returns the chunk that was read, otherwise
    # this raises an `AudioFileError`.
    #
    # If `message` is specified, then the error uses that as the message,
    # otherwise it uses a more generic error message that lists what it found
    # and the expected chunk name.
    protected def expectChunkType(chunkType : String, message : String? = nil) : String?
      chunk = readFourCC
      unless chunk == chunkType
        raise AudioFileError.new(message || "The chunk type must be '#{chunkType}', not '#{chunk}'")
      end
      chunk
    end

    # Attempts to read a RIFF Chunk ID.  The chunk ID must be one of the chunk
    # names listed in `chunkTypes`.  If it is, this returns the chunk that was
    # read, otherwise this raises an `AudioFileError`.
    #
    # If `message` is specified, then the error uses that as the message,
    # otherwise it uses a more generic error message that lists what it found
    # and the expected chunk name.
    protected def expectChunkType(chunkTypes : Array(String), message : String? = nil) : String?
      chunk = readFourCC
      unless chunkTypes.includes?(chunk)
        raise AudioFileError.new(message || "The chunk type must be one of '#{chunkTypes}', not '#{chunk}'")
      end
      chunk
    end

    ############################################################################

    {% begin %}
      {% for fn in [[:int8ToF64, Int8, Int8::MAX],
                    [:int16ToF64, Int16, Int16::MAX],
                    [:int24ToF64, Int32, (2.0 ** 23) - 1],
                    [:int32ToF64, Int32, Int32::MAX],
                    [:int64ToF64, Int64, Int64::MAX]] %}
        protected def {{fn[0].id}}(smp : Sample) : Float64
          smp.as({{fn[1].id}}) / {{fn[2]}}
        end
      {% end %}
    {% end %}

    protected def uint8ToF64(smp : Sample) : Float64
      (smp.as(UInt8).to_i16 - 128) / Int8::MAX
    end

    protected def float32ToF64(smp : Sample) : Float64
      smp.as(Float32).to_f64!
    end

    protected def float64ToF64(smp : Sample) : Float64
      smp.as(Float64)
    end

    ############################################################################

    {% begin %}
      {% for fn in [[:int8ToF32, Int8, Int8::MAX],
                    [:int16ToF32, Int16, Int16::MAX],
                    [:int24ToF32, Int32, (2.0 ** 23) - 1],
                    [:int32ToF32, Int32, Int32::MAX],
                    [:int64ToF32, Int64, Int64::MAX]] %}
        protected def {{fn[0].id}}(smp : Sample) : Float32
          (smp.as({{fn[1].id}}) / {{fn[2]}}).to_f32!
        end
      {% end %}
    {% end %}

    protected def uint8ToF32(smp : Sample) : Float32
      ((smp.as(UInt8).to_i16 - 128) / Int8::MAX).to_f32!
    end

    protected def float32ToF32(smp : Sample) : Float32
      smp.as(Float32)
    end

    protected def float64ToF32(smp : Sample) : Float32
      smp.as(Float64).to_f32!
    end

    ############################################################################

    {% begin %}
      {% for fn in [[:f64ToInt8, :to_i8!, Int8::MAX],
                    [:f64ToInt16, :to_i16!, Int16::MAX],
                    [:f64ToInt24, :to_i32!, (2.0 ** 23) - 1],
                    [:f64ToInt32, :to_i32!, Int32::MAX],
                    [:f64ToInt64, :to_i64!, Int64::MAX]] %}
        protected def {{fn[0].id}}(smp : Float64) : Sample
          (smp * {{fn[2]}}).{{fn[1].id}}.as(Sample)
        end
      {% end %}
    {% end %}

    protected def f64ToFloat32(smp : Float64) : Sample
      smp.to_f32!.as(Sample)
    end

    protected def f64ToFloat64(smp : Float64) : Sample
      smp.as(Sample)
    end

    protected def f64ToUInt8(smp : Sample) : Sample
      (smp * Int8::MAX + 128).to_u8!.as(Sample)
    end

    ############################################################################

    {% begin %}
      {% for fn in [[:f32ToInt8, :to_i8!, Int8::MAX],
                    [:f32ToInt16, :to_i16!, Int16::MAX],
                    [:f32ToInt24, :to_i32!, (2.0 ** 23) - 1],
                    [:f32ToInt32, :to_i32!, Int32::MAX],
                    [:f32ToInt64, :to_i64!, Int64::MAX]] %}
        protected def {{fn[0].id}}(smp : Float32) : Sample
          (smp * {{fn[2]}}).{{fn[1].id}}.as(Sample)
        end
      {% end %}
    {% end %}

    protected def f64ToFloat32(smp : Float64) : Sample
      smp.to_f32!.as(Sample)
    end

    protected def f32ToFloat32(smp : Float32) : Sample
      smp.as(Sample)
    end

    protected def f32ToUInt8(smp : Sample) : Sample
      (smp * Int8::MAX + 128).to_u8!.as(Sample)
    end

    ############################################################################

    {% begin %}
      {% for fn in [[:fastReadSampleUInt8, :readUInt8],
                    [:fastReadSampleInt8, :readInt8],
                    [:fastReadSampleInt16, :readInt16],
                    [:fastReadSampleInt24, :readInt24],
                    [:fastReadSampleInt32, :readInt32],
                    [:fastReadSampleInt64, :readInt64],
                    [:fastReadSampleFloat32, :readFloat32],
                    [:fastReadSampleFloat64, :readFloat64],
                    [:fastReadSampleInt16BE, :readInt16BE],
                    [:fastReadSampleInt32BE, :readInt32BE],
                    [:fastReadSampleInt64BE, :readInt64BE],
                    [:fastReadSampleFloat32BE, :readFloat32BE],
                    [:fastReadSampleFloat64BE, :readFloat64BE]] %}
        {% name = fn[0] %}
        {% readFn = fn[1] %}
        @[AlwaysInline]
        protected def {{name.id}} : Sample
          @io.{{readFn.id}}.as(Sample)
        end
      {% end %}
    {% end %}

    @[AlwaysInline]
    protected def fastReadSampleInt24BEFourByte : Sample
      (@io.readInt32BE & 0xFFFFFF).as(Sample)
    end

    ############################################################################

    {% begin %}
      {% for fn in [[:fastReadSamplesUInt8, UInt8, :fastReadSampleUInt8],
                    [:fastReadSamplesInt8, Int8, :fastReadSampleInt8],
                    [:fastReadSamplesInt16, Int16, :fastReadSampleInt16],
                    [:fastReadSamplesInt24, Int32, :fastReadSampleInt24],
                    [:fastReadSamplesInt32, Int32, :fastReadSampleInt32],
                    [:fastReadSamplesInt64, Int64, :fastReadSampleInt64],
                    [:fastReadSamplesFloat32, Float32, :fastReadSampleFloat32],
                    [:fastReadSamplesFloat64, Float64, :fastReadSampleFloat64],
                    [:fastReadSamplesInt16BE, Int16, :fastReadSampleInt16BE],
                    [:fastReadSamplesInt24BEFourByte, Int32, :fastReadSampleInt24BEFourByte],
                    [:fastReadSamplesInt32BE, Int32, :fastReadSampleInt32BE],
                    [:fastReadSamplesInt64BE, Int64, :fastReadSampleInt64BE],
                    [:fastReadSamplesFloat32BE, Float32, :fastReadSampleFloat32BE],
                    [:fastReadSamplesFloat64BE, Float64, :fastReadSampleFloat64BE]] %}
        {% name = fn[0] %}
        {% typ = fn[1] %}
        {% readFn = fn[2] %}
        protected def {{name.id}}(dest : SampleData) : Int32
          i : Int32 = 0
          smp : Sample?
          destPtr = dest.as(Array({{typ.id}})).to_unsafe
          begin
            until i == dest.size
              if smp = {{readFn.id}}
                destPtr[i] = smp.as({{typ.id}})
              else
                break
              end
              i += 1
            end
          rescue IO::EOFError
          end
          i
        end
      {% end %}
    {% end %}

    protected def fastReadSamplesInt24BEFourByte(dest : SampleData) : Int32
      i : Int32 = 0
      smp : Int32?
      destPtr = dest.as(Array(Int32)).to_unsafe
      begin
        until i == dest.size
          smp = @io.readInt24
          if smp && @io.read_byte
            destPtr[i] = smp.as(Int32)
          else
            break
          end
          i += 1
        end
      rescue IO::EOFError
      end
      i
    end

    ############################################################################

    {% begin %}
      {% for fn in [[:fastWriteSampleUInt8, UInt8, :write_byte],
                    [:fastWriteSampleInt8, Int8, :writeInt8],
                    [:fastWriteSampleInt16, Int16, :writeInt16],
                    [:fastWriteSampleInt24, Int32, :writeInt24],
                    [:fastWriteSampleInt32, Int32, :writeInt32],
                    [:fastWriteSampleInt64, Int64, :writeInt64],
                    [:fastWriteSampleFloat32, Float32, :writeFloat32],
                    [:fastWriteSampleFloat64, Float64, :writeFloat64],
                    [:fastWriteSampleInt16BE, Int16, :writeInt16BE],
                    [:fastWriteSampleInt32BE, Int32, :writeInt32BE],
                    [:fastWriteSampleInt64BE, Int64, :writeInt64BE],
                    [:fastWriteSampleFloat32BE, Float32, :writeFloat32BE],
                    [:fastWriteSampleFloat64BE, Float64, :writeFloat64BE]] %}
        {% name = fn[0] %}
        {% typ = fn[1] %}
        {% writeFn = fn[2] %}
        @[AlwaysInline]
        protected def {{name.id}}(smp : Sample) : Nil
          @io.{{writeFn.id}}(smp.as({{typ.id}}))
          @numSamples += 1
        end
      {% end %}
    {% end %}

    @[AlwaysInline]
    protected def fastWriteSampleInt24BEFourByte(smp : Sample) : Nil
      @io.writeInt24BE(smp.as(Int32) & 0xFFFFFF)
      @io.write_byte(0u8)
      @numSamples += 1
    end

    ############################################################################

    {% begin %}
      {% for fn in [[:fastWriteSamplesUInt8, UInt8, :write_byte],
                    [:fastWriteSamplesInt8, Int8, :writeInt8],
                    [:fastWriteSamplesInt16, Int16, :writeInt16],
                    [:fastWriteSamplesInt24, Int32, :writeInt24],
                    [:fastWriteSamplesInt32, Int32, :writeInt32],
                    [:fastWriteSamplesInt64, Int64, :writeInt64],
                    [:fastWriteSamplesFloat32, Float32, :writeFloat32],
                    [:fastWriteSamplesFloat64, Float64, :writeFloat64],
                    [:fastWriteSamplesInt16BE, Int16, :writeInt16BE],
                    [:fastWriteSamplesInt32BE, Int32, :writeInt32BE],
                    [:fastWriteSamplesInt64BE, Int64, :writeInt64BE],
                    [:fastWriteSamplesFloat32BE, Float32, :writeFloat32BE],
                    [:fastWriteSamplesFloat64BE, Float64, :writeFloat64BE]] %}
        {% name = fn[0] %}
        {% typ = fn[1] %}
        {% writeFn = fn[2] %}
        @[AlwaysInline]
        protected def {{name.id}}(samples : SampleData) : Nil
          samples.each { |smp| @io.{{writeFn.id}}(smp.as({{typ.id}})) }
          @numSamples += samples.size
        end
      {% end %}
    {% end %}

    @[AlwaysInline]
    protected def fastWriteSamplesInt24BEFourByte(samples : SampleData) : Nil
      samples.each do |smp|
        @io.writeInt24BE(smp.as(Int32) & 0xFFFFFF)
        @io.write_byte(0u8)
      end
      @numSamples += samples.size
    end

    ############################################################################

    protected def self.cannotConvertToFloat64(smp : Sample) : Float64
      raise "Cannot convert this encoding to Float64 natively"
    end

    protected def self.cannotConvertToFloat32(smp : Sample) : Float32
      raise "Cannot convert this encoding to Float32 natively"
    end

    protected def self.cannotConvertToSample(smp : Float64) : Sample
      raise "Cannot convert a Float64 to this encoding natively"
    end

    protected def self.cannotConvertToSample(smp : Float32) : Sample
      raise "Cannot convert a Float32 to this encoding natively"
    end

    protected def self.cannotReadSample : Sample
      raise "Cannot read a sample"
    end

    protected def self.cannotWriteSample(smp : Sample) : Nil
      raise "Cannot write a sample"
    end

    protected def self.cannotReadSamplesInto(data : SampleData) : Int32
      raise "Cannot read samples"
    end

    protected def self.cannotWriteSamples(smps : SampleData) : Nil
      raise "Cannot write samples"
    end
  end
end
