Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 74 additions & 49 deletions pysoundfile.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import os

from cffi import FFI
import numpy as np
from cffi import FFI
from os import SEEK_SET, SEEK_CUR, SEEK_END

"""PySoundFile is an audio library based on libsndfile, CFFI and Numpy

Expand Down Expand Up @@ -118,9 +117,11 @@

""")

read_mode = 0x10
write_mode = 0x20
read_write_mode = 0x30
_open_modes = {
0x10: 'READ',
0x20: 'WRITE',
0x30: 'RDWR'
}

snd_types = {
'WAV': 0x010000, # Microsoft WAV format (little endian default).
Expand Down Expand Up @@ -207,6 +208,18 @@ def reverse_dict(d): return {value:key for key, value in d.items()}

return (type, subtype, endianness)


class _ModeType(int):
def __repr__(self):
return _open_modes.get(self, int.__repr__(self))


def _add_constants_to_module_namespace(constants_dict, constants_type):
for k, v in constants_dict.items():
globals()[v] = constants_type(k)

_add_constants_to_module_namespace(_open_modes, _ModeType)

_snd = ffi.dlopen('sndfile')


Expand All @@ -216,15 +229,14 @@ class SoundFile(object):

Each SoundFile opens one sound file on the disk. This sound file
has a specific samplerate, data format and a set number of
channels. Each sound file can be opened in read_mode, write_mode,
or read_write_mode. Note that read_write_mode is unsupported for
some formats.
channels. Each sound file can be opened with one of the modes
READ/WRITE/RDWR. Note that RDWR is unsupported for some formats.

Data can be written to the file using write(), or read from the
file using read(). Every read and write operation starts at a
certain position in the file. Reading N frames will change this
position by N frames as well. Alternatively, seek() and
seek_absolute() can be used to set the current position to a frame
position by N frames as well. Alternatively, seek()
can be used to set the current position to a frame
index offset from the current position, the start of the file, or
the end of the file, respectively.

Expand All @@ -246,17 +258,21 @@ class SoundFile(object):

"""

def __init__(self, name, sample_rate=0, channels=0, format=0,
mode=read_mode, virtual_io=False):
def __init__(self, name, mode=READ, sample_rate=0, channels=0, format=0,
virtual_io=False):
"""Open a new SoundFile.

If a file is only opened in read_mode or in read_write_mode,
If a file is opened with mode READ or WRITE,
no sample_rate, channels or file format need to be given. If a
file is opened in write_mode, you must provide a sample_rate,
file is opened with mode RDWR, you must provide a sample_rate,
a number of channels, and a file format. An exception is the
RAW data format, which requires these data points for reading
as well.

Instead of the library constants READ/WRITE/RDWR you can also
use the (case-insensitive) strings 'r'/'w'/'rw' or
'READ'/'WRITE'/'RDWR'.

File formats consist of three parts:
- one of the file types from snd_types
- one of the data types from snd_subtypes
Expand All @@ -276,7 +292,17 @@ def __init__(self, name, sample_rate=0, channels=0, format=0,
if hasattr(format, '__getitem__'):
format = _encodeformat(format)
info.format = format
self._file_mode = mode

if isinstance(mode, str):
try:
mode = {'read': READ, 'r': READ,
'write': WRITE, 'w': WRITE,
'rdwr': RDWR, 'rw': RDWR}[mode.lower()]
except KeyError:
pass
if not isinstance(mode, _ModeType):
raise ValueError("Invalid mode: %s" % repr(mode))
self.mode = mode

if virtual_io:
fObj = name
Expand All @@ -287,11 +313,10 @@ def __init__(self, name, sample_rate=0, channels=0, format=0,
self._vio = self._init_vio(fObj)
vio = ffi.new("SF_VIRTUAL_IO*", self._vio)
self._vio['vio_cdata'] = vio
self._file = _snd.sf_open_virtual(vio, self._file_mode, info,
ffi.NULL)
self._file = _snd.sf_open_virtual(vio, mode, info, ffi.NULL)
else:
filename = ffi.new('char[]', name.encode())
self._file = _snd.sf_open(filename, self._file_mode, info)
self._file = _snd.sf_open(filename, mode, info)

self._handle_error()

Expand All @@ -311,9 +336,9 @@ def vio_get_filelen(user_data):
size = fObj._length
elif not hasattr(fObj, '__len__'):
old_file_position = fObj.tell()
fObj.seek(0, os.SEEK_END)
fObj.seek(0, SEEK_END)
size = fObj.tell()
fObj.seek(old_file_position, os.SEEK_SET)
fObj.seek(old_file_position, SEEK_SET)
else:
size = len(fObj)
return size
Expand Down Expand Up @@ -394,7 +419,7 @@ def _handle_error_number(self, err):
def __setattr__(self, name, value):
# access text data in the sound file through properties
if name in self._snd_strings:
if self._file_mode == read_mode:
if self.mode == READ:
raise RuntimeError("Can not change %s of file in read mode" %
name)
data = ffi.new('char[]', value.encode())
Expand Down Expand Up @@ -438,10 +463,10 @@ def __getitem__(self, frame):
"SoundFile can only be accessed in one or two dimensions")
frame, second_frame = frame
start, stop = self._get_slice_bounds(frame)
curr = self.seek(0)
self.seek_absolute(start)
curr = self.seek(0, SEEK_CUR | READ)
self.seek(start, SEEK_SET | READ)
data = self.read(stop - start)
self.seek_absolute(curr)
self.seek(curr, SEEK_SET | READ)
if second_frame:
return data[(slice(None), second_frame)]
else:
Expand All @@ -451,45 +476,43 @@ def __setitem__(self, frame, data):
# access the file as if it where a one-dimensional Numpy
# array. Data must be in the form (frames x channels).
# Both open slice bounds and negative values are allowed.
if self._file_mode == read_mode:
raise RuntimeError("Can not write to read-only file")
if self.mode == READ:
raise RuntimeError("Cannot write to file opened in READ mode!")
start, stop = self._get_slice_bounds(frame)
if stop - start != len(data):
raise IndexError(
"Could not fit data of length %i into slice of length %i" %
(len(data), stop - start))
curr = self.seek(0)
self.seek_absolute(start)
curr = self.seek(0, SEEK_CUR | WRITE)
self.seek(start, SEEK_SET | WRITE)
self.write(data)
self.seek_absolute(curr)
self.seek(curr, SEEK_SET | WRITE)
return data

def flush(self):
"""Write unwritten data to disk."""
_snd.sf_write_sync(self._file)

def seek(self, frames):
"""Set the read position relative to the current position.
def seek(self, frames, whence=SEEK_SET):
"""Set the read and/or write position.

Positive values will fast-forward. Negative values will rewind.
By default (whence=SEEK_SET), frames are counted from the
beginning of the file. SEEK_CUR seeks from the current position
(positive and negative values are allowed).
SEEK_END seeks from the end (use negative values).

Returns the new absolute read position in frames.
"""
return _snd.sf_seek(self._file, frames, os.SEEK_CUR)

def seek_absolute(self, frames):
"""Set an absolute read position.
In RDWR mode, the whence argument can be combined (using
logical or) with READ or WRITE in order to set only the read
or write position, respectively (e.g. SEEK_SET | WRITE).

Positive values will set the read position to the given frame
index. Negative values will set the read position to the given
index counted from the end of the file.
To set the read/write position to the beginning of the file,
use seek(0), to set it to right after the last frame,
e.g. for appending new data, use seek(0, SEEK_END).

Returns the new absolute read position in frames.
Returns the new absolute read position in frames or a negative
value on error.
"""
if frames >= 0:
return _snd.sf_seek(self._file, frames, os.SEEK_SET)
else:
return _snd.sf_seek(self._file, frames, os.SEEK_END)
return _snd.sf_seek(self._file, frames, whence)

def read(self, frames=-1, format=np.float32):
"""Read a number of frames from the file.
Expand All @@ -505,6 +528,8 @@ def read(self, frames=-1, format=np.float32):
smaller NumPy array will be returned.

"""
if self.mode == WRITE:
raise RuntimeError("Cannot read from file opened in WRITE mode!")
formats = {
np.float64: 'double[]',
np.float32: 'float[]',
Expand Down Expand Up @@ -540,8 +565,8 @@ def write(self, data):
array.

"""
if self._file_mode == read_mode:
raise RuntimeError("Can not write to read-only file")
if self.mode == READ:
raise RuntimeError("Cannot write to file opened in READ mode!")
formats = {
np.dtype(np.float64): 'double*',
np.dtype(np.float32): 'float*',
Expand Down