A
Alf P. Steinbach
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 = 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.
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
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.