A simple-to-use sound file writer
Alf P. Steinbach
alfps at start.no
Sun Jan 17 10:14:07 EST 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