A simple-to-use sound file writer

Alf P. Steinbach alfps at start.no
Thu Jan 14 06:09:33 CET 2010

```Just as a contribution, since someone hinted that I haven't really contributed
much to the Python community.

The [simple_sound] code will probably go into my ch 3 at <url:
http://tinyurl.com/programmingbookP3>, but sans sine wave generation since I
haven't yet discussed trig functions, and maybe /with/ changes suggested by you?

Module:

<code file="simple_sound.py">
"Lets you generate simple mono (single-channel) [.wav], [.aiff] or [.aifc] files."
import collections
import array
import math

DataFormat              = collections.namedtuple( "DataFormat",
"open_func, append_int16_func"
)

default_sample_rate     = 44100             # Usual CD quality.

def sample_square( freq, t ):
linear = freq*t % 1.0
if linear < 0.5:
return -1.0
else:
return 1.0

def sample_sawtooth( freq, t ):
linear = freq*t % 1.0
if linear < 0.5:
return 4.0*linear - 1.0
else:
return 3.0 - 4.0*linear

def sample_sine( freq, t ):
return math.sin( 2*math.pi*freq*t )

def _append_as_big_endian_int16_to( a, i ):
if i < 0:
i = i + 65536
assert( 0 <= i < 65536 )
a.append( i // 256 )
a.append( i % 256 )

def _append_as_little_endian_int16_to( a, i ):
if i < 0:
i = i + 65536
assert( 0 <= i < 65536 )
a.append( i % 256 )
a.append( i // 256 )

def aiff_format():
import aifc
return DataFormat( aifc.open, _append_as_big_endian_int16_to )

def wav_format():
import wave
return DataFormat( wave.open, _append_as_little_endian_int16_to )

class Writer:
"Writes normalized samples to a specified file or file-like object"
def __init__( self, filename, sample_rate = default_sample_rate,
data_format = aiff_format() ):
self._sample_rate = sample_rate
self._append_int16_func = data_format.append_int16_func
self._writer = data_format.open_func( filename, "w" )
self._writer.setnchannels( 1 )
self._writer.setsampwidth( 2 )          # 2 bytes = 16 bits
self._writer.setframerate( sample_rate )
self._samples = []

def sample_rate( self ):
return self._sample_rate

def write( self, normalized_sample ):
assert( -1 <= normalized_sample <= +1 )
self._samples.append( normalized_sample )

def close( self ):
data = array.array( "B" )               # B -> unsigned bytes.
append_int16_to = self._append_int16_func
for sample in self._samples:
level = round( 32767*sample )
append_int16_to( data, level )
self._writer.setnframes( len( self._samples ) )
self._writer.writeframes( data )
self._writer.close()
</code>

By the way, the reason that it holds on to data until 'close' and does the
writing there is to work around a bug in [wave.py]. That bug's now corrected but
wasn't when I wrote above. And possibly best to keep it like it is?

Ideally should deal with exceptions in 'close', calling close on the _writer,
but I haven't yet discussed exceptions in the hopefully-to-be book writings
where this probably will go.

Example usage, illustrating that it's simple to use (?):

<code file="aiff.py">
import simple_sound

sample_rate = simple_sound.default_sample_rate
total_time  = 2
n_samples   = sample_rate*total_time

writer = simple_sound.Writer( "ringtone.aiff" )
for i in range( n_samples ):
t = i/sample_rate
samples = (
simple_sound.sample_sine( 440, t ),
simple_sound.sample_sine( (5/4)*440, t ),
)
sample = sum( samples )/len( samples )
writer.write( sample )
writer.close()
</code>

Utility class that may be used to capture output (an instance of this or any
other file like class can be passed as "filename" to simple_sound.Writer):

<code>
class BytesCollector:
def __init__( self ):
self._bytes = array.array( "B" )
self._pos = 0

def raw_bytes( self ):
return self._bytes

def bytes_string( self ):
return self._bytes.tostring()

# File methods:

def tell( self ):
return self._pos

def seek( self, pos, anchor = 0 ):
assert( anchor == 0 )   # Others not supported
assert( pos <= len( self._bytes ) )
self._pos = pos

def write( self, bytes ):
pos = self._pos
if pos < len( self._bytes ):
s = slice( pos, pos + len( bytes ) )
self._bytes[s] = bytes
self._pos = s.stop
else:
self._bytes.extend( bytes )
self._pos = len( self._bytes )

def flush( self ):
pass

def close( self ):
pass
</code>

Cheers & enjoy,

- Alf

PS: Comments welcome, except the BytesCollector which I just hacked together to
test something, it may contain eroRs but worked for my purpose.

```