#### Benben
#### Copyright (C) 2023-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 "./oggjob"
module Benben::Rendering
# A render job that converts an Mpeg1 file into a WAV/Au.
class Mpeg1Job < Job
@ctx : Mpeg1File
@bufSize : Int32
@bufRealSize : Int32
@renderBuf : Array(Float32) = [] of Float32
@audioBuf : Array(Float32)
@audioBuf64 : Array(Float64) = [] of Float64
@convFn : Proc(Float64, Float32) | Proc(Float64, Float64) | Proc(Float64, Int16) |
Proc(Float64, Int32) | Proc(Float64, Int64) | Proc(Float64, Int8) | Proc(Float64, UInt8)
protected getter! stream : AudioFile?
@samplesPlayed : UInt32 = 0
@samplesRendered : Int64 = 0i64
def initialize(mpeg1 : Mpeg1File, @inFilename : Path, @outFilename : Path, @doneChan : JobChannel)
@source = mpeg1
@ctx = mpeg1
@totalSamples = @ctx.totalSamples.to_u64
@settings = Benben.config.dup
@reverb = Reverb.new(DEF_RENDER_BUFFER_SIZE)
@bufSize = DEF_RENDER_BUFFER_SIZE
@bufRealSize = @bufSize * 2 # multiplied by 2 for stereo
@audioBuf64 = Array(Float64).new(@bufRealSize, 0.0)
@audioBuf = Array(Float32).new(@bufRealSize, 0.0f32)
# Do we need to do resampling?
if @ctx.sampleRate != @settings.sampleRate
initResampler(@ctx.sampleRate, @settings.resampler)
@renderBuf = Array(Float32).new(@bufRealSize, 0.0f32)
end
# ReplayGain
@ctx.replayGain = @settings.replayGain
#
# Some settings need to be setup now so that the Renderer can calculate
# samples correctly, and simply because they don't need to be done in the
# render method.
#
@settings.maybeApplySongConfig(@inFilename)
@settings.apply(Benben.args) # Command line overrides everything
@eq = @settings.makeEQ # Setup the EQ. We always have an EQ in memory.
@eq.active = false if @settings.noEQDuringRendering?
# We don't use this, set it to zero
@nonLoopingExtraSamples = 0
# Get the function we'll use to convert the samples.
@convFn = getConversionFn
end
def render(statusChan : ProgressChan) : Nil
# Send settings to other things.
@settings.send(@reverb)
# Get the output stream and start it.
File.delete(@outFilename) if File.exists?(@outFilename) # This speeds things up???
@stream = getStream(@outFilename)
begin
# Main rendering loop
until @samplesPlayed > @totalSamples
# Render the buffer. This is a purposely unhygenic macro.
renderBuffers
# Write samples to the file, mapping to the correct format as-needed.
statusChan.send(DEF_RENDER_BUFFER_SIZE.to_u32!)
Fiber.yield
end
rescue err : Exception
@errored = true
@doneChan.send(RenderingError.new(@outFilename, "Failed to render file: #{err}", err))
return
ensure
stream.close
@ctx.unload
end
@doneChan.send(true)
end
# Calculates the total number of samples that this job will produce. These
# are stereo samples, so one sample = left and right. The value is
# memoized.
def calcTotalSampleSize : UInt32
@totalSamples.to_u32
end
protected def renderCb : Tuple(Slice(Float32), Int64)
rendered = @ctx.decode(@renderBuf).tdiv(4) # The decoder gives us the number of bytes back, so divide by 4.
if rendered >= 0
if rendered < @renderBuf.size
@renderBuf.fill(0.0f32, rendered..)
end
end
# Divide by 2 because because 2 channels
{renderBufSlice(@renderBuf), rendered.tdiv(2).to_i64!}
rescue err : Exception
# On error, just return zero
{renderBufSlice(@renderBuf), 0i64}
end
@[AlwaysInline]
private def renderBuffers
# Render the audio, then process
if resamp = @resampler
# We're resampling, so use that to get data
@samplesRendered = resamp.read(@resampRatio, @audioBuf)
else
@samplesRendered = @ctx.decode(@audioBuf)
if @samplesRendered > 0
if @samplesRendered < @audioBuf.size
# Fill remaining buffer with silence
@audioBuf.fill(0.0f32, @samplesRendered..)
end
end
end
sendToFile
end
@[AlwaysInline]
protected def sendToFile
if @samplesRendered > 0
newMax : Float64 = 0.0
# Convert to Float64
@audioBuf.size.times do |idx|
@audioBuf64[idx] = @audioBuf[idx].to_f64!
end
if @settings.enableStereoEnhancer?
RemiAudio::DSP::StereoEnhancer.process(@audioBuf64, @settings.stereoEnhancementAmount)
end
@eq.process(@audioBuf64)
@reverb.process(@audioBuf64)
if @settings.enableSoftClipping?
@audioBuf64.size.times do |idx|
# About -0.3db
@audioBuf64.put!(idx, RemiAudio::DSP::SoftClipping.process(0.9660508789898133, @audioBuf64.get!(idx)))
end
end
@max = newMax if (newMax = @audioBuf64.max) > @max
@min = newMax if (newMax = @audioBuf64.min) < @min
@samplesPlayed += @bufSize
@audioBuf64.each do |smp|
self.stream.writeSample(@convFn.call(smp))
end
end
@audioPos = 0
end
def generateCue(cue : RemiAudio::Cue, cuePath : Path, idx : Int) : Nil
cue.addFile do |file|
# Construct the filename
file.filename = self.outFilename.relative_to(cuePath).to_s
tag = Id3TagInfo.new(@ctx)
# Set the type, add a single track
file.type = RemiAudio::Cue::File::Type::Wave
file.addTrack do |trk|
trk.title = tag.title
trk.performer = tag.artist
trk.songwriter = tag.composer
trk.trackNumber = idx.to_u32 + 1
trk.setIndex(0, RemiAudio::Cue::Timestamp.new)
trk.pregap = RemiAudio::Cue::Timestamp.new(0, 2, 0)
end
end
end
end
end