4 # Copyright © 2018, 2020 Thorsten Glaser <tg@mirbsd.de>
6 # Provided that these terms and disclaimer and all copyright notices
7 # are retained or reproduced in an accompanying document, permission
8 # is granted to deal in this work without restriction, including un‐
9 # limited rights to use, publicly perform, distribute, sell, modify,
10 # merge, give away, or sublicence.
12 # This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to
13 # the utmost extent permitted by applicable law, neither express nor
14 # implied; without malicious intent or gross negligence. In no event
15 # may a licensor, author or contributor be held liable for indirect,
16 # direct, other damage, loss, or other issues arising in any way out
17 # of dealing in the work, even if advised of the possibility of such
18 # damage or existence of a defect, except proven that it results out
19 # of said person’s immediate fault when using the work as intended.
21 # python3 riffedit.py -d src.sf2 # dump info only
22 # python3 riffedit.py -i src.sf2 # identify metadata (see below)
23 # python3 riffedit.py src.sf2 dst.sf2 { [-az] 'chnk' 'content' } ...
24 # where -a means to align with NULs and -z to NUL-terminate
25 # chnk means the RIFF chunk, LIST<chnk>/chnk is also supported
26 # Chunks currently need to exist in the input, insertion and deletion
27 # is missing for some later version to add.
28 # The comment field is limited to 65535 ASCII bytes, the others to 255.
30 # Metadata from a soundfont only includes chunks useful in copyright
31 # tracking. It outputs the INFO chunks, using input ordering, in the
32 # format “chunk_name \xFE chunk_body \xFF”, where both name and body
33 # (properly UTF-8 encoded) have all characters not valid for XML re‐
34 # moved or replaced with the OPTU-16 value or U+FFFD.
36 # You may also use this under the same terms as the Fluid (R3) soundfont.
38 from io import SEEK_SET, SEEK_CUR
43 assert(sys.version_info[0] >= 3)
45 class RIFFChunk(object):
46 def __init__(self, parent):
49 while isinstance(self.file, RIFFChunk):
50 self.file = self.file.parent
52 cn = self.file.read(4)
53 cs = self.file.read(4)
56 if (len(cn) != 4) or (len(cs) != 4):
60 cs = struct.unpack_from('<L', cs)[0]
63 if cn in (b'RIFF', b'LIST'):
64 ct = self.file.read(4)
67 cf = cn + b'<' + ct + b'>'
71 self.chunk_pad = cs & 1
77 self.justpast = self.data_ofs + self.chunksize + self.chunk_pad
79 if isinstance(self.parent, RIFFChunk) and \
80 self.justpast > self.parent.justpast:
81 raise IndexError('End of this %s chunk %d > end of parent %s chunk %d' % \
82 (self.chunkfmt, self.justpast, self.parent.chunkfmt, self.parent.justpast))
84 if self.container is not None:
87 child = RIFFChunk(self)
90 self.children.append(child)
95 s = '<RIFFChunk(%s)' % self.chunkfmt
96 if self.container is not None:
98 for child in self.children:
105 self.file.seek(self.justpast, SEEK_SET)
106 return isinstance(self.parent, RIFFChunk) and \
107 self.justpast == self.parent.justpast
109 def __getitem__(self, key):
110 if self.container is None:
111 raise IndexError('Chunk %s is not of a container type' % self.chunkname)
112 for child in self.children:
113 if child.chunkfmt == key:
115 raise IndexError('Chunk %s does not have a child %s' % (self.chunkname, key))
118 if self.container is not None:
119 raise IndexError('Chunk %s is of a container type' % self.chunkname)
120 if self.data_mem is not None:
122 self.file.seek(self.data_ofs, SEEK_SET)
123 s = self.file.read(self.chunksize)
124 if len(s) != self.chunksize:
125 raise IOError('Could not read %d data bytes (got %d)' % (self.chunksize, len(s)))
128 def write(self, file):
129 if not isinstance(self.chunkname, bytes):
130 raise ValueError('Chunk name %s is not of type bytes' % self.chunkname)
131 if len(self.chunkname) != 4:
132 raise ValueError('Chunk name %s is not of length 4')
133 if file.write(self.chunkname + struct.pack('<L', self.chunksize)) != 8:
134 raise IOError('Could not write header bytes to destination file at chunk %s' % \
136 if self.container is not None:
138 if not isinstance(self.container, bytes):
139 raise ValueError('Container type %s is not of type bytes' % self.container)
140 if len(self.container) != 4:
141 raise ValueError('Container type %s is not of length 4')
142 if file.write(self.container) != 4:
143 raise IOError('Could not write container bytes to destination file at chunk %s' % \
145 for child in self.children:
147 cld = file.tell() - cld
148 if cld != self.chunksize:
149 raise ValueError('Children wrote %d bytes (expected %d) file at chunk %s' % \
150 (cld, self.chunksize, self.chunkfmt))
152 if self.data_mem is not None:
153 if file.write(self.data_mem) != self.chunksize:
154 raise IOError('Could not write %d data bytes to destination file at chunk %s' % \
155 (self.chunksize, self.chunkfmt))
157 self.file.seek(self.data_ofs, SEEK_SET)
158 total = self.chunksize
163 buf = self.file.read(n)
166 if file.write(buf) != n:
167 raise IOError('Could not write %d data bytes to destination file at chunk %s' % \
169 if self.chunk_pad > 0:
172 raise ValueError('Misaligned file after chunk %s' % self.chunkfmt)
174 def set_length(self, newlen):
175 old = self.chunksize + self.chunk_pad
176 self.chunksize = newlen
177 self.chunk_pad = self.chunksize & 1
178 new = self.chunksize + self.chunk_pad
179 if isinstance(self.parent, RIFFChunk):
180 self.parent.adjust_length(new - old)
182 def set_content(self, content, nul_pad=False):
183 if self.container is not None:
184 raise ValueError('Cannot set content of container type %s' % self.chunkfmt)
185 if isinstance(content, str):
186 content = content.encode('UTF-8')
187 if not isinstance(content, bytes):
188 raise ValueError('New content is not of type bytes')
189 if nul_pad and (len(content) & 1):
191 self.data_mem = content
192 self.set_length(len(content))
194 def adjust_length(self, delta):
195 self.set_length(self.chunksize + delta)
197 class RIFFFile(RIFFChunk):
198 def __init__(self, file):
200 self.container = True
209 self.children.append(child)
212 raise IndexError('No RIFF chunks found')
214 self.justpast = child.justpast
219 for child in self.children:
224 def __getitem__(self, key):
225 return self.children[key]
227 def write(self, file):
228 for child in self.children:
231 def dumpriff(container, level=0, isinfo=False):
232 indent = ('%s%ds' % ('%', 2*level)) % ''
233 print(indent + 'BEGIN level=%d' % level)
234 for chunk in container.children:
235 #print(indent + ' CHUNK %s of size %d, data at %d, next at %d' % (chunk.chunkfmt, chunk.chunksize, chunk.data_ofs, chunk.justpast))
237 print(indent + ' CHUNK %s(%d): %s' % (chunk.chunkfmt, chunk.chunksize, chunk.print()))
239 print(indent + ' CHUNK %s of size %d' % (chunk.chunkfmt, chunk.chunksize))
240 if chunk.container is not None:
241 dumpriff(chunk, level+1, chunk.chunkfmt == b'LIST<INFO>')
242 print(indent + 'END level=%d' % level)
244 if sys.argv[1] == '-i':
246 # bad characters in XML
247 for i in range(0, 32):
248 if i not in (0x09, 0x0A, 0x0D):
249 encode_table[i] = None
250 encode_table[0x7F] = 0xFFFD
251 for i in range(0x80, 0xA0):
252 encode_table[i] = 0xEF00 + i
253 for i in range(0xD800, 0xE000):
254 encode_table[i] = 0xFFFD
255 for i in range(0, 0x110000, 0x10000):
256 encode_table[i + 0xFFFE] = 0xFFFD
257 encode_table[i + 0xFFFF] = 0xFFFD
258 for i in range(0xFDD0, 0xFDF0):
259 encode_table[i] = 0xFFFD
260 # surrogateescape to OPTU-16
261 for i in range(128, 256):
262 encode_table[0xDC00 + i] = 0xEF00 + i
263 ident_encode_table = str.maketrans(encode_table)
267 return s.rstrip(b'\x00').\
268 decode(encoding='utf-8', errors='surrogateescape').\
269 translate(ident_encode_table).\
270 encode(encoding='utf-8', errors='replace')
272 if sys.argv[2] == '-':
275 f = open(sys.argv[2], 'rb')
277 for chunk in riff[0][b'LIST<INFO>'].children:
278 if chunk.chunkname not in (b'ifil', b'isng', b'IPRD', b'ISFT'):
279 for x in (ident_encode(chunk.chunkname), b'\xFE',
280 ident_encode(chunk.print()), b'\xFF'):
281 sys.stdout.buffer.write(x)
285 if sys.argv[1] == '-d':
286 with open(sys.argv[2], 'rb') as f:
290 with open(sys.argv[1], 'rb') as f, open(sys.argv[2], 'wb', buffering=65536) as dst:
294 _flags = { '-a': 1, '-z': 2, '-az': 3 }
295 while i < len(sys.argv):
297 if sys.argv[i] in _flags:
298 flags = _flags[sys.argv[i]]
300 if i >= len(sys.argv):
302 chunks = sys.argv[i].split('/')
303 if chunks[0].isnumeric():
308 chnk = chnk[os.fsencode(cur)]
309 val = os.fsencode(sys.argv[i + 1])
312 chnk.set_content(val, bool(flags & 1))
314 print("=> after processing:")