Artifact dd9a69916cc7aea67ede34fbf972df3f769b54e838c7a42fe80946bae9e3d8ee:

  • File src/rendering/mpeg1job.cr — part of check-in [36fe41c895] at 2024-06-20 12:25:28 on branch trunk — Add some missing Fiber.yield calls (user: alexa size: 6709)

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