Skip to content

Commit 1701bf5

Browse files
committed
wave: write FACT chunk for IEEE float
Per the RIFF/WAVE Rev. 3 documentation, non-PCM formats require a fact chunk, while PCM does not. This is also what libsdnfile/audacity do Reference: https://mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html (see the 'fact Chunk' section and linked Rev. 3 RIFF docs).
1 parent e025d53 commit 1701bf5

2 files changed

Lines changed: 76 additions & 4 deletions

File tree

Lib/test/test_wave.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,58 @@ def test_open_in_write_raises(self):
258258
support.gc_collect()
259259
self.assertIsNone(cm.unraisable)
260260

261+
def test_ieee_float_has_fact_chunk(self):
262+
nframes = 100
263+
with tempfile.NamedTemporaryFile(delete_on_close=False) as fp:
264+
filename = fp.name
265+
self.addCleanup(unlink, filename)
266+
267+
with wave.open(filename, 'wb') as w:
268+
w.setnchannels(1)
269+
w.setsampwidth(2)
270+
w.setframerate(22050)
271+
w.setformat(wave.WAVE_FORMAT_IEEE_FLOAT)
272+
w.writeframes(b'\x00\x00' * nframes)
273+
274+
with open(filename, 'rb') as f:
275+
f.read(12)
276+
fact_found = False
277+
fact_samples = None
278+
while True:
279+
chunk_id = f.read(4)
280+
if len(chunk_id) < 4:
281+
break
282+
chunk_size = struct.unpack('<L', f.read(4))[0]
283+
if chunk_id == b'fact':
284+
fact_found = True
285+
fact_samples = struct.unpack('<L', f.read(4))[0]
286+
break
287+
f.seek(chunk_size + (chunk_size & 1), 1)
288+
289+
self.assertTrue(fact_found)
290+
self.assertEqual(fact_samples, nframes)
291+
292+
def test_pcm_has_no_fact_chunk(self):
293+
with tempfile.NamedTemporaryFile(delete_on_close=False) as fp:
294+
filename = fp.name
295+
self.addCleanup(unlink, filename)
296+
297+
with wave.open(filename, 'wb') as w:
298+
w.setnchannels(1)
299+
w.setsampwidth(2)
300+
w.setframerate(22050)
301+
w.writeframes(b'\x00\x00' * 100)
302+
303+
with open(filename, 'rb') as f:
304+
f.read(12)
305+
while True:
306+
chunk_id = f.read(4)
307+
if len(chunk_id) < 4:
308+
break
309+
chunk_size = struct.unpack('<L', f.read(4))[0]
310+
self.assertNotEqual(chunk_id, b'fact')
311+
f.seek(chunk_size + (chunk_size & 1), 1)
312+
261313

262314
class WaveOpen(unittest.TestCase):
263315
def test_open_pathlike(self):

Lib/wave.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,7 @@ def initfp(self, file):
468468
self._nframeswritten = 0
469469
self._datawritten = 0
470470
self._datalength = 0
471+
self._fact_sample_count_pos = None
471472
self._headerwritten = False
472473

473474
def __del__(self):
@@ -615,6 +616,9 @@ def _ensure_header_written(self, datasize):
615616
raise Error('sampling rate not specified')
616617
self._write_header(datasize)
617618

619+
def _needs_fact_chunk(self):
620+
return self._format == WAVE_FORMAT_IEEE_FLOAT
621+
618622
def _write_header(self, initlength):
619623
assert not self._headerwritten
620624
self._file.write(b'RIFF')
@@ -625,12 +629,23 @@ def _write_header(self, initlength):
625629
self._form_length_pos = self._file.tell()
626630
except (AttributeError, OSError):
627631
self._form_length_pos = None
628-
self._file.write(struct.pack('<L4s4sLHHLLHH4s',
629-
36 + self._datalength, b'WAVE', b'fmt ', 16,
632+
has_fact = self._needs_fact_chunk()
633+
header_overhead = 36 + (12 if has_fact else 0)
634+
self._file.write(struct.pack('<L4s4sLHHLLHH',
635+
header_overhead + self._datalength, b'WAVE', b'fmt ', 16,
630636
self._format, self._nchannels, self._framerate,
631637
self._nchannels * self._framerate * self._sampwidth,
632638
self._nchannels * self._sampwidth,
633-
self._sampwidth * 8, b'data'))
639+
self._sampwidth * 8))
640+
if has_fact:
641+
self._file.write(b'fact')
642+
self._file.write(struct.pack('<L', 4))
643+
try:
644+
self._fact_sample_count_pos = self._file.tell()
645+
except (AttributeError, OSError):
646+
self._fact_sample_count_pos = None
647+
self._file.write(struct.pack('<L', self._nframes))
648+
self._file.write(b'data')
634649
if self._form_length_pos is not None:
635650
self._data_length_pos = self._file.tell()
636651
self._file.write(struct.pack('<L', self._datalength))
@@ -641,8 +656,13 @@ def _patchheader(self):
641656
if self._datawritten == self._datalength:
642657
return
643658
curpos = self._file.tell()
659+
header_overhead = 36 + (12 if self._needs_fact_chunk() else 0)
644660
self._file.seek(self._form_length_pos, 0)
645-
self._file.write(struct.pack('<L', 36 + self._datawritten))
661+
self._file.write(struct.pack('<L', header_overhead + self._datawritten))
662+
if self._fact_sample_count_pos is not None:
663+
self._file.seek(self._fact_sample_count_pos, 0)
664+
nframes = self._datawritten // (self._nchannels * self._sampwidth)
665+
self._file.write(struct.pack('<L', nframes))
646666
self._file.seek(self._data_length_pos, 0)
647667
self._file.write(struct.pack('<L', self._datawritten))
648668
self._file.seek(curpos, 0)

0 commit comments

Comments
 (0)