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.



More information about the Python-list mailing list