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