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