Friday, July 4, 2008

How to make a simple .wav file with Python

EDIT: I've edited this code a little and incorporated reader's comments, here's the new version

I've recently had an idea to test some vibration-resonance problems by playing a set of sounds and detecting the vibrations in question. The glitch is, how to create a sound of desired frequency? I believe there is a lot of ways to solve this but having only a hammer (read Python) as my tool it looked like a nail ;-)

The idea is then to create a simple sine wave and play it back with a computer. My first try was with pygame module but the sound quality was poor and unsatisfactory.
If not playing sound directly one can write the wave to the file and then play the file with whatever means are suitable. This turned out to be the better way after all.
A search of the internet turned up this discussion of creating .wav files with Python. I've adopted the "minimal example" from there (written by Andrea Valle) to be usable with modern modules (it used obsolete Numeric and RandomArray modules). The code is not neat but it works well.

import numpy as N
import wave

class SoundFile:
def __init__(self, signal):
self.file = wave.open('test.wav', 'wb')
self.signal = signal
self.sr = 44100

def write(self):
self.file.setparams((1, 2, self.sr, 44100*4, 'NONE', 'noncompressed'))
self.file.writeframes(self.signal)
self.file.close()

# let's prepare signal
duration = 4 # seconds
samplerate = 44100 # Hz
samples = duration*samplerate
frequency = 440 # Hz
period = samplerate / float(frequency) # in sample points
omega = N.pi * 2 / period

xaxis = N.arange(int(period),dtype = N.float) * omega
ydata = 16384 * N.sin(xaxis)

signal = N.resize(ydata, (samples,))

ssignal = ''
for i in range(len(signal)):
ssignal += wave.struct.pack('h',signal[i]) # transform to binary

f = SoundFile(ssignal)
f.write()
print 'file written'
This code should work as intended and produce .wav files of given length and frequency. A few comments:
a) if you need different sampling frequencies, just replace all 44100 numbers with desired sampling frequency;
b) the setparams command is relatively well described in wave doc in Python help, its fourth parameter (nframes) may probably be whatever number you want, the procedure is designed to write all data you sent to it;
c) the 16384 number seems to be a volume setting for .wav file, this value is near the maximum so only lowering the volume is possible from here;
d) wave.struct.pack('h', #) - this was tough to find but without this command the resulting file will not be correct (at least on Windows machines), I do not yet understand what it does...

22 comments:

Anonymous said...

Hi,
do you have also a python at home? It is a nice snake :-)

&y said...

I tried out your code, but I didn't get a 440Hz sine wave as I expected your code to produce. What I did get was an A but a couple octaves lower (110Hz), and with lots of harmonics.

Why do you think this might have happened?

R.L. said...

Hello Andrew,
I've checked the code and it seems to produce ~441 waves per second which should be correct. The problem than may be in the wave.struct.pack() command. If you happen to use other OS than Windows, try discarding the loop within which this command is (at the end of the script) and fix the signal/ssignal variable names.
I've had similar results like you without this command so maybe it is only required on Windows.
By the way, how do you check the resulting file i.e. learn about the harmonics and such? I've only used my ears to do so :-)
R.L.

Gregory said...

This is really neat. I made it into an online utility here so you can generate the files right from your browser.

Let me know what you think. Can you think of any improvements?

R.L. said...

Wow. Gregory that's great, I had no idea something like this was possible. Very nice.
There are of course a lot of ways to improve the code. It is probably very poorly written from a programmer's point of view. You could also have the user input the sampling frequency (which is hard-coded at this time). The generated frequency is slightly incorrect because of rounding error, this could also be fixed.
Oh, I have a question too, why is the output file with .xwav extension? I could not find any reference to this extension but renaming to .wav seems to work.

Greg said...

R.L. All the utilities on Utility Mill are modifiable (it's like a wiki for Python programs) so feel free to improve it.

I may have used the wrong Mime type, do you now what it should be for wav?

Nate said...
This comment has been removed by the author.
Mike Axiak said...
This comment has been removed by the author.
Mike Axiak said...

This is nice but your concatenation loop can probably be written better as ::

ssignal = ''.join((wave.struct.pack('h', item) for item in signal))

Since repetitive string concatenation can be slow.

-Mike

Fingon said...

I found this post very helpful, it really got me started with python wave generation. Thanks! :)

Some comments :
1) the 16384 is indeed a volume measure, but its maximum is 32768, 2^15, which is the maximum of an unsigned short. The 'h' has to do with this short format and with how a sound "works" : sound consists of 44100 samples per second, and every sample consists of 16 bit, which is a short. So every sample can take a value from about -2^15 to +2^15.

2) The N.resize is nice to speed things up but like you said already, it creates (very minor) artifacts due to the aliasing which occurs at the end of a period, especially for high frequencies (short periods). Besides, the speed is no concern. It is better to do it like this :
x = N.arange(samples, dtype = N.float)
y = volume * N.sin(x * omega)
This way there is no aliasing.

R.L. said...

Fingon, thak you very much for an insightful comment. I intended to rewrite this code into something more neat but I'm currently overwhelmed with work so it will have to wait.

Sadique said...

Thanks man! That was of great help.

Rachat de credit said...

Thank you it has been a good help, now to make a simple .wav file with python is simple with the help of your tips. Thank you

Mutuelle sante said...

Thank you so much it has been a good guide, now to make a simple .wav file with python is definitely simple with the help of your information. Kudos

Dave said...

Rather than using struct.pack, you can use the numpy tostring() method:

ssignal = signal.astype('int16').tostring()

Also, if you have multiple channels of data -- e.g. an ndarray with shape (2, 44100*4) -- you can use the transpose method (or just T for short) to interleave the frames.

ssignal = signal.T.astype('int16').tostring()

Anonymous said...

i tried your code,but i got an error:
Traceback (most recent call last):
File "C:/Python27/bithday message.py", line 9, in
import numpy as N
ImportError: No module named numpy.
what do you think might have happened?

Anonymous said...

i tried your code,but i got an error:
Traceback (most recent call last):
File "C:/Python27/bithday message.py", line 9, in
import numpy as N
ImportError: No module named numpy.
what do you think might have happened?

Mike Axiak said...

Anonymous: You have to install numpy first.

Dave said...

You can get numpy for windows here:

http://www.lfd.uci.edu/~gohlke/pythonlibs/#numpy

Jory Ferrell said...
This comment has been removed by the author.
Jory Ferrell said...


The wave module appears to be written mainly in C. So this struct object needs to be packed in order to be processed properly by the C code in order to be written. With out it, the data is not translated (to binary?) properly and the code just sees the data as gibberish.

Look at the wave.pyc file in your libs, and this link:
http://docs.python.org/2/library/wave.html

Anonymous said...

This was most helpful thanks