Chapter 19
Sounds are represented in computer memory as a sequence of discrete samples of air pressure readings. CDs are recorded using 44,100 of these samples per second. With that many samples, it is possible to reconstruct the original sound waves in such a way that our ears have a difficult time hearing any difference between the reproduction and the original sound. Given access to those samples in a Python program, we can do some interesting things.
Listing 19.1: Reverse WAV
1 # reversewav.py
2 import array
3 import contextlib
4 import wave
5
6 def datatype(width):
7 return "B" if width == 1 else "h"
8
9 def readwav(fname):
10 with contextlib.closing(wave.open(fname)) as f:
11 params = f.getparams()
12 frames = f.readframes(params[3])
13 return array.array(datatype(params[1]), frames), params
14
15 def writewav(fname, data, params):
16 with contextlib.closing(wave.open(fname, "w")) as f:
17 f.setparams(params)
18 f.writeframes(data.tostring())
19 print(fname, "written.")
20
21 def main():
22 fname = input("Enter the name of a .wav file: ")
23 data, params = readwav(fname)
24 outfname = "rev" + fname
25 writewav(outfname, data[::−1], params)
26
27 main()
Before running this program, put a WAV file with name ending in .wav in the same folder or directory as the program, and provide that file name when the program requests it. Then listen to the file that was created. See page 71 if you do not recognize the conditional expression on line 7.
The wave module provides one key function:
wave.open(file[, mode]) |
Open file in mode "r" (read, the default) |
|
or "w" (write). |
The wave.open() function returns either a Wave_read or Wave_write object. If f is a Wave_read object, then it has these methods:
f.getparams() |
Get tuple of parameters describing f. |
f.readframes(n) |
Read n frames from f. |
The parameters returned by .getparams() are described below. There is also a separate .get...() method for each of the individual parameters.
If f is a Wave_write object, then it has these corresponding methods:
f.setparams(params) |
Set parameters for f to params. |
f.writeframes(frames) |
Write frames to f. |
The argument to .setparams() may be a tuple or list. There is a separate .set...() method for each of the individual parameters. In Listing 19.1, the output file parameters are set to be the same as those that were used in the input file.
Understanding the parameters that control WAV files and their relation to the frames returned is the key to using them correctly. A call to .getparams() returns these six values:
(nchannels, sampwidth, framerate, nframes, comptype, compname)
nchannels: The number of channels per frame: 1 for mono recordings, 2 for stereo. If there are 2, the samples alternate left, right, left, right, etc.
sampwidth: The number of bytes per sample. One-byte samples are unsigned integers 0 to 255, whereas two-byte samples are signed integers −32, 768 to 32,767.
framerate: Also known as the sampling frequency, this is the number of frames per second (44,100 for CD quality).
nframes: The number of frames in the file.
For example, if nchannels = 2 and sampwidth = 2, then each frame is 4 bytes long and made up of 2 samples, left followed by right. If nchannels = 1 and sampwidth = 1, then each frame is a single sample made up of one byte.
The last two parameters refer to compression; we will not use them.
Both .getparams() and .setparams() use a tuple to communicate all six parameters at once. In Python, a tuple is exactly like a list except that it is immutable.
Tuples are written with parentheses instead of square brackets: for example, t = (1, 2, 3) is a tuple, whereas u = [1, 2, 3] is a list. Individual entries may be accessed by index, as in lines 12 and 13 of Listing 19.1.
It is sometimes convenient to unpack a tuple into its separate entries using multiple assignment:
<var1>, <var2>, ..., <varN> = <tuple>
Each variable on the left is assigned to the corresponding value from the tuple on the right, as long as the number of items match.
Recall our earlier discussion of multiple return values and multiple assignment (see page 95). In the terminology of this chapter, functions return more than one value by returning a tuple, and that tuple is often unpacked by the caller.
The array module converts the raw bytes in a sound sample into an array that is easy to manipulate.
array.array(code, rawdata) |
Array initialized with rawdata |
|
according to code. |
The type codes corresponding to data in WAV files are:
B |
Unsigned 1-byte integer. |
h |
Signed 2-byte integer. |
The array object returned by array.array() supports indexing, slicing, concatenation, repeated concatenation, and most of the same methods as lists. In other words, once you have sound data in an array, you can manipulate it just as you would a list. If data is an array, you can find out the type code it was created with like this:
data.typecode |
Type code used to create data. |
This is technically a field of the array object, which you will learn about in Chapters 21 and 23.
Finally, Listing 19.1 requires the contextlib module, which provides methods that objects need in order to be used inside with statements. In this case, we need a closing function because without it, Wave_read and Wave_write objects do not know to call their .close() method when the with block completes.
contextlib.closing(obj) |
Manager to close obj when block finishes. |
f.setparams((nchannels, sampwidth, framerate,
nframes, comptype, compname))
What could the inner set of parentheses be replaced with?
Hint: it can be done in one line using min() and max().
The data array returned by this function will be longer than the original because of the delayed echo, so you will not be able to use the original file’s params when you write the new file. You will need to send a new set of parameters (which may be in a list) to write the new file.
18.220.151.158