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
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: Not yet implemented, waiting for RemiAudio to be updated.
* 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: Not yet implemented, waiting for RemiAudio to be updated.
* Seeking: working for WAV/Au, modules, and MPEG-1 only
* Resampling: working

======
Config
======

* Overall: 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
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: Not yet implemented, waiting for RemiAudio to be updated.
* CUE writing: working
* Normalization: working
* Auto-directories: working, only for VGM

=====
Input
=====







|







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
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.1

  remiconf:
    fossil: https://chiselapp.com/user/MistressRemilia/repository/remiconf
    version: 0.1.5

  remihjson:
    fossil: https://chiselapp.com/user/MistressRemilia/repository/remihjson












|







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
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.1

  remiconf:
    fossil: https://chiselapp.com/user/MistressRemilia/repository/remiconf
    version: 0.1.5

  remixspf:
    fossil: https://chiselapp.com/user/MistressRemilia/repository/remixspf







|







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







|







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



275


    end
  end

  class PcmTagInfo < TagInfo
    def initialize(file : PcmFile)
    end
  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