Skip to content

Commit

Permalink
Add support for floating point wave files.
Browse files Browse the repository at this point in the history
This adds support for floating point wav files and fix python#60729.
  • Loading branch information
lkoenig committed Mar 14, 2023
1 parent a703f74 commit 4ee51f5
Show file tree
Hide file tree
Showing 9 changed files with 142 additions and 27 deletions.
12 changes: 12 additions & 0 deletions Lib/aifc.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@
warnings._deprecated(__name__, remove=(3, 13))


AIFC_ENCODING_LINEAR_PCM = 1

class Error(Exception):
pass

Expand Down Expand Up @@ -400,6 +402,9 @@ def getsampwidth(self):
def getframerate(self):
return self._framerate

def getencoding(self):
return AIFC_ENCODING_LINEAR_PCM

def getcomptype(self):
return self._comptype

Expand Down Expand Up @@ -675,6 +680,13 @@ def setnframes(self, nframes):
def getnframes(self):
return self._nframeswritten

def getencoding(self):
return AIFC_ENCODING_LINEAR_PCM

def setencoding(self, encoding):
if encoding != AIFC_ENCODING_LINEAR_PCM:
raise Error("Unsupported encoding")

def setcomptype(self, comptype, compname):
if self._nframeswritten:
raise Error('cannot change parameters after starting to write')
Expand Down
68 changes: 50 additions & 18 deletions Lib/sunau.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,9 @@ def getcompname(self):
else:
return 'not compressed'

def getencoding(self):
return self._encoding

def getparams(self):
return _sunau_params(self.getnchannels(), self.getsampwidth(),
self.getframerate(), self.getnframes(),
Expand Down Expand Up @@ -342,6 +345,7 @@ def initfp(self, file):
self._datawritten = 0
self._datalength = 0
self._info = b''
self._encoding = AUDIO_FILE_ENCODING_MULAW_8
self._comptype = 'ULAW' # default is U-law

def setnchannels(self, nchannels):
Expand All @@ -362,6 +366,7 @@ def setsampwidth(self, sampwidth):
if sampwidth not in (1, 2, 3, 4):
raise Error('bad sample width')
self._sampwidth = sampwidth
self._update_encoding()

def getsampwidth(self):
if not self._framerate:
Expand Down Expand Up @@ -405,6 +410,31 @@ def getcompname(self):
else:
return 'not compressed'

def setencoding(self, encoding):
if self._nframeswritten:
raise Error('cannot change parameters after starting to write')
if encoding == AUDIO_FILE_ENCODING_LINEAR_8:
self.setcomptype('NONE', None)
self.setsampwidth(1)
elif encoding == AUDIO_FILE_ENCODING_LINEAR_16:
self.setcomptype('NONE', None)
self.setsampwidth(2)
elif encoding == AUDIO_FILE_ENCODING_LINEAR_24:
self.setcomptype('NONE', None)
self.setsampwidth(3)
elif encoding == AUDIO_FILE_ENCODING_LINEAR_32:
self.setcomptype('NONE', None)
self.setsampwidth(4)
elif encoding == AUDIO_FILE_ENCODING_MULAW_8:
self.setcomptype('ULAW', None)
self.setsampwidth(2)
else:
raise Error('unsupported encoding %r', encoding)
assert self._encoding == encoding

def getencoding(self):
return self._encoding

def setparams(self, params):
nchannels, sampwidth, framerate, nframes, comptype, compname = params
self.setnchannels(nchannels)
Expand Down Expand Up @@ -458,38 +488,40 @@ def close(self):
#
# private methods
#

def _ensure_header_written(self):
if not self._nframeswritten:
if not self._nchannels:
raise Error('# of channels not specified')
if not self._sampwidth:
raise Error('sample width not specified')
if not self._framerate:
raise Error('frame rate not specified')
self._write_header()

def _write_header(self):
def _update_encoding(self):
if self._comptype == 'NONE':
if self._sampwidth == 1:
encoding = AUDIO_FILE_ENCODING_LINEAR_8
self._encoding = AUDIO_FILE_ENCODING_LINEAR_8
self._framesize = 1
elif self._sampwidth == 2:
encoding = AUDIO_FILE_ENCODING_LINEAR_16
self._encoding = AUDIO_FILE_ENCODING_LINEAR_16
self._framesize = 2
elif self._sampwidth == 3:
encoding = AUDIO_FILE_ENCODING_LINEAR_24
self._encoding = AUDIO_FILE_ENCODING_LINEAR_24
self._framesize = 3
elif self._sampwidth == 4:
encoding = AUDIO_FILE_ENCODING_LINEAR_32
self._encoding = AUDIO_FILE_ENCODING_LINEAR_32
self._framesize = 4
else:
raise Error('internal error')
elif self._comptype == 'ULAW':
encoding = AUDIO_FILE_ENCODING_MULAW_8
self._encoding = AUDIO_FILE_ENCODING_MULAW_8
self._framesize = 1
else:
raise Error('internal error')

def _ensure_header_written(self):
if not self._nframeswritten:
if not self._nchannels:
raise Error('# of channels not specified')
if not self._sampwidth:
raise Error('sample width not specified')
if not self._framerate:
raise Error('frame rate not specified')
self._write_header()

def _write_header(self):
self._update_encoding()
self._framesize = self._framesize * self._nchannels
_write_u32(self._file, AUDIO_FILE_MAGIC)
header_size = 25 + len(self._info)
Expand All @@ -505,7 +537,7 @@ def _write_header(self):
self._form_length_pos = None
_write_u32(self._file, length)
self._datalength = length
_write_u32(self._file, encoding)
_write_u32(self._file, self._encoding)
_write_u32(self._file, self._framerate)
_write_u32(self._file, self._nchannels)
self._file.write(self._info)
Expand Down
Binary file added Lib/test/audiodata/pluck-float32.wav
Binary file not shown.
12 changes: 9 additions & 3 deletions Lib/test/audiotests.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,14 @@ def tearDown(self):
unlink(TESTFN)

def check_params(self, f, nchannels, sampwidth, framerate, nframes,
comptype, compname):
comptype, compname, encoding):
self.assertEqual(f.getnchannels(), nchannels)
self.assertEqual(f.getsampwidth(), sampwidth)
self.assertEqual(f.getframerate(), framerate)
self.assertEqual(f.getnframes(), nframes)
self.assertEqual(f.getcomptype(), comptype)
self.assertEqual(f.getcompname(), compname)
self.assertEqual(f.getencoding(), encoding)

params = f.getparams()
self.assertEqual(params,
Expand All @@ -51,13 +52,17 @@ def check_params(self, f, nchannels, sampwidth, framerate, nframes,


class AudioWriteTests(AudioTests):
readonly = False

def create_file(self, testfile):
if self.readonly:
self.skipTest('Read only file format')
f = self.fout = self.module.open(testfile, 'wb')
f.setnchannels(self.nchannels)
f.setsampwidth(self.sampwidth)
f.setframerate(self.framerate)
f.setcomptype(self.comptype, self.compname)
f.setencoding(self.encoding)
return f

def check_file(self, testfile, nframes, frames):
Expand All @@ -67,13 +72,14 @@ def check_file(self, testfile, nframes, frames):
self.assertEqual(f.getframerate(), self.framerate)
self.assertEqual(f.getnframes(), nframes)
self.assertEqual(f.readframes(nframes), frames)
self.assertEqual(f.getencoding(), self.encoding)

def test_write_params(self):
f = self.create_file(TESTFN)
f.setnframes(self.nframes)
f.writeframes(self.frames)
self.check_params(f, self.nchannels, self.sampwidth, self.framerate,
self.nframes, self.comptype, self.compname)
self.nframes, self.comptype, self.compname, self.encoding)
f.close()

def test_write_context_manager_calls_close(self):
Expand Down Expand Up @@ -257,7 +263,7 @@ def test_read_params(self):
f = self.f = self.module.open(self.sndfilepath)
#self.assertEqual(f.getfp().name, self.sndfilepath)
self.check_params(f, self.nchannels, self.sampwidth, self.framerate,
self.sndfilenframes, self.comptype, self.compname)
self.sndfilenframes, self.comptype, self.compname, self.encoding)

def test_close(self):
with open(self.sndfilepath, 'rb') as testfile:
Expand Down
1 change: 1 addition & 0 deletions Lib/test/test_aifc.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class AifcTest(audiotests.AudioWriteTests,
module = aifc
close_fd = True
test_unseekable_read = None
encoding = aifc.AIFC_ENCODING_LINEAR_PCM


class AifcPCM8Test(AifcTest, unittest.TestCase):
Expand Down
6 changes: 6 additions & 0 deletions Lib/test/test_sunau.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from test import audiotests
import io
import struct
import sunau
import sys
from test.support import warnings_helper

Expand All @@ -21,6 +22,7 @@ class SunauPCM8Test(SunauTest, unittest.TestCase):
sampwidth = 1
framerate = 11025
nframes = 48
encoding = sunau.AUDIO_FILE_ENCODING_LINEAR_8
comptype = 'NONE'
compname = 'not compressed'
frames = bytes.fromhex("""\
Expand All @@ -38,6 +40,7 @@ class SunauPCM16Test(SunauTest, unittest.TestCase):
sampwidth = 2
framerate = 11025
nframes = 48
encoding = sunau.AUDIO_FILE_ENCODING_LINEAR_16
comptype = 'NONE'
compname = 'not compressed'
frames = bytes.fromhex("""\
Expand All @@ -57,6 +60,7 @@ class SunauPCM24Test(SunauTest, unittest.TestCase):
sampwidth = 3
framerate = 11025
nframes = 48
encoding = sunau.AUDIO_FILE_ENCODING_LINEAR_24
comptype = 'NONE'
compname = 'not compressed'
frames = bytes.fromhex("""\
Expand All @@ -82,6 +86,7 @@ class SunauPCM32Test(SunauTest, unittest.TestCase):
sampwidth = 4
framerate = 11025
nframes = 48
encoding = sunau.AUDIO_FILE_ENCODING_LINEAR_32
comptype = 'NONE'
compname = 'not compressed'
frames = bytes.fromhex("""\
Expand All @@ -107,6 +112,7 @@ class SunauULAWTest(SunauTest, unittest.TestCase):
sampwidth = 2
framerate = 11025
nframes = 48
encoding = sunau.AUDIO_FILE_ENCODING_MULAW_8
comptype = 'ULAW'
compname = 'CCITT G.711 u-law'
frames = bytes.fromhex("""\
Expand Down
33 changes: 32 additions & 1 deletion Lib/test/test_wave.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class WavePCM8Test(WaveTest, unittest.TestCase):
sampwidth = 1
framerate = 11025
nframes = 48
encoding = wave.WAVE_FORMAT_PCM
comptype = 'NONE'
compname = 'not compressed'
frames = bytes.fromhex("""\
Expand All @@ -36,6 +37,7 @@ class WavePCM16Test(WaveTest, unittest.TestCase):
sampwidth = 2
framerate = 11025
nframes = 48
encoding = wave.WAVE_FORMAT_PCM
comptype = 'NONE'
compname = 'not compressed'
frames = bytes.fromhex("""\
Expand All @@ -57,6 +59,7 @@ class WavePCM24Test(WaveTest, unittest.TestCase):
sampwidth = 3
framerate = 11025
nframes = 48
encoding = wave.WAVE_FORMAT_PCM
comptype = 'NONE'
compname = 'not compressed'
frames = bytes.fromhex("""\
Expand Down Expand Up @@ -84,6 +87,8 @@ class WavePCM24ExtTest(WaveTest, unittest.TestCase):
sampwidth = 3
framerate = 11025
nframes = 48
encoding = wave.WAVE_FORMAT_EXTENSIBLE
readonly = True # Writing EXTENSIBLE wave format is not supported.
comptype = 'NONE'
compname = 'not compressed'
frames = bytes.fromhex("""\
Expand Down Expand Up @@ -111,6 +116,7 @@ class WavePCM32Test(WaveTest, unittest.TestCase):
sampwidth = 4
framerate = 11025
nframes = 48
encoding = wave.WAVE_FORMAT_PCM
comptype = 'NONE'
compname = 'not compressed'
frames = bytes.fromhex("""\
Expand All @@ -131,9 +137,34 @@ class WavePCM32Test(WaveTest, unittest.TestCase):
frames = wave._byteswap(frames, 4)


class WaveIeeeFloatingPointTest(WaveTest, unittest.TestCase):
sndfilename = 'pluck-float32.wav'
sndfilenframes = 3307
nchannels = 2
sampwidth = 4
framerate = 11025
nframes = 48
encoding = wave.WAVE_FORMAT_IEEE_FLOAT
comptype = 'NONE'
compname = 'not compressed'
frames = bytes.fromhex("""\
60598B3C001423BA 1FB4163F8054FA3B 0E4FC43E80C51D3D 53467EBF4030843D \
FC84D0BE304C563D 3053113F40BEFC3C B72F00BFC03E583C E0FEDA3C805142BC \
54510FBFE02638BD 569F16BF40FDCABD C060A63EECA421BE 3CE5523E2C3349BE \
0C2E10BE14725BBE 5268E7BEDC3B6CBE 985AE03D80497ABE B4B606BEECB67EBE \
B0B12E3FC87C6CBE 005519BD4C0F3EBE F8BD1B3EECDF03BE 924E9FBE588D8DBD \
D4E150BF501711BD B079A0BD20FBFBBC 5863863D40760CBD 0E3C83BE40E217BD \
04FF0B3EF07839BD E29AFB3E80A714BD B91007BFE042D3BC B5AD4D3F80CDA0BB \
1AB1C3BEB04E023D D33A063FC0A8973D 8012F9BEE074EC3D 7341223FD415153E \
D80409BE04A63A3E 00F27BBFBC25333E 0000803FFC29223E 000080BF38A7143E \
3638133F283BEB3D 7C6E253F00CADB3D 686A02BE88FDF53D 920CC7BE28E1FB3D \
185B5ABED8A2CE3D 5189463FC8A7A53D E88F8C3DF0FFA13D 1CE6AE3EE0A0B03D \
DF90223F184EE43D 376768BF2CD8093E 281612BF60B3EE3D 2F26083F88B4A53D \
""")

class MiscTestCase(unittest.TestCase):
def test__all__(self):
not_exported = {'WAVE_FORMAT_PCM', 'WAVE_FORMAT_EXTENSIBLE', 'KSDATAFORMAT_SUBTYPE_PCM'}
not_exported = {'WAVE_FORMAT_PCM', 'WAVE_FORMAT_IEEE_FLOAT', 'WAVE_FORMAT_EXTENSIBLE', 'KSDATAFORMAT_SUBTYPE_PCM'}
support.check__all__(self, wave, not_exported=not_exported)


Expand Down
Loading

0 comments on commit 4ee51f5

Please sign in to comment.