Sunday, February 21, 2010

How to make a .wav file with Python, revisited

I've noticed that my two-year old post about making a .wav file with Python is being read a lot so I forced myself to modify the code a little. Now it should be nicer to read and modify further according to your needs/wishes. I've also incorporated the comments of helpful readers Mike Axiak and Fingon. Thanks guys ;-)
The link to the source of this idea, code by Andrea Valle, does not work as I'm typing this so I can only thank him by saying his name. My contribution to all this is minor.

import numpy as N
import wave

def get_signal_data(frequency=440, duration=1, volume=32768, samplerate=44100):
"""Outputs a numpy array of intensities"""
samples = duration * samplerate
period = samplerate / float(frequency)
omega = N.pi * 2 / period
t = N.arange(samples, dtype=N.float)
y = volume * N.sin(t * omega)
return y

def numpy2string(y):
"""Expects a numpy vector of numbers, outputs a string"""
signal = "".join((wave.struct.pack('h', item) for item in y))
# this formats data for wave library, 'h' means data are formatted
# as short ints
return signal

class SoundFile:
def __init__(self, signal, filename, duration=1, samplerate=44100):
self.file = wave.open(filename, 'wb')
self.signal = signal
self.sr = samplerate
self.duration = duration

def write(self):
self.file.setparams((1, 2, self.sr, self.sr*self.duration, 'NONE', 'noncompressed'))
# setparams takes a tuple of:
# nchannels, sampwidth, framerate, nframes, comptype, compname
self.file.writeframes(self.signal)
self.file.close()

if __name__ == '__main__':
duration = 2
myfilename = 'test.wav'
data = get_signal_data(440, duration)
signal = numpy2string(data)
f = SoundFile(signal, myfilename, duration)
f.write()
print 'file written'

8 comments:

Jason F said...

Thanks for the post, this got me started on writing wav files of my simulated analog synthesizer output. I found that you can write integer NumPy arrays directly to wav files using scipy.io.wavefile.write:

http://www.scipy.org/doc/api_docs/SciPy.io.wavfile.html

RVJ said...

could you please write a code segment for reading a .wav file and to perform FFT on it using scipylab

Helton Moraes said...

Thanks very much for posting an update, doing so two years later is a true act of generosity of yours. I want to do some tests with siren sounds, and your script will be very helpful, so as the tip from Jason F.

Anonymous said...

FYI, I had a range problem with the sample values after changing the sampling frequency to 96k. The sample values can include 32768.0, which a signed short cannot handle.

This is a band aid fix:

# The output range includes 32768 which cannot fit int16_t.
# Clip it to 32767.
samples = map( lambda item: 32767 if item >= 32768 else item, samples )

Anonymous said...

So I'm working on a linux machine with and 32bit version of python and I keep getting the error "'module' object has no attribute 'struct'".
I have also run the code on a machine with the 64bit version and it works fine. Any thoughts on what is going on and/or how to fix it?

Keith said...

Anonymous: just use the struct module itself, not the wave.struct module (it's the same thing).

RL, to do the pack, I think I get better performance if I do this instead of the string join approach:

x = N.arange(samplecount, dtype = N.float) * omega
signal = (32767 * amplitude) * N.sin(x)

fmtstr = 'h' * samplecount
signalbuffer = struct.pack(fmtstr,*tuple(signal))

Keith said...

Thanks for the starting point help!

In the spirit of community, here's the program I wrote to let me do what I needed to do, using the OP as a starting point:

import numpy as N
import wave
import struct

def MakeSineWaveBuffer(frequency = [440], amplitude = [.5], duration = 5, Fs = 44100):
samplecount = duration * Fs

# create the signals
signals = []
for f, a in zip(frequency, amplitude):
period = Fs / float(f) # in sample points
omega = N.pi * 2 / period

x = N.arange(samplecount, dtype = N.float) * omega
signal = (a) * N.sin(x)
signals.append(signal)

# add them up
signal = signals[0]
for s in signals[1:]:
signal += s

# see if we are too loud
m = N.max(signal)
if m > 1.0:
signal /= m

# convert to 16-bit audio numerical space
signal *= 32767

fmtstr = 'h' * samplecount
signalbuffer = struct.pack(fmtstr,*tuple(signal))

return signalbuffer

class SoundFile(object):
def __init__(self, signal, Fs = 44100):
self.signal = signal
self.Fs = Fs

def write(self, fname):
numsamples = len(self.signal) / 2
fp = wave.open(fname, "wb")
fp.setparams((1, 2, self.Fs, numsamples, 'NONE', 'noncompressed'))
fp.writeframes(self.signal)
fp.close()


def ExtractSignalFromBuffer(buffer):
samplecount = len(buffer) / 2
fmtstr = 'h' * samplecount
return struct.unpack(fmtstr, buffer)

if __name__ == "__main__":
sig = MakeSineWaveBuffer(frequency = [100, 200,300,400], amplitude = [.3, .5, .7, .9], duration = 2, Fs = 44100)
sf = SoundFile(sig)
sf.write("test.wav")
reclaimedsig = ExtractSignalFromBuffer(sig)

import pylab

pylab.subplot(211)
pylab.plot(reclaimedsig)
pylab.grid()
pylab.subplot(212)
fftmag = (N.abs(N.fft.rfft(reclaimedsig)) ** 2)
m = N.max(fftmag)
fftmag /= m
fftfreq = N.fft.fftfreq(len(reclaimedsig)/2+1)
print len(fftmag), len(fftfreq)
pylab.plot(fftfreq, fftmag)
pylab.grid()
pylab.show()

Anonymous said...

your indents are gone in your comment.

anyway, on all these python 2.7 scripts, i'm getting this error on python 3.5:

struct.error: required argument is not an integer