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.
--
http://mail.python.org/mailman/listinfo/python-list

Reply via email to