A simple-to-use sound file writer

Alf P. Steinbach alfps at start.no
Sun Jan 17 16:14:07 CET 2010


* Alf P. Steinbach:
> Just as a contribution, ...

The original code I posted was only written for Python 3.1.1 (because the code 
was for my writings which assumes 3.x). In the simple_sound module this caused a 
deprecation warning with 2.x. And the example program didn't work with 2.x.

I've now installed CPython 2.6.4 and fixed the code so that it works nicely also 
with that version of Python.


<code file="simple_sound.py">
"Generate simple mono (single-channel) [.wav], [.aiff] or [.aifc] files."

# Works with Python 2.6.4 and Python 3.1.1, but has not been extensively tested.
# Author: Alf P. Steinbach.
#
# Changes from original 3.1.1 version:
# * A deprecation warning suppressed by explicit cast to int.
# * The default sound library is now not imported until it's actually used.
# * Added exception handling (for the code's original purpose I couldn't).
#
# Notes:
# (1) It might be possible to optimize this by using array of 16-bit integers, then
#     checking 'sys.byteorder' and doing a 'data.byteswap()' call as appropriate.
# (2) Data is kept in memory until 'close' due to a bug in the 'wave' module. 
That bug
#     has now been fixed. But it may be present in any Python installation.

import collections
import array
import math

default_sample_rate     = 44100             # Usual CD quality.

def sample_sawtooth( freq, t ):
     linear = freq*t % 1.0
     return 2*linear - 1.0

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

def sample_triangle( 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 )

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

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

def _append_as_little_endian_int16_to( bytes_array, i ):
     if i < 0:
         i = i + 65536
     assert( 0 <= i < 65536 )
     bytes_array.append( i % 256 )
     bytes_array.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     = None
         ):

         if data_format is None:
             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 ):
         try:
             data = array.array( "B" )               # B -> unsigned bytes.
             append_int16_to = self._append_int16_func
             for sample in self._samples:
                 level = int( round( 32767*sample ) )
                 append_int16_to( data, level )
             self._writer.setnframes( len( self._samples ) )
             self._writer.writeframes( data )
         finally:
             self._writer.close()

def example( filename = "ringtone.aiff" ):
     global default_sample_rate

     sample_rate = default_sample_rate
     total_time  = 2
     n_samples   = sample_rate*total_time

     writer = Writer( filename )
     for i in range( n_samples ):
         t = 1.0*i/sample_rate
         samples = (
             sample_sine( 440, t ),
             sample_sine( (5.0/4.0)*440, t ),
             )
         sample = sum( samples )/len( samples )
         writer.write( sample )
     writer.close()

if __name__ == "__main__":
     example()
</code>


Cheers, & enjoy!,

- Alf



More information about the Python-list mailing list