Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
Changes In Branch add-qoa Excluding Merge-Ins
This is equivalent to a diff from 7709609af5 to f4d68d2755
|
2024-08-13
| ||
| 08:01 | Merge and integrate add-qoa branch check-in: d335d786ea user: alexa tags: trunk | |
| 08:00 | Update TRUNKSTATUS Closed-Leaf check-in: f4d68d2755 user: alexa tags: add-qoa | |
| 07:58 | Fix Qoa seeking and position display. check-in: 5e544d6174 user: alexa tags: add-qoa | |
|
2024-08-12
| ||
| 22:53 | Initial QOA support added. check-in: d53a8acded user: alexa tags: add-qoa | |
|
2024-08-09
| ||
| 13:16 | Bump RemiAudio revision check-in: 7709609af5 user: alexa tags: trunk | |
| 06:42 | Bump required Haematite and RemiAudio versions check-in: c481a73edd user: alexa tags: trunk | |
Changes to NEWS.
1 2 3 4 5 6 7 8 9 10 |
;;;; -*- coding: utf-8; fill-column: 78 -*-
changes relative to Benben 0.5.0:
* Enhancement: You can now specify either a single theme, or an array of
themes, in the config file. When it's an array, a random one out of that
array will be chosen.
* Enhancement: If the terminal is larger than 80x24, the play queue will
expand to fill the empty space (it previously expanded horizontally, but
not vertically).
* Enhancement: Normalization now happens in parallel as jobs are being
| > | 1 2 3 4 5 6 7 8 9 10 11 |
;;;; -*- coding: utf-8; fill-column: 78 -*-
changes relative to Benben 0.5.0:
* New: Quite OK Audio (QOA) format support added.
* Enhancement: You can now specify either a single theme, or an array of
themes, in the config file. When it's an array, a random one out of that
array will be chosen.
* Enhancement: If the terminal is larger than 80x24, the play queue will
expand to fill the empty space (it previously expanded horizontally, but
not vertically).
* Enhancement: Normalization now happens in parallel as jobs are being
|
| ︙ | ︙ |
Changes to TRUNKSTATUS.
| ︙ | ︙ | |||
20 21 22 23 24 25 26 | * SoundFonts: Mostly working (the "global" soundfont is technically set incorrecly - see TODO in code) * MP1/2/3: working * FLAC: working * Opus: Working * Vorbis: working * WAV/Au: working | | | | | 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | * SoundFonts: Mostly working (the "global" soundfont is technically set incorrecly - see TODO in code) * MP1/2/3: working * FLAC: working * Opus: Working * Vorbis: working * WAV/Au: working * QOA: working * XSPF/JSPF: working, seems to use more RAM than I would like * Local files: working * HTTP: removed * Gemini: removed ============= Audio/Formats ============= * VGMs: working * Modules (libxmp): working * MIDI (Haematite): working * MP1/2/3 (libmpg123): working * FLAC (RemiAudio): working * Opus (libopus): working * Vorbis (libvorbis): working * WAV/Au (RemiAudio): working * QOA: working * Seeking: working for WAV/Au, modules, QOA, and MPEG-1 only * Resampling: working ====== Config ====== * Overall: working |
| ︙ | ︙ | |||
62 63 64 65 66 67 68 | * Modules (libxmp): working * MIDI (Haematite): working * MP1/2/3 (libmpg123): working * FLAC (RemiAudio): working * Opus (libopus): working * Vorbis (libvorbis): working * WAV/Au (RemiAudio): purposely not implemented | | | 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | * Modules (libxmp): working * MIDI (Haematite): working * MP1/2/3 (libmpg123): working * FLAC (RemiAudio): working * Opus (libopus): working * Vorbis (libvorbis): working * WAV/Au (RemiAudio): purposely not implemented * QOA: working, not tested * CUE writing: working * Normalization: working * Auto-directories: working, only for VGM ===== Input ===== |
| ︙ | ︙ |
Changes to shard.lock.
1 2 3 4 5 6 7 8 9 10 11 12 |
version: 2.0
shards:
haematite:
fossil: https://chiselapp.com/user/MistressRemilia/repository/Haematite
version: 0.5.3
libremiliacr:
fossil: https://chiselapp.com/user/MistressRemilia/repository/libremiliacr
version: 0.90.8
remiaudio:
fossil: https://chiselapp.com/user/MistressRemilia/repository/remiaudio
| | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
version: 2.0
shards:
haematite:
fossil: https://chiselapp.com/user/MistressRemilia/repository/Haematite
version: 0.5.3
libremiliacr:
fossil: https://chiselapp.com/user/MistressRemilia/repository/libremiliacr
version: 0.90.8
remiaudio:
fossil: https://chiselapp.com/user/MistressRemilia/repository/remiaudio
version: 0.6.3
remiconf:
fossil: https://chiselapp.com/user/MistressRemilia/repository/remiconf
version: 0.1.5
remihjson:
fossil: https://chiselapp.com/user/MistressRemilia/repository/remihjson
|
| ︙ | ︙ |
Changes to shard.yml.
| ︙ | ︙ | |||
17 18 19 20 21 22 23 |
haematite:
fossil: https://chiselapp.com/user/MistressRemilia/repository/Haematite
version: 0.5.3
remiaudio: # We use a slightly newer version of RemiAudio to get QOA support.
fossil: https://chiselapp.com/user/MistressRemilia/repository/remiaudio
| | | 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
haematite:
fossil: https://chiselapp.com/user/MistressRemilia/repository/Haematite
version: 0.5.3
remiaudio: # We use a slightly newer version of RemiAudio to get QOA support.
fossil: https://chiselapp.com/user/MistressRemilia/repository/remiaudio
version: 0.6.3
remiconf:
fossil: https://chiselapp.com/user/MistressRemilia/repository/remiconf
version: 0.1.5
remixspf:
fossil: https://chiselapp.com/user/MistressRemilia/repository/remixspf
|
| ︙ | ︙ |
Changes to src/audio-formats/playablefile.cr.
| ︙ | ︙ | |||
59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
FileType::Flac if FlacFile.test(filename)
when ".mp3", ".mp2", ".mp1"
FileType::Mpeg1 if Mpeg1File.test(filename)
when ".wav", ".wave", ".au"
FileType::Pcm if PcmFile.test(filename)
when ".midi", ".mid", ".mus", ".rmi"
FileType::Midi if MidiFile.test(filename)
else nil # Using the extension didn't yield a result
end
rescue Exception
nil
end
# Determines if **filename** is a file type that is supported by Benben, and
| > > | 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
FileType::Flac if FlacFile.test(filename)
when ".mp3", ".mp2", ".mp1"
FileType::Mpeg1 if Mpeg1File.test(filename)
when ".wav", ".wave", ".au"
FileType::Pcm if PcmFile.test(filename)
when ".midi", ".mid", ".mus", ".rmi"
FileType::Midi if MidiFile.test(filename)
when ".qoa"
FileType::Qoa if QoaFile.test(filename)
else nil # Using the extension didn't yield a result
end
rescue Exception
nil
end
# Determines if **filename** is a file type that is supported by Benben, and
|
| ︙ | ︙ | |||
84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
# If we've made it here, then the filename extension didn't help us. Do
# more intensive checks.
case
when VgmFile.test(filename)
ret = FileType::Vgm
when MidiFile.test(filename)
ret = FileType::Midi
when RemiXmp.test(filename)
ret = FileType::Module
when VorbisFile.test(filename)
ret = FileType::Vorbis
when OpusFile.test(filename)
ret = FileType::Opus
when FlacFile.test(filename)
| > > | 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
# If we've made it here, then the filename extension didn't help us. Do
# more intensive checks.
case
when VgmFile.test(filename)
ret = FileType::Vgm
when MidiFile.test(filename)
ret = FileType::Midi
when QoaFile.test(filename)
ret = FileType::Qoa
when RemiXmp.test(filename)
ret = FileType::Module
when VorbisFile.test(filename)
ret = FileType::Vorbis
when OpusFile.test(filename)
ret = FileType::Opus
when FlacFile.test(filename)
|
| ︙ | ︙ | |||
120 121 122 123 124 125 126 127 128 129 130 131 132 133 |
in .module? then return ModuleFile.new(filename)
in .flac? then return FlacFile.new(filename)
in .opus? then return OpusFile.new(filename)
in .vorbis? then return VorbisFile.new(filename)
in .mpeg1? then return Mpeg1File.new(filename)
in .midi? then return MidiFile.new(filename)
in .pcm? then return PcmFile.new(filename)
in .unknown?
RemiLib.log.error("Cannot play file: #{filename}")
nil
end
rescue err : Yuno::YunoError
Benben.dlog!("YunoSynth Error loading file: #{err} (#{err.backtrace}")
RemiLib.log.error("Cannot load file: #{err}")
| > | 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 |
in .module? then return ModuleFile.new(filename)
in .flac? then return FlacFile.new(filename)
in .opus? then return OpusFile.new(filename)
in .vorbis? then return VorbisFile.new(filename)
in .mpeg1? then return Mpeg1File.new(filename)
in .midi? then return MidiFile.new(filename)
in .pcm? then return PcmFile.new(filename)
in .qoa? then return QoaFile.new(filename)
in .unknown?
RemiLib.log.error("Cannot play file: #{filename}")
nil
end
rescue err : Yuno::YunoError
Benben.dlog!("YunoSynth Error loading file: #{err} (#{err.backtrace}")
RemiLib.log.error("Cannot load file: #{err}")
|
| ︙ | ︙ |
Added src/audio-formats/qoafile.cr.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 |
#### 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 "./playablefile"
####
#### Wrapper for QOA files
####
module Benben
class QoaFile < PlayableFile
@ctx : Qoa::Decoder?
@samplesDecoded : Int64 = 0i64
protected def initialize(@filename : String)
ensureFile || raise PlayableFileError.new("Cannot load file: #{@filename}")
@taginfo = TagInfo.create(self)
_ = self.timeLength
end
def finalize
@ctx = nil
end
def self.test(filename : Path|String) : Bool
if qoa = Qoa::Decoder.test(filename)
qoa.channels == 2
else
false
end
rescue Exception
false
end
def ensureFile : Bool
@ctx = Qoa::Decoder.new(filename) if @ctx.nil?
true
rescue err : Exception
RemiLib.log.error("Cannot load QOA #{@filename}: #{err}")
false
end
def unload : Nil
Benben.dlog!("Unloading QOA file: #{@filename}")
@ctx = nil
end
def sampleRate
ensureFile
@ctx.not_nil!.qoa.sampleRate
end
def channels
ensureFile
@ctx.not_nil!.qoa.channels
end
def totalSamples : UInt64
ensureFile
@ctx.not_nil!.totalSamples
end
def totalFrames : UInt64
ensureFile
@ctx.not_nil!.totalFrames.to_u64!
end
@[AlwaysInline]
def framePos : Int64
ensureFile
@ctx.not_nil!.framesDecoded.to_i64
end
@[AlwaysInline]
def pos : Int64
@samplesDecoded
end
@[AlwaysInline]
def pos=(value) : Int64
raise NotImplementedError.new("Use #framePos= for QoaFile")
end
# Seeks to the given frame.
@[AlwaysInline]
def framePos=(value) : Nil
ensureFile
@ctx.not_nil!.seek(value) # Note: value is a FRAME INDEX here
@samplesDecoded = @ctx.not_nil!.framesDecoded.to_i64 * Qoa::FRAME_LEN
@samplesDecoded *= @ctx.not_nil!.qoa.channels
end
@[AlwaysInline]
def decode(dest : Array(Float32)) : Int64
ensureFile
numRead = @ctx.not_nil!.decode(dest)
if numRead < dest.size
dest.fill(0.0f32, numRead..)
end
@samplesDecoded += numRead.tdiv(@ctx.not_nil!.qoa.channels)
numRead.to_i64!
end
end
end
|
Changes to src/common.cr.
| ︙ | ︙ | |||
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | ### ### Exceptions and Aliases ### class BenbenError < Exception end alias RAFlac = RemiAudio::Codecs::FLAC alias MVerb = RemiAudio::DSP::MVerb alias Zita = RemiAudio::DSP::ZitaReverb alias ReverbPreset = MVerb::Preset|Zita::Preset alias ThemeColor = UInt8|Tuple(UInt8, UInt8, UInt8)|String end require "./audio-formats/modulefile" require "./audio-formats/flacfile" require "./audio-formats/opusfile" require "./audio-formats/vorbisfile" require "./audio-formats/mpeg1file" require "./audio-formats/midifile" require "./audio-formats/vgmfile" require "./audio-formats/pcmfile" #### #### Common Stuff and Globals #### module Benben ### | > > | 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 | ### ### Exceptions and Aliases ### class BenbenError < Exception end alias Qoa = RemiAudio::Codecs::Qoa alias RAFlac = RemiAudio::Codecs::FLAC alias MVerb = RemiAudio::DSP::MVerb alias Zita = RemiAudio::DSP::ZitaReverb alias ReverbPreset = MVerb::Preset|Zita::Preset alias ThemeColor = UInt8|Tuple(UInt8, UInt8, UInt8)|String end require "./audio-formats/modulefile" require "./audio-formats/flacfile" require "./audio-formats/opusfile" require "./audio-formats/vorbisfile" require "./audio-formats/mpeg1file" require "./audio-formats/midifile" require "./audio-formats/vgmfile" require "./audio-formats/pcmfile" require "./audio-formats/qoafile" #### #### Common Stuff and Globals #### module Benben ### |
| ︙ | ︙ | |||
182 183 184 185 186 187 188 189 190 191 192 193 194 195 |
Module
Flac
Opus
Vorbis
Mpeg1
Midi
Pcm
Unknown
end
enum ReplayGain
Disabled
Mix
Album
| > | 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 |
Module
Flac
Opus
Vorbis
Mpeg1
Midi
Pcm
Qoa
Unknown
end
enum ReplayGain
Disabled
Mix
Album
|
| ︙ | ︙ | |||
300 301 302 303 304 305 306 307 308 309 310 311 312 313 | DEF_SONG_CONFG_MODULES = nil DEF_SONG_CONFG_OPUS = nil DEF_SONG_CONFG_VORBIS = nil DEF_SONG_CONFG_MPEG1 = nil DEF_SONG_CONFG_FLAC = nil DEF_SONG_CONFG_MIDI = nil DEF_SONG_CONFG_PCM = nil # The name of the default theme. DEF_THEME_NAME = "default" DEF_MIDI_REVERB_ENABLE = true DEF_MIDI_REVERB_TYPE = Haematite::ReverbMode::MVerb DEF_MIDI_DISABLE_REMAPPING = false | > | 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 | DEF_SONG_CONFG_MODULES = nil DEF_SONG_CONFG_OPUS = nil DEF_SONG_CONFG_VORBIS = nil DEF_SONG_CONFG_MPEG1 = nil DEF_SONG_CONFG_FLAC = nil DEF_SONG_CONFG_MIDI = nil DEF_SONG_CONFG_PCM = nil DEF_SONG_CONFG_QOA = nil # The name of the default theme. DEF_THEME_NAME = "default" DEF_MIDI_REVERB_ENABLE = true DEF_MIDI_REVERB_TYPE = Haematite::ReverbMode::MVerb DEF_MIDI_DISABLE_REMAPPING = false |
| ︙ | ︙ |
Changes to src/playermanager.cr.
| ︙ | ︙ | |||
101 102 103 104 105 106 107 108 109 110 111 112 113 114 |
in PcmFile
Benben.dlog!("PlayerManager: Playing a PCM file")
@player = PcmPlayer.new unless @player.is_a?(PcmPlayer)
raise "Expected a PCM file, but got a #{file.class}" unless file.is_a?(PcmFile)
@player.play(file)
in PlayableFile
raise "Cannot play an unknown file type"
end
end
def getNextFile
Benben.fileHandler.unloadCurrent
| > > > > > > | 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 |
in PcmFile
Benben.dlog!("PlayerManager: Playing a PCM file")
@player = PcmPlayer.new unless @player.is_a?(PcmPlayer)
raise "Expected a PCM file, but got a #{file.class}" unless file.is_a?(PcmFile)
@player.play(file)
in QoaFile
Benben.dlog!("PlayerManager: Playing a QOA file")
@player = QoaPlayer.new unless @player.is_a?(QoaPlayer)
raise "Expected a QOA file, but got a #{file.class}" unless file.is_a?(QoaFile)
@player.play(file)
in PlayableFile
raise "Cannot play an unknown file type"
end
end
def getNextFile
Benben.fileHandler.unloadCurrent
|
| ︙ | ︙ |
Added src/players/qoaplayer.cr.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 |
#### 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 "remiaudio"
####
#### QOA File Playback
####
module Benben
class QoaPlayer < Player
protected getter! ctx : QoaFile?
@samplesRendered : Int64 = 0i64
@tempBuf : Array(Float32) = [] of Float32
@seekFrames : Int64 = 2i64
def initialize
Benben.dlog!("QoaPlayer starting up")
# Setup buffers and variables
@bufSize = Benben.config.bufferSize.to_u32
@bufRealSize = @bufSize * 2 # multiplied by 2 for stereo
@buf = Array(Float32).new(@bufRealSize, 0.0f32)
end
protected def resetInternalVars : Nil
super
@maxLoops.set(Benben.config.maxLoops.to_i32 - 1)
end
def play(file : PlayableFile) : Nil
Benben.dlog!("QoaPlayer is going to play a file")
raise "QoaPlayer received something that wasn't a QoaFile" unless file.is_a?(QoaFile)
@ctx = file
self.infoLine = formatStr("QOA File")
self.state = PlayerState::Frame
# The number of frames to seek depends on the input file's sample rate.
@seekFrames = (file.sampleRate.tdiv(Qoa::FRAME_LEN).to_i64 * Benben.config.seekTime).tdiv(2)
# Reset some variables
resetInternalVars
# Set the total number of samples.
@totalSamples.set(self.ctx.totalSamples)
# Send settings to other things.
Benben.ui.apply(Benben.config)
Benben.config.send(@effects)
# Setup resampling
if file.sampleRate != Benben.config.sampleRate
Benben.dlog!("QoaPlayer: Resampling needed, using #{Benben.config.resampler} resampling")
@tempBuf = Array(Float32).new(@bufRealSize, 0.0f32)
initResampler(file.sampleRate, Benben.config.resampler)
else
Benben.dlog!("QoaPlayer: Resampling is not needed")
@resampler = nil
end
Benben.dlog!("QoaPlayer is ready to render frames")
rescue err : RemiAudio::RemiAudioError
{% if flag?(:benben_debug) %}
raise BenbenError.new("Qoa error: #{err}\nStack trace: #{err.backtrace.join("\n => ")}")
{% else %}
raise BenbenError.new("Qoa error: #{err}")
{% end %}
end
protected def renderCb : Tuple(Slice(Float32), Int64)
rendered = self.ctx.decode(@tempBuf)
# Divide by 2 because because 2 channels
{renderBufSlice(@tempBuf), rendered.tdiv(2).to_i64!}
rescue err : Exception
# On error, just return zero
{renderBufSlice(@tempBuf), 0i64}
end
@[AlwaysInline]
private def renderBuffers
if resamp = @resampler
# We're resampling, so use that to get data
@samplesRendered = resamp.read(@resampRatio, @buf)
else
# Not resampling. Render the audio using our context directly
@samplesRendered = self.ctx.decode(@buf)
end
# Apply volume
@bufRealSize.times do |idx|
@buf.unsafe_put(idx, @buf.unsafe_fetch(idx) * @volume)
end
# Process with effects
@effects.process(@buf)
end
def playFrame : Bool
case @state
in .frame?
# Render the buffer, update position info, then send the data to the
# audio device.
renderBuffers
@samplesPlayed.set(self.ctx.pos.to_u64!)
Benben.driver.writeBuffer(@buf)
# Did we finish?
if @samplesPlayed.get >= @totalSamples.get
# Do we loop?
if @maxLoops.get >= 0 && (@currentLoop.get >= @maxLoops.get)
self.state = PlayerState::Tails
else
self.ctx.pos = 0
@currentLoop.add(1)
@samplesPlayed.set(0)
end
end
true
in .fadeout?
raise "Qoa files don't have fadeout capabilities"
in .tails?
# Track is not looping, play just a bit longer to add some silence
# or let some instrument tails play.
@buf.fill(0.0f32)
@effects.channelVolL.set(SILENCE)
@effects.channelVolR.set(SILENCE)
if @nonLoopingExtraLoops > 0
@nonLoopingExtraLoops -= 1
Benben.driver.writeBuffer(@buf)
true
else
self.state = PlayerState::Done
false
end
in .paused?
@buf.fill(0.0f32)
@samplesPlayed.set(self.ctx.pos.to_u64!) # Could be changed by seeking
Benben.driver.writeBuffer(@buf) # Do not cause a buffer underrun with PortAudio
true
in .done?
self.state = PlayerState::Done
false
end
end
def state=(newState : PlayerState) : Nil
Benben.dlog!("QoaPlayer: Changing state to #{newState} (current state: #{@state})")
@lastState = @state
@state = newState
end
def togglePause : Nil
if @state.paused?
self.state = @lastState
else
self.state = PlayerState::Paused
end
end
def toggleEq : Nil
if @effects.toggleEq
Benben.config.equalizerEnabled = true
Benben.config.equalizerEnabledFromSongConf = false
else
Benben.config.equalizerEnabled = false
Benben.config.equalizerEnabledFromSongConf = false
end
end
def toggleSoftClipping : Nil
Benben.config.enableSoftClipping = @effects.toggleSoftClipping
end
def toggleStereoEnhancer : Nil
Benben.config.enableStereoEnhancer = @effects.toggleStereoEnhancer
end
def toggleReverb : Nil
Benben.config.reverbEnabled = @effects.toggleReverb
end
def volUp : Float64
@volume = (@volume + VOL_STEP).clamp(0.0, 3.0).to_f32!
Benben.dlog!("QoaPlayer volume changed to #{@volume}")
@volume.to_f64!
end
def volDown : Float64
@volume = (@volume - VOL_STEP).clamp(0.0, 3.0).to_f32!
Benben.dlog!("QoaPlayer volume changed to #{@volume}")
@volume.to_f64!
end
def loopUp : Nil
unless @maxLoops == Int32::MAX
wasIndefinite = @maxLoops.get < 0
@maxLoops.add(1)
if wasIndefinite
@currentLoop.set(Math.max(0u32, @maxLoops.get - 1).to_u32)
end
end
end
def loopDown : Nil
unless @maxLoops.get < 0
@maxLoops.sub(1)
# If the new maxLoopTimes would cause the song to end, just put our
# current loop as the final loop.
if @maxLoops.get >= 0 && @currentLoop.get > @maxLoops.get
@currentLoop.set(Math.max(0, @maxLoops.get - 1).to_u32)
end
end
end
def seekForward : Nil
unless self.ctx.framePos + @seekFrames >= self.ctx.totalFrames
self.ctx.framePos += @seekFrames
end
end
def seekBackward : Nil
if self.ctx.framePos - @seekFrames < 0
self.ctx.framePos = 0
else
self.ctx.framePos -= @seekFrames
end
end
def stop : Nil
Benben.dlog!("Stopping QoaPlayer")
@skipFadeout = true
self.state = PlayerState::Done
@ctx.try(&.unload)
end
def curFile : PlayableFile?
@ctx
end
def maxLoopTimes : Int32
@maxLoops.get
end
end
end
|
Changes to src/rendering/job.cr.
| ︙ | ︙ | |||
216 217 218 219 220 221 222 223 224 225 226 227 228 229 |
in OpusFile then outputPath
in VorbisFile then outputPath
in Mpeg1File then outputPath
in FlacFile then outputPath
in ModuleFile then outputPath
in PcmFile then outputPath
in MidiFile then outputPath
in PlayableFile then raise "Case statement was not updated to take care of files of class #{typeof(pfile)} / #{pfile.class}"
end
end
###
### Sample Conversions/Dithering
###
| > | 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 |
in OpusFile then outputPath
in VorbisFile then outputPath
in Mpeg1File then outputPath
in FlacFile then outputPath
in ModuleFile then outputPath
in PcmFile then outputPath
in MidiFile then outputPath
in QoaFile then outputPath
in PlayableFile then raise "Case statement was not updated to take care of files of class #{typeof(pfile)} / #{pfile.class}"
end
end
###
### Sample Conversions/Dithering
###
|
| ︙ | ︙ | |||
314 315 316 317 318 319 320 | require "./vgmjob" require "./modulejob" require "./flacjob" require "./vorbisjob" require "./opusjob" require "./mpeg1job" require "./midijob" | > | 315 316 317 318 319 320 321 322 | require "./vgmjob" require "./modulejob" require "./flacjob" require "./vorbisjob" require "./opusjob" require "./mpeg1job" require "./midijob" require "./qoajob" |
Changes to src/rendering/midijob.cr.
| ︙ | ︙ | |||
185 186 187 188 189 190 191 |
cue.addFile do |file|
# Construct the filename
file.filename = self.outFilename.relative_to(cuePath).to_s
# Set the type, add a single track
file.type = RemiAudio::Cue::File::Type::Wave
file.addTrack do |trk|
| | | 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 |
cue.addFile do |file|
# Construct the filename
file.filename = self.outFilename.relative_to(cuePath).to_s
# Set the type, add a single track
file.type = RemiAudio::Cue::File::Type::Wave
file.addTrack do |trk|
trk.title = self.outFilename.basename
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
|
| ︙ | ︙ |
Added src/rendering/qoajob.cr.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 |
#### 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/>.
module Benben::Rendering
# A render job that converts a QOA file into a WAV/Au.
class QoaJob < Job
@ctx : QoaFile
@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(qoa : QoaFile, @inFilename : Path, @outFilename : Path, @doneChan : JobChannel)
@source = qoa
@ctx = qoa
@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
#
# 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
ret = @totalSamples.to_u32
ret *= 2 if Benben.args["normalize"].called?
ret
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
# Set the type, add a single track
file.type = RemiAudio::Cue::File::Type::Wave
file.addTrack do |trk|
trk.title = self.outFilename.basename
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
|
Changes to src/rendering/renderer.cr.
| ︙ | ︙ | |||
200 201 202 203 204 205 206 207 208 209 210 211 212 213 |
jobs << OpusJob.new(file.as(OpusFile), Path[Benben.fileHandler.currentFilename], filename, jobChan)
in VorbisFile
jobs << VorbisJob.new(file.as(VorbisFile), Path[Benben.fileHandler.currentFilename], filename, jobChan)
in Mpeg1File
jobs << Mpeg1Job.new(file.as(Mpeg1File), Path[Benben.fileHandler.currentFilename], filename, jobChan)
in MidiFile
jobs << MidiJob.new(file.as(MidiFile), Path[Benben.fileHandler.currentFilename], filename, jobChan)
in PcmFile
raise "Should have been checked earlier"
in PlayableFile
raise "Unexpectedly received nil PlayableFile"
end
Fiber.yield
end
| > > | 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 |
jobs << OpusJob.new(file.as(OpusFile), Path[Benben.fileHandler.currentFilename], filename, jobChan)
in VorbisFile
jobs << VorbisJob.new(file.as(VorbisFile), Path[Benben.fileHandler.currentFilename], filename, jobChan)
in Mpeg1File
jobs << Mpeg1Job.new(file.as(Mpeg1File), Path[Benben.fileHandler.currentFilename], filename, jobChan)
in MidiFile
jobs << MidiJob.new(file.as(MidiFile), Path[Benben.fileHandler.currentFilename], filename, jobChan)
in QoaFile
jobs << QoaJob.new(file.as(QoaFile), Path[Benben.fileHandler.currentFilename], filename, jobChan)
in PcmFile
raise "Should have been checked earlier"
in PlayableFile
raise "Unexpectedly received nil PlayableFile"
end
Fiber.yield
end
|
| ︙ | ︙ |
Changes to src/taginfo.cr.
| ︙ | ︙ | |||
33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
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 PlayableFile then raise "Forgot to update case statement"
end
end
end
module ReplayInfoTagsMixin
@trackGain : Float64?
| > | 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
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 PlayableFile then raise "Forgot to update case statement"
end
end
end
module ReplayInfoTagsMixin
@trackGain : Float64?
|
| ︙ | ︙ | |||
268 269 270 271 272 273 274 |
end
end
class PcmTagInfo < TagInfo
def initialize(file : PcmFile)
end
end
| > > > | > > | 269 270 271 272 273 274 275 276 277 278 279 280 281 |
end
end
class PcmTagInfo < TagInfo
def initialize(file : PcmFile)
end
end
class QoaTagInfo < TagInfo
def initialize(file : QoaFile)
end
end
end
|
Changes to src/uis/orig/orig.cr.
| ︙ | ︙ | |||
441 442 443 444 445 446 447 448 449 450 451 452 453 454 |
@tagFields[TAG_FIELD_RELEASE_DATE].set("Channels: ", pcm.channels.to_s)
@tagFields[TAG_FIELD_CREATOR ].set("Resampling: ", (Benben.config.sampleRate == pcm.sampleRate ? "No" : "Yes"))
else
RemiLib.log.warn("UI has PcmTagInfo, but the player does not have a PcmFile")
@tagFields[TAG_FIELD_TRACK ].set("File: ", "")
@tagFields[TAG_FIELD_GAME ].set("Sample Rate: ", "")
@tagFields[TAG_FIELD_AUTHOR ].set("Samples: ", "")
@tagFields[TAG_FIELD_SYSTEM ].set("Typ: ", "")
@tagFields[TAG_FIELD_RELEASE_DATE].set("Channels: ", "")
@tagFields[TAG_FIELD_RELEASE_DATE].set("Resampling: ", "")
end
@tagFields[TAG_FIELD_RVA ].set("RVA: ", "N/A")
in TagInfo, Nil
| > > > > > > > > > > > > > > > > > > > > > | 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 |
@tagFields[TAG_FIELD_RELEASE_DATE].set("Channels: ", pcm.channels.to_s)
@tagFields[TAG_FIELD_CREATOR ].set("Resampling: ", (Benben.config.sampleRate == pcm.sampleRate ? "No" : "Yes"))
else
RemiLib.log.warn("UI has PcmTagInfo, but the player does not have a PcmFile")
@tagFields[TAG_FIELD_TRACK ].set("File: ", "")
@tagFields[TAG_FIELD_GAME ].set("Sample Rate: ", "")
@tagFields[TAG_FIELD_AUTHOR ].set("Samples: ", "")
@tagFields[TAG_FIELD_SYSTEM ].set("Typ: ", "")
@tagFields[TAG_FIELD_RELEASE_DATE].set("Channels: ", "")
@tagFields[TAG_FIELD_RELEASE_DATE].set("Resampling: ", "")
end
@tagFields[TAG_FIELD_RVA ].set("RVA: ", "N/A")
in QoaTagInfo
# QOA files don't have tag information, so we repurpose the fields to
# show other information.
if qoa = Benben.player.curFile.as?(QoaFile)
@tagFields[TAG_FIELD_TRACK ].set("File: ", Path[qoa.filename].basename)
@tagFields[TAG_FIELD_GAME ].set("Sample Rate: ", qoa.sampleRate.to_s)
@tagFields[TAG_FIELD_AUTHOR ].set("Samples: ", formatStr("~:d", qoa.totalSamples))
@tagFields[TAG_FIELD_SYSTEM ].set("Type: ", "QOA")
@tagFields[TAG_FIELD_RELEASE_DATE].set("Channels: ", qoa.channels.to_s)
@tagFields[TAG_FIELD_CREATOR ].set("Resampling: ", (Benben.config.sampleRate == qoa.sampleRate ? "No" : "Yes"))
else
RemiLib.log.warn("UI has QoaTagInfo, but the player does not have a QoaFile")
@tagFields[TAG_FIELD_TRACK ].set("File: ", "")
@tagFields[TAG_FIELD_GAME ].set("Sample Rate: ", "")
@tagFields[TAG_FIELD_AUTHOR ].set("Samples: ", "")
@tagFields[TAG_FIELD_SYSTEM ].set("Typ: ", "")
@tagFields[TAG_FIELD_RELEASE_DATE].set("Channels: ", "")
@tagFields[TAG_FIELD_RELEASE_DATE].set("Resampling: ", "")
end
@tagFields[TAG_FIELD_RVA ].set("RVA: ", "N/A")
in TagInfo, Nil
|
| ︙ | ︙ |