Artifact 0b68bad56471009a370ced15acf63b34c9519cec428c278c4b415c315ea4da43:

  • File src/remiaudio/formats/sampleinfo.cr — part of check-in [98921eb869] at 2024-01-05 07:36:37 on branch trunk — Copyright update (user: alexa size: 5457)

#### 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 "math"
require "libremiliacr"
require "../common"
require "./au"
require "./wav"

# The `SampleInfo` structure is used to get various information about the
# samples in an `AudioFile`.
struct RemiAudio::Formats::SampleInfo
  class FormatError < Exception
  end

  # The minimum sample value, as a `::Float64` sample values.
  getter min : Float64 = 0.0

  # The maximum sample value, as a `::Float64` sample values.  This is the
  # maximum among all channels.
  getter max : Float64 = 0.0

  # The peak amplitude of this file, in decibels.  This is the peak among all
  # channels.
  getter peak : Float64 = 0.0
  # The average (RMS) amplitude of this file, in decibels.  This is the
  # average between all channels.
  getter rms : Float64 = 0.0

  # The total number of samples across all channels.
  getter numSamples : UInt64 = 0u64

  # The total number of samples per channels.
  getter samplesPerChan : UInt64 = 0u64

  # The minimum sample value, as a `::Float64` sample values, for each
  # channel.
  getter minPerChan : Array(Float64) = [] of Float64

  # The maximum sample value, as a `::Float64` sample values, for each
  # channel.
  getter maxPerChan : Array(Float64) = [] of Float64

  # The peak amplitude of this file per channel, in decibels.
  getter peakPerChan : Array(Float64) = [] of Float64

  # The average (RMS) amplitude of this file for each channel, in decibels.
  getter rmsPerChan : Array(Float64) = [] of Float64

  # The sample rate of the source audio.
  getter sampleRate : UInt32 = 0u32

  # The bit depth of the source audio.
  getter bitDepth : UInt32 = 0u32

  # Creates a new `SampleInfo` by reading the currently available samples in
  # *aud*
  def initialize(aud : AudioFile)
    @sampleRate = aud.sampleRate
    @bitDepth = aud.bitDepth.to_u32

    # Nothing to do?
    return if aud.channels == 0 || aud.numSamples == 0

    @maxPerChan = Array(Float64).new(aud.channels, Float64::MIN)
    @minPerChan = Array(Float64).new(aud.channels, Float64::MAX)
    @peakPerChan = Array(Float64).new(aud.channels, Float64.zero)
    @rmsPerChan = Array(Float64).new(aud.channels, Float64.zero)
    ch : UInt32 = 0u32

    aud.withExcursion(0) do
      while smp = aud.read?
        # Gather min/max
        @maxPerChan[ch] = smp if smp > @maxPerChan[ch]
        @minPerChan[ch] = smp if smp < @minPerChan[ch]

        # Gather RMS
        @rmsPerChan[ch] += (smp ** 2)

        # Update other info
        ch = (ch + 1) % aud.channels
        @numSamples += 1
      end
    end

    # Consistency check.
    unless @samplesPerChan % aud.channels == 0 && @numSamples == aud.numSamples
      raise RemiAudioError.new("The number of samples (#{@numSamples}) does not match the expected value")
    end

    # Gather peaks
    aud.channels.times do |ch|
      @peakPerChan[ch] = if @maxPerChan[ch].abs > @minPerChan[ch].abs
                           RemiAudio.linearToDecibels(@maxPerChan[ch].abs)
                         else
                           RemiAudio.linearToDecibels(@minPerChan[ch].abs)
                         end
    end

    # Determine the number of samples per channel.
    @samplesPerChan = @numSamples.tdiv(aud.channels)

    # Calculate RMS for each channel
    @rmsPerChan.map! do |val|
      val /= @samplesPerChan
      RemiAudio.linearToDecibels(Math.sqrt(val))
    end

    # Get the other overall values
    @max = @maxPerChan.max
    @min = @minPerChan.min
    @peak = @peakPerChan.max
    @rms = @rmsPerChan.sum / @rmsPerChan.size
  end

  # Creates a new `SampleInfo` instance containing the information for the
  # given Au file.
  def self.fromAu(filename : String|Path) : SampleInfo
    AuFile.open(filename) { |wav| SampleInfo.new(wav) }
  end

  # Creates a new `SampleInfo` instance containing the information for the
  # given RIFF WAVE file.
  def self.fromWav(filename : String|Path) : SampleInfo
    WavFile.open(filename) { |wav| SampleInfo.new(wav) }
  end

  # Attempts to determine the format of *filename*, then creates a new
  # `SampleInfo` instance containing the information for that file.
  #
  # If this cannot determine the format, it will raise a `FormatError`.
  def self.from(filename : String|Path) : SampleInfo
    typ = File.open(filename, "rb") do |f|
      str = f.read_string(4)
      case str
      when AU_MAGIC
        :au
      when "RIFF"
        f.readUInt32
        str = f.read_string(4)
        if str == "WAVE"
          :wav
        else
          nil
        end
      else nil
      end
    end

    case typ
    when :au then fromAu(filename)
    when :wav then fromWav(filename)
    else raise FormatError.new("Cannot determine the file format")
    end
  end
end