Artifact 205a1b7755b4c2268f3ebfef1db722fab1f386ed49c6d22fb121041ba91668f2:

  • File src/taginfo.cr — part of check-in [af1c947a0d] at 2024-09-13 22:22:22 on branch move-ogg-formats-to-racodecs — Move Opus and Vorbis decoding to RACodecs. This hasn't been tested well yet, but it appears to work. (user: alexa size: 8432) [more...]

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

####
#### Base Tag Information
####

module Benben
  abstract class TagInfo
    property title : String = ""
    property artist : String = ""
    property album : String = ""
    property date : String = ""
    property genre : String = ""

    def self.create(file : PlayableFile) : TagInfo
      case file
      in VgmFile then VgmTagInfo.new(file.file)
      in ModuleFile then ModuleTagInfo.new(file)
      in FlacFile then VorbisTagInfo.new(file)
      in OpusFile then VorbisTagInfo.new(file)
      in VorbisFile then VorbisTagInfo.new(file)
      in Mpeg1File then Id3TagInfo.new(file)
      in MidiFile then MidiTagInfo.new(file)
      in PcmFile then PcmTagInfo.new(file)
      in QoaFile then QoaTagInfo.new(file)
      in WavPackFile then ApeTagInfo.new(file)
      in PlayableFile then raise "Forgot to update case statement"
      end
    end
  end

  module ReplayInfoTagsMixin
    @trackGain : Float64?
    @trackPeak : Float64?
    @albumGain : Float64?
    @albumPeak : Float64?

    protected def parseGain(value : String) : Float64
      str = value.strip.downcase
      if str.ends_with?("db")
        str = str[0...-2].strip
      end

      if ret = str.to_f64?
        ret
      else
        1.0
      end
    end

    protected def parsePeak(value : String) : Float64
      if ret = value.strip.to_f64?
        ret
      else
        1.0
      end
    end

    def trackGain : Float64
      @trackGain || @albumGain || 0.0
    end

    def trackPeak : Float64
      @trackPeak || 0.0
    end

    def albumGain : Float64
      @albumGain || @trackGain || 0.0
    end

    def albumPeak : Float64
      @albumPeak || 0.0
    end

    def trackGainLinear : Float64
      10.0 ** (self.trackGain / 20.0)
    end

    def albumGainLinear : Float64
      10.0 ** (self.albumGain / 20.0)
    end
  end

  class ModuleTagInfo < TagInfo
    property type : String = ""
    property tracks : String = ""
    property patterns : String = ""
    property instruments : String = ""
    property tracksPerPat : String = ""

    def initialize(file : ModuleFile)
      info = file.info(true)
      info.detailed.try do |dtinfo|
        @title = dtinfo.name
        @type = dtinfo.type
        @tracks = dtinfo.numTracks.to_s
        @patterns = dtinfo.numPatterns.to_s
        @instruments = dtinfo.numInstruments.to_s
        @tracksPerPat = dtinfo.tracksPerPattern.to_s
      end
    end
  end

  class VgmTagInfo < TagInfo
    property titleJp : String = ""
    property artistJp : String = ""
    property gameJp : String = ""
    property systemEn : String = ""
    property systemJp : String = ""
    property creator : String = ""
    property notes : String = ""

    def initialize(file : Yuno::VgmFile)
      gd3 = file.gd3Tag
      @title = gd3.trackNameEn # Mapped to title
      @titleJp = gd3.trackNameJp
      @artist = gd3.authorNameEn # Mapped to artist
      @artistJp = gd3.authorNameJp
      @album = gd3.gameNameEn # Mapped to album
      @gameJp = gd3.gameNameJp
      @systemEn = gd3.systemNameEn
      @systemJp = gd3.systemNameJp
      @date = gd3.releaseDate # Mapped to date
      @creator = gd3.creator
      @notes = gd3.notes
      @genre = "VGM"
    end
  end

  class VorbisTagInfo < TagInfo
    include ReplayInfoTagsMixin

    property composer : String = ""
    property performer : String = ""

    def initialize(file : FlacFile)
      flac = file.flac
      flac.blocks.find(&.is_a?(RAFlac::VorbisCommentBlock)).try do |block|
        raise "Block changed class?" unless block.is_a?(RAFlac::VorbisCommentBlock)
        data = block.data
        data["TITLE"]?.try { |str| @title = str.join(", ") }
        data["ARTIST"]?.try { |str| @artist = str.join(", ") }
        data["ALBUM"]?.try { |str| @album = str.join(", ") }
        data["DATE"]?.try { |str| @date = str.join(", ") }
        data["GENRE"]?.try { |str| @genre = str.join(", ") }
        data["COMPOSER"]?.try { |str| @composer = str.join(", ") }
        data["PERFORMER"]?.try { |str| @performer = str.join(", ") }

        data["REPLAYGAIN_TRACK_GAIN"]?.try { |str| @trackGain = parseGain(str[-1]) unless str.empty? }
        data["REPLAYGAIN_TRACK_PEAK"]?.try { |str| @trackPeak = parsePeak(str[-1]) unless str.empty? }
        data["REPLAYGAIN_ALBUM_GAIN"]?.try { |str| @albumGain = parseGain(str[-1]) unless str.empty? }
        data["REPLAYGAIN_ALBUM_PEAK"]?.try { |str| @albumPeak = parsePeak(str[-1]) unless str.empty? }
      end
    end

    def initialize(file : OpusFile|VorbisFile)
      file.comments.try do |data|
        data["TITLE"]?.try { |str| @title = str.join(", ") }
        data["ARTIST"]?.try { |str| @artist = str.join(", ") }
        data["ALBUM"]?.try { |str| @album = str.join(", ") }
        data["DATE"]?.try { |str| @date = str.join(", ") }
        data["GENRE"]?.try { |str| @genre = str.join(", ") }
        data["COMPOSER"]?.try { |str| @composer = str.join(", ") }
        data["PERFORMER"]?.try { |str| @performer = str.join(", ") }

        data["REPLAYGAIN_TRACK_GAIN"]?.try { |str| @trackGain = parseGain(str[-1]) unless str.empty? }
        data["REPLAYGAIN_TRACK_PEAK"]?.try { |str| @trackPeak = parsePeak(str[-1]) unless str.empty? }
        data["REPLAYGAIN_ALBUM_GAIN"]?.try { |str| @albumGain = parseGain(str[-1]) unless str.empty? }
        data["REPLAYGAIN_ALBUM_PEAK"]?.try { |str| @albumPeak = parsePeak(str[-1]) unless str.empty? }

        if file.is_a?(OpusFile)
          # The tags R128_ALBUM_GAIN and R128_TRACK_GAIN should be preferred
          # over REPLAYGAIN_* tags for Opus files.

          data["R128_TRACK_GAIN"]?.try do |str|
            unless str.empty?
              # Stored in Q7.8 format
              if inum = str[-1].to_i16?
                @trackGain = Utils.q78ToF64Db(inum)
                Benben.dlog!("R128 Track Gain: #{@trackGain}")
              end
            end
          end

          data["R128_ALBUM_GAIN"]?.try do |str|
            unless str.empty?
              if inum = str[-1].to_i16?
                @albumGain = Utils.q78ToF64Db(inum)
                Benben.dlog!("R128 Album Gain: #{@albumGain}")
              end
            end
          end
        end
      end
    rescue err : Exception
      {% if flag?(:benben_debug) %}
        RemiLib.log.error("Error parsing the VorbisComment for #{file.filename}: #{err} (#{err.backtrace})")
      {% else %}
        RemiLib.log.error("Error parsing the VorbisComment for #{file.filename}: #{err}")
      {% end %}
    end
  end

  class Id3TagInfo < TagInfo
    property composer : String = ""
    property performer : String = ""

    def initialize(file : Mpeg1File)
      id3 = file.id3
      @title = id3.title
      @artist = id3.artist
      @album = id3.album
      @date = id3.date
      @genre = id3.genre
    end
  end

  class MidiTagInfo < TagInfo
    def initialize(file : MidiFile)
    end
  end

  class PcmTagInfo < TagInfo
    def initialize(file : PcmFile)
    end
  end

  class QoaTagInfo < TagInfo
    def initialize(file : QoaFile)
    end
  end


  class ApeTagInfo < TagInfo
    property composer : String = ""
    property performer : String = ""

    def initialize(file : WavPackFile)
      data = file.ape
      data["TITLE"]?.try { |str| @title = str.join(", ") }
      data["ARTIST"]?.try { |str| @artist = str.join(", ") }
      data["ALBUM"]?.try { |str| @album = str.join(", ") }
      data["YEAR"]?.try { |str| @date = str.join(", ") }
      data["GENRE"]?.try { |str| @genre = str.join(", ") }
      data["COMPOSER"]?.try { |str| @composer = str.join(", ") }
      data["PERFORMER"]?.try { |str| @performer = str.join(", ") }
    end
  end
end