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
264
265
266
267
268
269
270
271
272
273
274
275
276
277
| #### RemiAudio
#### Copyright (C) 2022-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 "./common"
module RemiAudio
# Stores [Ogg Vorbis Comment](https://xiph.org/vorbis/doc/v-comment.html)
# metadata.
class VorbisComment
class Error < RemiAudioError
end
# :nodoc:
EQUAL_SIGN = '='.ord.to_u8
# The vendor string for the Vorbis Comments set.
property vendor : String = ""
@comments : Hash(String, Array(String)) = {} of String => Array(String)
# Creates a new, empty set of Vorbis Comments.
def initialize
end
# Creates a new set of Vorbis Comments by parsing `rawData`. If
# `ignoreMissingFramingBit` is `true`, then the parser will not check for
# the framing bit at the end of the comments.
def initialize(rawData : Bytes, ignoreMissingFramingBit : Bool = false)
parse(rawData, ignoreMissingFramingBit)
end
# Creates a new set of Vorbis Comments by reading from `io`. If
# `ignoreMissingFramingBit` is `true`, then the parser will not check for
# the framing bit at the end of the comments.
def initialize(io : IO, ignoreMissingFramingBit : Bool = false)
parse(io, ignoreMissingFramingBit)
end
# Returns `true` if `name` is a valid string for a Vorbis Comment key, or
# `false` otherwise.
def self.validKeyName(name : String) : Bool
name.ascii_only? && name.to_slice.all? { |x| x >= 0x20 && x <= 0x7D }
end
# Returns a Tuple where the first element is the key name, and the second
# element is the value.
@[AlwaysInline]
protected def parseComment(io : IO) : Tuple(String, String)
len = io.readUInt32
str = io.read_string(len)
data = str.split('=', 2)
# Do a few checks
if data.size != 2
raise VorbisComment::Error.new("Invalid Vorbis Comment at #{io.pos}, no key/value pair detected")
#elsif data[0].size == 0
# raise VorbisComment::Error.new("Vorbis Comment has a blank key name at #{io.pos}")
elsif !VorbisComment.validKeyName(data[0])
raise VorbisComment::Error.new("Invalid Vorbis Comment at #{io.pos}, key is invalid")
end
{data[0].upcase, data[1]}
end
# Appends new comments onto this set of Vorbis Comments by parsing
# `rawData`. If `ignoreMissingFramingBit` is `true`, then the parser will
# not check for the framing bit at the end of the comments. This returns
# the number of comments added to the set.
def parse(rawData : Bytes, ignoreMissingFramingBit : Bool = false) : Int32
io = IO::Memory.new(rawData)
parse(io, ignoreMissingFramingBit)
end
# Appends new comments onto this set of Vorbis Comments by parsing
# `rawData`. If `ignoreMissingFramingBit` is `true`, then the parser will
# not check for the framing bit at the end of the comments. This returns
# the number of comments added to the set.
def parse(io : IO, ignoreMissingFramingBit : Bool = false) : Int32
numRead : Int32 = 0
# Read the vendor string
vendLen = io.readUInt32
self.vendor = io.read_string(vendLen)
# Read the comments
numComments = io.readUInt32
numComments.times do |_|
self << parseComment(io)
numRead += 1
end
# Possibly check for the framing bit
unless ignoreMissingFramingBit
begin
if byte = io.read_byte
unless byte == 1
raise VorbisComment::Error.new("Framing bit not set in Vorbis Comments")
end
else
raise VorbisComment::Error.new("Framing bit missing in Vorbis Comments")
end
rescue IO::EOFError
raise VorbisComment::Error.new("Framing bit not found in Vorbis Comments")
end
end
numRead
end
# Writes this comment set to `io`, then returns `io`. If `noFramingBit` is
# `true`, then this will not write the framing bit at the end.
def write(io : IO, noFramingBit : Bool = false) : IO
# Write vendor string.
io.write_bytes(@vendor.size.to_u32, IO::ByteFormat::LittleEndian)
io.write(@vendor.to_slice) unless @vendor.empty?
# Write comments
io.write_bytes(self.size, IO::ByteFormat::LittleEndian)
self.each do |key, val|
io.write_bytes((key.size + val.size + 1).to_u32, IO::ByteFormat::LittleEndian)
io.write(key.upcase.to_slice)
io.write_byte(EQUAL_SIGN)
io.write(val.to_slice)
end
io.write_byte(1u8) unless noFramingBit
io
end
# Returns the set of comments associated with the given key. If the key
# does not exist, this raises a `KeyError` is raised.
#
# The case of `name` does not matter. It cannot be blank.
#
# Note: modifications to the returned array will change the data stored in
# this `VorbisComment` instance.
def [](name : String) : Array(String)
@comments[name.upcase]
end
# Attempts to return the set of comments associated with the given key. If
# the key does not exist, this returns `nil`.
#
# The case of `name` does not matter.
#
# Note: modifications to the returned array will change the data stored in
# this `VorbisComment` instance.
def []?(name : String) : Array(String)?
@comments[name.upcase]?
end
# Returns the number of unique key names stored in this set.
def numKeys
@comments.size
end
# Returns the total number of comments stored in this set.
@[AlwaysInline]
def size : UInt32
ret : UInt32 = 0u32
@comments.each_value do |v|
ret += v.size
end
ret
end
# Returns `true` if this set has no comments, or `false` otherwise.
def empty?
@comments.empty?
end
# Loops over all of the comments, yielding each unique comment as a
# `Tuple(String, String)` to the block.
def each(&)
@comments.each_key do |k|
@comments[k].each do |v|
yield k, v
end
end
end
# Loops over all the unique keys, yielding the `Array(String)` of comments
# for each key to the block.
#
# Note: modifications to the yielded array will change the data stored in
# this `VorbisComment` instance.
def eachKey(&)
@comments.each_key do |k|
yield @comments[k]
end
end
# Adds a new comment to the set. The 0th value of the Tuple is the key
# name. If the key is not a valid key name, this will raise a
# `VorbisComment::Error`. This returns `self.`
#
# The key name will always be converted to upper case.
def <<(value : Tuple(String, String))
unless VorbisComment.validKeyName(value[0])
raise VorbisComment::Error.new("Invalid Vorbis Comment key")
end
key = value[0].upcase
unless @comments.has_key?(key)
@comments[key] = [] of String
end
@comments[key] << value[1]
self
end
# Adds a new comment to the set. If the key is not a valid key name, this
# will raise a `VorbisComment::Error`. This returns `self.`
#
# `key` will always be converted to upper case.
@[AlwaysInline]
def []=(key : String, value : String)
self << {key, value}
self
end
# Removes all comments stored under the key name `key`. If the key does not
# exist, this does nothing. This returns `self.`
#
# The case of `key` does not matter.
@[AlwaysInline]
def delete(key : String)
@comments.delete(key.upcase)
self
end
# Removes a specific comment stored under the key name `key`. If the key
# does not exist, this raises a `KeyError`. This returns the deleted
# comment value.
#
# The case of `key` does not matter.
@[AlwaysInline]
def delete(key : String, valNum : Int) : String
@comments[key.upcase].delete_at(valNum)
end
# Removes all comments from this set, then returns `self`.
def clear
@comments.clear
self
end
# Returns an array of `Tuple(String, String)` containing all of the comments
# in this set. The returned array can be safely modified without affecting
# the underlying data in this `VorbisComment` instance.
def comments : Array(Tuple(String, String))
ret = [] of Tuple(String, String)
@comments.each_key do |k|
@comments.each_value do |v|
ret << {k, v}
end
end
ret
end
end
end
|