Skip to content

Commit d42a04c

Browse files
mbeijenlkoenig
andauthored
GH-60729: Add IEEE format wave audio support (GH-145931)
(this re-applies reverted commit 61f2a1a, with a test fix) Co-authored-by: Lionel Koenig <lionelk@google.com>
1 parent 6486211 commit d42a04c

File tree

7 files changed

+354
-25
lines changed

7 files changed

+354
-25
lines changed

Doc/library/wave.rst

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,19 @@
99
--------------
1010

1111
The :mod:`!wave` module provides a convenient interface to the Waveform Audio
12-
"WAVE" (or "WAV") file format. Only uncompressed PCM encoded wave files are
13-
supported.
12+
"WAVE" (or "WAV") file format.
13+
14+
The module supports uncompressed PCM and IEEE floating-point WAV formats.
1415

1516
.. versionchanged:: 3.12
1617

1718
Support for ``WAVE_FORMAT_EXTENSIBLE`` headers was added, provided that the
1819
extended format is ``KSDATAFORMAT_SUBTYPE_PCM``.
1920

21+
.. versionchanged:: next
22+
23+
Support for reading and writing ``WAVE_FORMAT_IEEE_FLOAT`` files was added.
24+
2025
The :mod:`!wave` module defines the following function and exception:
2126

2227

@@ -60,6 +65,21 @@ The :mod:`!wave` module defines the following function and exception:
6065
specification or hits an implementation deficiency.
6166

6267

68+
.. data:: WAVE_FORMAT_PCM
69+
70+
Format code for uncompressed PCM audio.
71+
72+
73+
.. data:: WAVE_FORMAT_IEEE_FLOAT
74+
75+
Format code for IEEE floating-point audio.
76+
77+
78+
.. data:: WAVE_FORMAT_EXTENSIBLE
79+
80+
Format code for WAVE extensible headers.
81+
82+
6383
.. _wave-read-objects:
6484

6585
Wave_read Objects
@@ -98,6 +118,14 @@ Wave_read Objects
98118
Returns number of audio frames.
99119

100120

121+
.. method:: getformat()
122+
123+
Returns the frame format code.
124+
125+
This is one of :data:`WAVE_FORMAT_PCM`,
126+
:data:`WAVE_FORMAT_IEEE_FLOAT`, or :data:`WAVE_FORMAT_EXTENSIBLE`.
127+
128+
101129
.. method:: getcomptype()
102130

103131
Returns compression type (``'NONE'`` is the only supported type).
@@ -112,8 +140,8 @@ Wave_read Objects
112140
.. method:: getparams()
113141

114142
Returns a :func:`~collections.namedtuple` ``(nchannels, sampwidth,
115-
framerate, nframes, comptype, compname)``, equivalent to output of the
116-
``get*()`` methods.
143+
framerate, nframes, comptype, compname)``, equivalent to output
144+
of the ``get*()`` methods.
117145

118146

119147
.. method:: readframes(n)
@@ -190,6 +218,9 @@ Wave_write Objects
190218

191219
Set the sample width to *n* bytes.
192220

221+
For :data:`WAVE_FORMAT_IEEE_FLOAT`, only 4-byte (32-bit) and
222+
8-byte (64-bit) sample widths are supported.
223+
193224

194225
.. method:: getsampwidth()
195226

@@ -238,11 +269,32 @@ Wave_write Objects
238269
Return the human-readable compression type name.
239270

240271

272+
.. method:: setformat(format)
273+
274+
Set the frame format code.
275+
276+
Supported values are :data:`WAVE_FORMAT_PCM` and
277+
:data:`WAVE_FORMAT_IEEE_FLOAT`.
278+
279+
When setting :data:`WAVE_FORMAT_IEEE_FLOAT`, the sample width must be
280+
4 or 8 bytes.
281+
282+
283+
.. method:: getformat()
284+
285+
Return the current frame format code.
286+
287+
241288
.. method:: setparams(tuple)
242289

243-
The *tuple* should be ``(nchannels, sampwidth, framerate, nframes, comptype,
244-
compname)``, with values valid for the ``set*()`` methods. Sets all
245-
parameters.
290+
The *tuple* should be
291+
``(nchannels, sampwidth, framerate, nframes, comptype, compname, format)``,
292+
with values valid for the ``set*()`` methods. Sets all parameters.
293+
294+
For backwards compatibility, a 6-item tuple without *format* is also
295+
accepted and defaults to :data:`WAVE_FORMAT_PCM`.
296+
297+
For ``format=WAVE_FORMAT_IEEE_FLOAT``, *sampwidth* must be 4 or 8.
246298

247299

248300
.. method:: getparams()
@@ -279,3 +331,6 @@ Wave_write Objects
279331
Note that it is invalid to set any parameters after calling :meth:`writeframes`
280332
or :meth:`writeframesraw`, and any attempt to do so will raise
281333
:exc:`wave.Error`.
334+
335+
For :data:`WAVE_FORMAT_IEEE_FLOAT` output, a ``fact`` chunk is written as
336+
required by the WAVE specification for non-PCM formats.

Doc/whatsnew/3.15.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1511,6 +1511,21 @@ typing
15111511
wave
15121512
----
15131513

1514+
* Added support for IEEE floating-point WAVE audio
1515+
(``WAVE_FORMAT_IEEE_FLOAT``) in :mod:`wave`.
1516+
1517+
* Added :meth:`wave.Wave_read.getformat`, :meth:`wave.Wave_write.getformat`,
1518+
and :meth:`wave.Wave_write.setformat` for explicit frame format handling.
1519+
1520+
* :meth:`wave.Wave_write.setparams` accepts both 7-item tuples including
1521+
``format`` and 6-item tuples for backwards compatibility (defaulting to
1522+
``WAVE_FORMAT_PCM``).
1523+
1524+
* ``WAVE_FORMAT_IEEE_FLOAT`` output now includes a ``fact`` chunk,
1525+
as required for non-PCM WAVE formats.
1526+
1527+
(Contributed by Lionel Koenig and Michiel W. Beijen in :gh:`60729`.)
1528+
15141529
* Removed the ``getmark()``, ``setmark()`` and ``getmarkers()`` methods
15151530
of the :class:`~wave.Wave_read` and :class:`~wave.Wave_write` classes,
15161531
which were deprecated since Python 3.13.
25.9 KB
Binary file not shown.

Lib/test/audiotests.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,18 @@ def tearDown(self):
2727
unlink(TESTFN)
2828

2929
def check_params(self, f, nchannels, sampwidth, framerate, nframes,
30-
comptype, compname):
30+
comptype, compname, format):
3131
self.assertEqual(f.getnchannels(), nchannels)
3232
self.assertEqual(f.getsampwidth(), sampwidth)
3333
self.assertEqual(f.getframerate(), framerate)
3434
self.assertEqual(f.getnframes(), nframes)
3535
self.assertEqual(f.getcomptype(), comptype)
3636
self.assertEqual(f.getcompname(), compname)
37+
self.assertEqual(f.getformat(), format)
3738

3839
params = f.getparams()
3940
self.assertEqual(params,
40-
(nchannels, sampwidth, framerate, nframes, comptype, compname))
41+
(nchannels, sampwidth, framerate, nframes, comptype, compname))
4142
self.assertEqual(params.nchannels, nchannels)
4243
self.assertEqual(params.sampwidth, sampwidth)
4344
self.assertEqual(params.framerate, framerate)
@@ -51,13 +52,17 @@ def check_params(self, f, nchannels, sampwidth, framerate, nframes,
5152

5253

5354
class AudioWriteTests(AudioTests):
55+
readonly = False
5456

5557
def create_file(self, testfile):
58+
if self.readonly:
59+
self.skipTest('Read only file format')
5660
f = self.fout = self.module.open(testfile, 'wb')
5761
f.setnchannels(self.nchannels)
5862
f.setsampwidth(self.sampwidth)
5963
f.setframerate(self.framerate)
6064
f.setcomptype(self.comptype, self.compname)
65+
f.setformat(self.format)
6166
return f
6267

6368
def check_file(self, testfile, nframes, frames):
@@ -67,13 +72,14 @@ def check_file(self, testfile, nframes, frames):
6772
self.assertEqual(f.getframerate(), self.framerate)
6873
self.assertEqual(f.getnframes(), nframes)
6974
self.assertEqual(f.readframes(nframes), frames)
75+
self.assertEqual(f.getformat(), self.format)
7076

7177
def test_write_params(self):
7278
f = self.create_file(TESTFN)
7379
f.setnframes(self.nframes)
7480
f.writeframes(self.frames)
7581
self.check_params(f, self.nchannels, self.sampwidth, self.framerate,
76-
self.nframes, self.comptype, self.compname)
82+
self.nframes, self.comptype, self.compname, self.format)
7783
f.close()
7884

7985
def test_write_context_manager_calls_close(self):
@@ -257,7 +263,7 @@ def test_read_params(self):
257263
f = self.f = self.module.open(self.sndfilepath)
258264
#self.assertEqual(f.getfp().name, self.sndfilepath)
259265
self.check_params(f, self.nchannels, self.sampwidth, self.framerate,
260-
self.sndfilenframes, self.comptype, self.compname)
266+
self.sndfilenframes, self.comptype, self.compname, self.format)
261267

262268
def test_close(self):
263269
with open(self.sndfilepath, 'rb') as testfile:
@@ -298,6 +304,8 @@ def test_read(self):
298304
f.setpos(f.getnframes() + 1)
299305

300306
def test_copy(self):
307+
if self.readonly:
308+
self.skipTest('Read only file format')
301309
f = self.f = self.module.open(self.sndfilepath)
302310
fout = self.fout = self.module.open(TESTFN, 'wb')
303311
fout.setparams(f.getparams())

0 commit comments

Comments
 (0)