diff --git a/pysoundfile.py b/pysoundfile.py index e6fbb1a..76a245c 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -333,13 +333,17 @@ def __init__(self, file, mode='r', sample_rate=None, channels=None, raise ValueError("Invalid mode: %s" % repr(mode)) original_format = format - if format is None: - filename = getattr(file, 'name', file) - format = str(filename).rsplit('.', 1)[-1].upper() - if self.mode == 'w' and format not in _formats: - raise TypeError( - "No format specified and unable to get format from " - "file extension: %s" % repr(filename)) + filename = getattr(file, 'name', file) + file_extension = str(filename).rsplit('.', 1)[-1].upper() + if format is None and ('w' in self.mode or + file_extension == 'RAW'): + if file_extension not in _formats: + if self.mode == 'w': + raise TypeError( + "No format specified and unable to get format from " + "file extension: %s" % repr(filename)) + else: + format = file_extension self._info = _ffi.new("SF_INFO*") if self.mode == 'w' or str(format).upper() == 'RAW': @@ -349,7 +353,16 @@ def __init__(self, file, mode='r', sample_rate=None, channels=None, if channels is None: raise TypeError("channels must be specified") self._info.channels = channels + if str(format).upper() == 'RAW' and subtype is None: + raise TypeError("RAW files must specify a subtype") self._info.format = _format_int(format, subtype, endian) + elif self.mode == 'rw': + if sample_rate is not None: + self._info.samplerate = sample_rate + if channels is not None: + self._info.channels = channels + if format is not None: + self._info.format = _format_int(format, subtype, endian) else: if [sample_rate, channels, original_format, subtype, endian] != \ [None] * 5: diff --git a/tests/test_pysoundfile.py b/tests/test_pysoundfile.py index 90b608a..9bd0fe3 100644 --- a/tests/test_pysoundfile.py +++ b/tests/test_pysoundfile.py @@ -1,338 +1,494 @@ -import unittest import pysoundfile as sf import numpy as np import os -import io - -class TestWaveFile(unittest.TestCase): - def setUp(self): - """create a dummy wave file""" - self.sample_rate = 44100 - self.channels = 2 - self.filename = 'test.wav' - self.data = np.ones((self.sample_rate, self.channels))*0.5 - with sf.SoundFile(self.filename, 'w', self.sample_rate, self.channels) as f: - f.write(self.data) - - def tearDown(self): - os.remove(self.filename) - - -class TestBasicAttributesOfWaveFile(TestWaveFile): - def test_file_exists(self): - """The test file should exist""" - self.assertTrue(os.path.isfile(self.filename)) - - def test_open_file_descriptor(self): - """Opening a file handle should work""" - handle = os.open(self.filename, os.O_RDONLY) - with sf.SoundFile(handle) as f: - self.assertTrue(np.all(self.data == f[:])) - - def test_open_virtual_io(self): - """Opening a file-like object should work""" - with open(self.filename, 'rb') as bytesio: - with sf.SoundFile(bytesio) as f: - self.assertTrue(np.all(self.data == f[:])) - - def test_read_mode(self): - """Opening the file in read mode should open in read mode from beginning""" - with sf.SoundFile(self.filename) as f: - self.assertEqual(f.mode, 'r') - self.assertEqual(f.seek(0, sf.SEEK_CUR), 0) - - def test_write_mode(self): - """Opening the file in write mode should open in write mode from beginning""" - with sf.SoundFile(self.filename, 'w', self.sample_rate, self.channels) as f: - self.assertEqual(f.mode, 'w') - self.assertEqual(f.seek(0, sf.SEEK_CUR), 0) - - def test_rw_mode(self): - """Opening the file in rw mode should open in rw mode from end""" - with sf.SoundFile(self.filename, 'rw') as f: - self.assertEqual(f.mode, 'rw') - self.assertEqual(f.seek(0, sf.SEEK_CUR), len(f)) - - def test_channels(self): - """The test file should have the correct number of channels""" - with sf.SoundFile(self.filename) as f: - self.assertEqual(f.channels, self.channels) - - def test_sample_rate(self): - """The test file should have the correct number of sample rate""" - with sf.SoundFile(self.filename) as f: - self.assertEqual(f.sample_rate, self.sample_rate) - - def test_format(self): - """The test file should be a wave file""" - with sf.SoundFile(self.filename) as f: - self.assertEqual(f.format, 'WAV') - self.assertEqual(f.subtype, 'PCM_16') - self.assertEqual(f.endian, 'FILE') - self.assertEqual(f.format_info, 'WAV (Microsoft)') - self.assertEqual(f.subtype_info, 'Signed 16 bit PCM') - - def test_context_manager(self): - """The context manager should close the file""" - with sf.SoundFile(self.filename) as f: - pass - self.assertTrue(f.closed) - - def test_closing(self): - """Closing a file should close it""" - f = sf.SoundFile(self.filename) - self.assertFalse(f.closed) - f.close() - self.assertTrue(f.closed) - - def test_file_length(self): - """The file should have the correct length""" - with sf.SoundFile(self.filename) as f: - self.assertEqual(len(f), self.sample_rate) - - def test_file_contents(self): - """The file should contain the correct data""" - with sf.SoundFile(self.filename) as f: - self.assertTrue(np.all(self.data == f[:])) - - def test_file_attributes(self): - """Changing a file attribute should save it on disk""" - with sf.SoundFile(self.filename, 'rw') as f: - f.title = 'testing' - with sf.SoundFile(self.filename) as f: - self.assertEqual(f.title, 'testing') - - def test_non_file_attributes(self): - """Changing a non-file attribute should not save to disk""" - with sf.SoundFile(self.filename, 'rw') as f: - f.foobar = 'testing' - with sf.SoundFile(self.filename) as f: - with self.assertRaises(AttributeError): - f.foobar - - -class TestSeekWaveFile(TestWaveFile): - def test_seek(self): - """Seeking should advance the read/write pointer""" - with sf.SoundFile(self.filename) as f: - self.assertEqual(f.seek(100), 100) - - def test_seek_cur(self): - """seeking multiple times should advance the read/write pointer""" - with sf.SoundFile(self.filename) as f: - f.seek(100) - self.assertEqual(f.seek(100, whence=sf.SEEK_CUR), 200) - - def test_seek_end(self): - """seeking from end should advance the read/write pointer""" - with sf.SoundFile(self.filename) as f: - self.assertEqual(f.seek(-100, whence=SEEK_END), self.sample_rate-100) - - def test_seek_read(self): - """Read-seeking should advance the read pointer""" - with sf.SoundFile(self.filename) as f: - self.assertEqual(f.seek(100, which='r'), 100) - - def test_seek_write(self): - """write-seeking should advance the write pointer""" - with sf.SoundFile(self.filename, 'rw') as f: - self.assertEqual(f.seek(100, which='w'), 100) - - def test_flush(self): - """After flushing, data should be written to disk""" - with sf.SoundFile(self.filename, 'rw') as f: - size = os.path.getsize(self.filename) - f.write(np.zeros((10,2))) - f.flush() - self.assertEqual(os.path.getsize(self.filename), size+40) - - -class TestSeekWaveFile(TestWaveFile): - def test_read(self): - """read should read data and advance the read pointer""" - with sf.SoundFile(self.filename) as f: - data = f.read(100) - self.assertTrue(np.all(data == self.data[:100])) - self.assertEqual(100, f.seek(0, sf.SEEK_CUR)) - - def test_read_write_only(self): - """reading a write-only file should not work""" - with sf.SoundFile(self.filename, 'w', self.sample_rate, self.channels) as f: - with self.assertRaises(RuntimeError) as err: - f.read(100) - - def test_default_read_format(self): - """By default, np.float64 should be read""" - with sf.SoundFile(self.filename) as f: - self.assertEqual(f[:].dtype, np.float64) - - def test_read_int16(self): - """reading 16 bit integers should read np.int16""" - with sf.SoundFile(self.filename) as f: - data = f.read(100, dtype='int16') - self.assertEqual(data.dtype, np.int16) - - def test_read_int32(self): - """reading 32 bit integers should read np.int32""" - with sf.SoundFile(self.filename) as f: - data = f.read(100, dtype='int32') - self.assertEqual(data.dtype, np.int32) - - def test_read_float32(self): - """reading 32 bit floats should read np.float32""" - with sf.SoundFile(self.filename) as f: - data = f.read(100, dtype='float32') - self.assertEqual(data.dtype, np.float32) - - def test_read_indexing(self): - """Reading using indexing should read but not advance read pointer""" - with sf.SoundFile(self.filename) as f: - self.assertTrue(np.all(f[:100] == self.data[:100])) - self.assertEqual(0, f.seek(0, sf.SEEK_CUR)) - - def test_read_number_of_frames(self): - """Reading N frames should return N frames""" - with sf.SoundFile(self.filename) as f: - data = f.read(100) - self.assertEqual(len(data), 100) - - def test_read_all_frames(self): - """Reading should return all remaining frames""" - with sf.SoundFile(self.filename) as f: - f.seek(-100, sf.SEEK_END) - data = f.read() - self.assertEqual(len(data), 100) - - def test_read_number_of_frames_over_end(self): - """Reading N frames at EOF should return only remaining frames""" - with sf.SoundFile(self.filename) as f: - f.seek(-50, sf.SEEK_END) - data = f.read(100) - self.assertEqual(len(data), 50) - - def test_read_number_of_frames_over_end_with_fill(self): - """Reading N frames with fill at EOF should return N frames""" - with sf.SoundFile(self.filename) as f: - f.seek(-50, sf.SEEK_END) - data = f.read(100, fill_value=0) - self.assertEqual(len(data), 100) - self.assertTrue(np.all(data[50:] == 0)) - - def test_read_into_out(self): - """Reading into out should return data and write into out""" - with sf.SoundFile(self.filename) as f: - data = np.empty((100, f.channels), dtype='float64') - out_data = f.read(out=data) - self.assertTrue(np.all(data == out_data)) - - def test_read_mono_into_out(self): - """Reading mono signal into out should return data and write into out""" - # create a dummy mono wave file - self.sample_rate = 44100 - self.channels = 1 - self.filename = 'test.wav' - self.data = np.ones((self.sample_rate, self.channels))*0.5 - with sf.SoundFile(self.filename, 'w', self.sample_rate, self.channels) as f: - f.write(self.data) - - with sf.SoundFile(self.filename) as f: - data = np.empty((100, f.channels), dtype='float64') - out_data = f.read(out=data) - self.assertTrue(np.all(data == out_data)) - - def test_read_into_out_with_too_many_channels(self): - """Reading into malformed out should throw an error""" - with sf.SoundFile(self.filename) as f: - data = np.empty((100, f.channels+1), dtype='float64') - with self.assertRaises(ValueError) as err: - out_data = f.read(out=data) - - def test_read_into_out_with_too_many_dimensions(self): - """Reading into malformed out should throw an error""" - with sf.SoundFile(self.filename) as f: - data = np.empty((100, f.channels, 1), dtype='float64') - with self.assertRaises(ValueError) as err: - out_data = f.read(out=data) - - def test_read_into_zero_len_out(self): - """Reading into aa zero len out should not read anything""" - with sf.SoundFile(self.filename) as f: - data = np.empty((0, f.channels), dtype='float64') - out_data = f.read(out=data) - self.assertTrue(np.all(data == out_data)) - - def test_read_into_out_over_end(self): - """Reading into out over end should return shorter data and write into out""" - with sf.SoundFile(self.filename) as f: - data = np.empty((100, f.channels), dtype='float64') - f.seek(-50, sf.SEEK_END) - out_data = f.read(out=data) - self.assertTrue(np.all(data[:50] == out_data[:50])) - self.assertEqual(out_data.shape, (50,2)) - self.assertEqual(data.shape, (100,2)) - - def test_read_into_out_over_end_with_fill(self): - """Reading into out over end with fill should return padded data and write into out""" - with sf.SoundFile(self.filename) as f: - data = np.empty((100, f.channels), dtype='float64') - f.seek(-50, sf.SEEK_END) - out_data = f.read(out=data, fill_value=0) - self.assertTrue(np.all(data == out_data)) - self.assertTrue(np.all(data[50:] == 0)) - - def test_read_mono_as_array(self): - """Reading with always_2d=False should return array""" - # create a dummy mono wave file - self.sample_rate = 44100 - self.channels = 1 - self.filename = 'test.wav' - self.data = np.ones((self.sample_rate, self.channels))*0.5 - with sf.SoundFile(self.filename, 'w', self.sample_rate, self.channels) as f: - f.write(self.data) - - with sf.SoundFile(self.filename) as f: - data = f.read(100, always_2d=False) - self.assertEqual(data.shape, (100,)) - -class TestWriteWaveFile(TestWaveFile): - def test_write(self): - """write should write data and advance the write pointer""" - with sf.SoundFile(self.filename, 'rw') as f: - data = np.zeros((100,2)) - position = f.seek(0, sf.SEEK_CUR) - f.write(data) - self.assertTrue(np.all(f[-100:] == data)) - self.assertEqual(100, f.seek(0, sf.SEEK_CUR)-position) - - def test_write_read_only(self): - """writing to a read-only file should not work""" - with sf.SoundFile(self.filename) as f: - with self.assertRaises(RuntimeError) as err: - f.write(np.ones((100,2))) - - def test_write_float_precision(self): - """Written float data should be written at most 2**-15 off""" - with sf.SoundFile(self.filename, 'rw') as f: - data = np.ones((100,2)) - f.write(data) - written_data = f[-100:] - self.assertTrue(np.allclose(data, written_data, atol=2**-15)) - - def test_write_int_precision(self): - """Written int data should be written""" - with sf.SoundFile(self.filename, 'rw') as f: - data = np.zeros((100,2)) + 2**15-1 # full scale int16 - data = np.array(data, dtype='int16') - f.write(data) - f.seek(-100, sf.SEEK_CUR) - written_data = f.read(dtype='int16') - self.assertTrue(np.all(data == written_data)) - - def test_write_indexing(self): - """Writing using indexing should write but not advance write pointer""" - with sf.SoundFile(self.filename, 'rw') as f: - position = f.seek(0, sf.SEEK_CUR) - data = np.zeros((100,2)) - f[:100] = data - self.assertEqual(position, f.seek(0, sf.SEEK_CUR)) - self.assertTrue(np.all(data == f[:100])) +import pytest + +data_r = np.array([[1.0, -1.0], + [0.8, -0.8], + [0.6, -0.6], + [0.4, -0.4], + [0.2, -0.2]]) +file_r = 'tests/test_r.wav' +data_r_mono = np.array([1.0, 0.8, 0.6, 0.4, 0.2]) +file_r_mono = 'tests/test_r_mono.wav' +data_r_raw = np.array(data_r, copy=True) +file_r_raw = 'tests/test_r.raw' +file_w = 'tests/test_w.wav' + + +def allclose(x, y): + return np.allclose(x, y, atol=2**-15) + + +def open_filename(filename, rw, _): + if rw == 'r': + return sf.SoundFile(filename) + elif rw == 'w': + return sf.SoundFile(filename, mode=rw, sample_rate=44100, channels=2) + elif rw == 'rw' and 'test_r' in filename: + return sf.SoundFile(filename, mode=rw) + elif rw == 'rw' and 'test_w' in filename: + return sf.SoundFile(filename, mode=rw, sample_rate=44100, channels=2) + + +def open_filehandle(filename, rw, _): + # TODO: does sf.SoundFile auto-close the handle??? + # request.addfinalizer(lambda: os.close(handle)) + if rw == 'r': + handle = os.open(filename, os.O_RDONLY) + elif rw == 'w': + handle = os.open(filename, os.O_CREAT | os.O_WRONLY) + elif rw == 'rw' and filename == file_r: + handle = os.open(filename, os.O_RDWR) + elif rw == 'rw' and filename == file_w: + handle = os.open(filename, os.O_CREAT | os.O_RDWR) + if 'test_r' in filename: + return sf.SoundFile(handle, mode=rw) + elif 'test_w' in filename: + return sf.SoundFile(handle, mode=rw, sample_rate=44100, + channels=2, format='wav') + + +def open_bytestream(filename, rw, request): + if rw == 'r': + bytesio = open(filename, 'rb') + elif rw == 'w': + bytesio = open(filename, 'wb') + elif rw == 'rw' and filename == file_r: + bytesio = open(filename, 'a+b') + elif rw == 'rw' and filename == file_w: + bytesio = open(filename, 'w+b') + if 'test_r' in filename: + file = sf.SoundFile(bytesio, mode=rw) + elif 'test_w' in filename: + file = sf.SoundFile(bytesio, mode=rw, sample_rate=44100, + channels=2, format='wav') + request.addfinalizer(bytesio.close) + return file + + +@pytest.fixture(params=[('r', open_filename), + ('r', open_filehandle), + ('r', open_bytestream)]) +def wavefile_r(request): + rw, open_func = request.param + file = open_func(file_r, rw, request) + request.addfinalizer(file.close) + return file + + +@pytest.fixture(params=[('r', open_filename), + ('r', open_filehandle), + ('r', open_bytestream)]) +def wavefile_r_mono(request): + rw, open_func = request.param + file = open_func(file_r_mono, rw, request) + request.addfinalizer(file.close) + return file + + +@pytest.fixture(params=[('w', open_filename), + ('w', open_filehandle), + ('w', open_bytestream)]) +def wavefile_w(request): + rw, open_func = request.param + file = open_func(file_w, rw, request) + request.addfinalizer(file.close) + request.addfinalizer(lambda: os.remove(file_w)) + return file + + +@pytest.fixture(params=[('rw', open_filename), + ('rw', open_filehandle), + # rw is not permissable with bytestreams + ]) +def wavefile_rw_existing(request): + rw, open_func = request.param + file = open_func(file_r, rw, request) + request.addfinalizer(file.close) + return file + + +@pytest.fixture(params=[('rw', open_filename), + ('rw', open_filehandle), + # rw is not permissable with bytestreams + ]) +def wavefile_rw_new(request): + rw, open_func = request.param + file = open_func(file_w, rw, request) + request.addfinalizer(file.close) + request.addfinalizer(lambda: os.remove(file_w)) + return file + + +@pytest.fixture(params=[('r', open_filename), + ('w', open_filename), + ('rw', open_filename), + ('r', open_filehandle), + ('w', open_filehandle), + ('rw', open_filehandle), + ('r', open_bytestream), + ('w', open_bytestream), + # rw is not permissable with bytestreams + ]) +def wavefile_all(request): + rw, open_func = request.param + if 'r' in rw: + file = open_func(file_r, rw, request) + elif rw == 'w': + file = open_func(file_w, rw, request) + request.addfinalizer(file.close) + if rw == 'w': + request.addfinalizer(lambda: os.remove(file_w)) + return file + + +# ----------------------------------------------------------------------------- +# Test file metadata +# ----------------------------------------------------------------------------- + + +def test_file_content(wavefile_r): + assert allclose(data_r, wavefile_r[:]) + + +def test_mode_should_be_in_read_mode(wavefile_r): + assert wavefile_r.mode == 'r' + + +def test_mode_should_be_in_write_mode(wavefile_w): + assert wavefile_w.mode == 'w' + + +def test_mode_should_be_in_readwrite_mode(wavefile_rw_existing): + assert wavefile_rw_existing.mode == 'rw' + + +def test_mode_read_should_start_at_beginning(wavefile_r): + assert wavefile_r.seek(0, sf.SEEK_CUR) == 0 + + +def test_mode_write_should_start_at_beginning(wavefile_w): + assert wavefile_w.seek(0, sf.SEEK_CUR) == 0 + + +def test_mode_rw_should_start_at_end(wavefile_rw_existing): + assert wavefile_rw_existing.seek(0, sf.SEEK_CUR) == 5 + + +def test_number_of_channels(wavefile_all): + assert wavefile_all.channels == 2 + + +def test_sample_rate(wavefile_all): + assert wavefile_all.sample_rate == 44100 + + +def test_format_metadata(wavefile_all): + assert wavefile_all.format == 'WAV' + assert wavefile_all.subtype == 'PCM_16' + assert wavefile_all.endian == 'FILE' + assert wavefile_all.format_info == 'WAV (Microsoft)' + assert wavefile_all.subtype_info == 'Signed 16 bit PCM' + + +def test_data_length_r(wavefile_r): + assert len(wavefile_r) == len(data_r) + + +def test_data_length_w(wavefile_w): + assert len(wavefile_w) == 0 + + +def test_file_exists(wavefile_w): + assert os.path.isfile(file_w) + + +# ----------------------------------------------------------------------------- +# Test seek +# ----------------------------------------------------------------------------- + + +def test_seek_should_advance_read_pointer(wavefile_r): + assert wavefile_r.seek(2) == 2 + + +def test_seek_multiple_times_should_advance_read_pointer(wavefile_r): + wavefile_r.seek(2) + assert wavefile_r.seek(2, whence=sf.SEEK_CUR) == 4 + + +def test_seek_to_end_should_advance_read_pointer_to_end(wavefile_r): + assert wavefile_r.seek(-2, whence=sf.SEEK_END) == 3 + + +def test_seek_read_pointer_should_advance_read_pointer(wavefile_r): + assert wavefile_r.seek(2, which='r') == 2 + + +def test_seek_write_pointer_should_advance_write_pointer(wavefile_w): + assert wavefile_w.seek(2, which='w') == 2 + + +# ----------------------------------------------------------------------------- +# Test read +# ----------------------------------------------------------------------------- + + +def test_read_write_only(wavefile_w): + with pytest.raises(RuntimeError): + wavefile_w.read(2) + + +def test_read_should_read_data_and_advance_read_pointer(wavefile_r): + data = wavefile_r.read(2) + assert allclose(data, data_r[:2]) + assert wavefile_r.seek(0, sf.SEEK_CUR) == 2 + + +def test_read_should_read_float64_data(wavefile_r): + assert wavefile_r[:].dtype == np.float64 + + +def test_read_int16_should_read_int16_data(wavefile_r): + assert wavefile_r.read(2, dtype='int16').dtype == np.int16 + + +def test_read_int32_should_read_int32_data(wavefile_r): + assert wavefile_r.read(2, dtype='int32').dtype == np.int32 + + +def test_read_float32_should_read_float32_data(wavefile_r): + assert wavefile_r.read(2, dtype='float32').dtype == np.float32 + + +def test_read_by_indexing_should_read_but_not_advance_read_pointer(wavefile_r): + assert allclose(wavefile_r[:2], data_r[:2]) + assert wavefile_r.seek(0, sf.SEEK_CUR) == 0 + + +def test_read_n_frames_should_return_n_frames(wavefile_r): + assert len(wavefile_r.read(2)) == 2 + + +def test_read_all_frames_should_read_all_remaining_frames(wavefile_r): + wavefile_r.seek(-2, sf.SEEK_END) + assert allclose(wavefile_r.read(), data_r[-2:]) + + +def test_read_over_end_should_return_only_remaining_frames(wavefile_r): + wavefile_r.seek(-2, sf.SEEK_END) + assert allclose(wavefile_r.read(4), data_r[-2:]) + + +def test_read_over_end_with_fill_should_reaturn_asked_frames(wavefile_r): + wavefile_r.seek(-2, sf.SEEK_END) + data = wavefile_r.read(4, fill_value=0) + assert allclose(data[:2], data_r[-2:]) + assert np.all(data[2:] == 0) + assert len(data) == 4 + + +def test_read_into_out_should_return_data_and_write_into_out(wavefile_r): + out = np.empty((2, wavefile_r.channels), dtype='float64') + data = wavefile_r.read(out=out) + assert np.all(data == out) + + +def test_read_into_malformed_out_should_fail(wavefile_r): + out = np.empty((2, wavefile_r.channels+1), dtype='float64') + with pytest.raises(ValueError): + wavefile_r.read(out=out) + + +def test_read_into_out_with_too_many_dimensions_should_fail(wavefile_r): + out = np.empty((2, wavefile_r.channels, 1), dtype='float64') + with pytest.raises(ValueError): + wavefile_r.read(out=out) + + +def test_read_into_zero_len_out_should_not_read_anything(wavefile_r): + out = np.empty((0, wavefile_r.channels), dtype='float64') + data = wavefile_r.read(out=out) + assert len(data) == 0 + assert len(out) == 0 + assert wavefile_r.seek(0, sf.SEEK_CUR) == 0 + + +def test_read_into_out_over_end_should_return_shorter_data_and_write_into_out( + wavefile_r): + out = np.ones((4, wavefile_r.channels), dtype='float64') + wavefile_r.seek(-2, sf.SEEK_END) + data = wavefile_r.read(out=out) + assert np.all(data[:2] == out[:2]) + assert np.all(data[2:] == 1) + assert out.shape == (4, wavefile_r.channels) + assert data.shape == (2, wavefile_r.channels) + + +def test_read_into_out_over_end_with_fill_should_return_full_data_and_write_into_out( + wavefile_r): + out = np.ones((4, wavefile_r.channels), dtype='float64') + wavefile_r.seek(-2, sf.SEEK_END) + data = wavefile_r.read(out=out, fill_value=0) + assert np.all(data == out) + assert np.all(data[2:] == 0) + assert out.shape == (4, wavefile_r.channels) + + +# ----------------------------------------------------------------------------- +# Test write +# ----------------------------------------------------------------------------- + + +def test_write_to_read_only_file_should_fail(wavefile_r): + with pytest.raises(RuntimeError): + wavefile_r.write(data_r) + + +def test_write_should_write_and_advance_write_pointer(wavefile_w): + position_w = wavefile_w.seek(0, sf.SEEK_CUR, which='w') + position_r = wavefile_w.seek(0, sf.SEEK_CUR, which='r') + wavefile_w.write(data_r) + assert wavefile_w.seek(0, sf.SEEK_CUR, which='w') == position_w+len(data_r) + assert wavefile_w.seek(0, sf.SEEK_CUR, which='r') == position_r + + +def test_write_flush_should_write_to_disk(wavefile_w): + wavefile_w.flush() + size = os.path.getsize(file_w) + wavefile_w.write(data_r) + wavefile_w.flush() + assert os.path.getsize(file_w) == size + data_r.size*2 # 16 bit integer + + +# ----------------------------------------------------------------------------- +# Test read/write +# ----------------------------------------------------------------------------- + + +def test_rw_initial_read_and_write_pointer(wavefile_rw_existing): + assert wavefile_rw_existing.seek(0, sf.SEEK_CUR, which='w') == 5 + assert wavefile_rw_existing.seek(0, sf.SEEK_CUR, which='r') == 0 + + +def test_rw_seek_write_should_advance_write_pointer(wavefile_rw_existing): + assert wavefile_rw_existing.seek(2, which='w') == 2 + assert wavefile_rw_existing.seek(0, sf.SEEK_CUR, which='r') == 0 + + +def test_rw_seek_read_should_advance_read_pointer(wavefile_rw_existing): + assert wavefile_rw_existing.seek(2, which='r') == 2 + assert wavefile_rw_existing.seek(0, sf.SEEK_CUR, which='w') == 5 + + +def test_rw_writing_float_should_be_written_approximately_correct( + wavefile_rw_new): + data = np.ones((5, 2), dtype='float64') + wavefile_rw_new.seek(0, which='w') + wavefile_rw_new.write(data) + written_data = wavefile_rw_new[-len(data):] + assert allclose(data, written_data) + + +def test_rw_writing_int_should_be_written_exactly_correct(wavefile_rw_new): + data = np.zeros((5, 2)) + 2**15 - 1 # full scale int16 + wavefile_rw_new.seek(0, which='w') + wavefile_rw_new.write(np.array(data, dtype='int16')) + written_data = wavefile_rw_new.read(dtype='int16') + assert np.all(data == written_data) + + +def test_rw_writing_using_indexing_should_write_but_not_advance_write_pointer( + wavefile_rw_new): + data = np.ones((5, 2)) + # grow file to make room for indexing + wavefile_rw_new.write(np.zeros((5, 2))) + position = wavefile_rw_new.seek(0, sf.SEEK_CUR, which='w') + wavefile_rw_new[:len(data)] = data + written_data = wavefile_rw_new[:len(data)] + assert allclose(data, written_data) + assert position == wavefile_rw_new.seek(0, sf.SEEK_CUR, which='w') + + +# ----------------------------------------------------------------------------- +# Other tests +# ----------------------------------------------------------------------------- + + +def test_context_manager_should_open_and_close_file(): + with open_filename(file_r, 'r', None) as f: + assert not f.closed + assert f.closed + + +def test_closing_should_close_file(): + f = open_filename(file_r, 'r', None) + assert not f.closed + f.close() + assert f.closed + + +def test_file_attributes_should_save_to_disk(): + with open_filename(file_w, 'w', None) as f: + f.title = 'testing' + with open_filename(file_w, 'r', None) as f: + assert f.title == 'testing' + os.remove(file_w) + + +def test_non_file_attributes_should_not_save_to_disk(): + with open_filename(file_w, 'w', None) as f: + f.foobar = 'testing' + with open_filename(file_w, 'r', None) as f: + with pytest.raises(AttributeError): + f.foobar + os.remove(file_w) + + +def test_read_mono_without_always2d_should_read_array(wavefile_r_mono): + out_data = wavefile_r_mono.read(always_2d=False) + assert allclose(out_data, data_r_mono) + assert out_data.ndim == 1 + + +def test_read_mono_should_read_matrix(wavefile_r_mono): + out_data = wavefile_r_mono.read() + assert allclose(out_data, [[x] for x in data_r_mono]) + assert out_data.ndim == 2 + + +def test_read_mono_into_mono_out_should_read_into_out(wavefile_r_mono): + data = np.empty(5, dtype='float64') + out_data = wavefile_r_mono.read(out=data) + assert np.all(data == out_data) + assert id(data) == id(out_data) + + +def test_read_mono_into_out_should_read_into_out(wavefile_r_mono): + data = np.empty((5, 1), dtype='float64') + out_data = wavefile_r_mono.read(out=data) + assert np.all(data == out_data) + assert id(data) == id(out_data) + + +# ----------------------------------------------------------------------------- +# RAW tests +# ----------------------------------------------------------------------------- + + +def test_read_raw_files_should_read_data(): + with sf.SoundFile(file_r_raw, sample_rate=44100, + channels=2, subtype='PCM_16') as f: + assert allclose(f.read(), data_r_raw) + + +def test_read_raw_files_with_too_few_arguments_should_fail(): + with pytest.raises(TypeError): # missing everything + sf.SoundFile(file_r_raw) + with pytest.raises(TypeError): # missing subtype + sf.SoundFile(file_r_raw, sample_rate=44100, channels=2) + with pytest.raises(TypeError): # missing channels + sf.SoundFile(file_r_raw, sample_rate=44100, subtype='PCM_16') + with pytest.raises(TypeError): # missing sample_rate + sf.SoundFile(file_r_raw, channels=2, subtype='PCM_16') diff --git a/tests/test_r.raw b/tests/test_r.raw new file mode 100644 index 0000000..d33622a --- /dev/null +++ b/tests/test_r.raw @@ -0,0 +1 @@ +ÿ€ffš™ÌL4³33ÍÌ™gæ \ No newline at end of file diff --git a/tests/test_r.wav b/tests/test_r.wav new file mode 100644 index 0000000..99ad77c Binary files /dev/null and b/tests/test_r.wav differ diff --git a/tests/test_r_mono.wav b/tests/test_r_mono.wav new file mode 100644 index 0000000..c7e90c1 Binary files /dev/null and b/tests/test_r_mono.wav differ