Skip to content

Commit 3f9c3ca

Browse files
bzaczynskimartin-martingahjelle
authored
Python wave (#492)
* Python wave: Initial commit (WIP) * Fix linter: remove Python 3.12-specific keyword 'type' * Update materials * Add the missing prompt * WAV: Apply TR feedback --------- Co-authored-by: Martin Breuss <martin-martin@users.noreply.github.com> Co-authored-by: Geir Arne Hjelle <geirarne@gmail.com>
1 parent 4f1e9d1 commit 3f9c3ca

46 files changed

Lines changed: 821 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

python-wav-files/README.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Reading and Writing WAV Files in Python
2+
3+
Sample code and sounds for the [Reading and Writing WAV Files in Python](https://realpython.com/python-wav-files/) tutorial on Real Python.
4+
5+
## Setup
6+
7+
Create and activate a new virtual environment:
8+
9+
```
10+
$ python3 -m venv venv/ --prompt wave
11+
$ source venv/bin/activate
12+
```
13+
14+
Install the required dependencies:
15+
16+
```
17+
(wave) $ python -m pip install -r requirements.txt -c constraints.txt
18+
```
19+
20+
## Usage
21+
22+
### Synthesize Sounds
23+
24+
```
25+
(wave) $ python synth_mono.py
26+
(wave) $ python synth_stereo.py
27+
(wave) $ python synth_beat.py
28+
```
29+
30+
### Synthesize 16-bit Stereo Sounds
31+
32+
```
33+
(wave) $ python synth_stereo_16bits_array.py
34+
(wave) $ python synth_stereo_16bits_bytearray.py
35+
(wave) $ python synth_stereo_16bits_ndarray.py
36+
```
37+
38+
### Plot a Static Waveform
39+
40+
```
41+
(wave) $ python plot_waveform.py sounds/Bicycle-bell.wav
42+
(wave) $ python plot_waveform.py sounds/Bongo_sound.wav -s 3.5 -e 3.65
43+
```
44+
45+
### Animate an Oscilloscope
46+
47+
```
48+
(wave) $ python plot_oscilloscope.py sounds/Bicycle-bell.wav
49+
(wave) $ python plot_oscilloscope.py sounds/Bongo_sound.wav -s 0.005
50+
```
51+
52+
### Animate a Spectrogram
53+
54+
```
55+
(wave) $ python plot_spectrogram.py sounds/Bicycle-bell.wav
56+
(wave) $ python plot_spectrogram.py sounds/Bongo_sound.wav -s 0.0005 -o 95
57+
```
58+
59+
### Record a Radio Stream
60+
61+
```
62+
(wave) $ RADIO_URL=http://prem2.di.fm:80/classiceurodance?your-secret-token
63+
(wave) $ python record_stream.py "$RADIO_URL" -o ripped.wav
64+
```
65+
66+
### Boost the Stereo Field
67+
68+
```
69+
(wave) $ python stereo_booster.py -i sounds/Bicycle-bell.wav -o boosted.wav -s 5
70+
```

python-wav-files/constraints.txt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
contourpy==1.2.0
2+
cycler==0.12.1
3+
fonttools==4.49.0
4+
kiwisolver==1.4.5
5+
matplotlib==3.8.3
6+
numpy==1.26.4
7+
packaging==23.2
8+
pillow==10.2.0
9+
pyav==12.0.2
10+
pyparsing==3.1.1
11+
python-dateutil==2.8.2
12+
six==1.16.0
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
from argparse import ArgumentParser
2+
from pathlib import Path
3+
4+
import matplotlib.pyplot as plt
5+
import numpy as np
6+
7+
from waveio import WAVReader
8+
9+
10+
def main():
11+
args = parse_args()
12+
with WAVReader(args.path) as wav:
13+
animate(
14+
args.path.name,
15+
args.seconds,
16+
slide_window(args.seconds, wav),
17+
)
18+
19+
20+
def parse_args():
21+
parser = ArgumentParser(description="Animate WAV file waveform")
22+
parser.add_argument("path", type=Path, help="path to the WAV file")
23+
parser.add_argument(
24+
"-s",
25+
"--seconds",
26+
type=float,
27+
default=0.05,
28+
help="sliding window size in seconds",
29+
)
30+
return parser.parse_args()
31+
32+
33+
def slide_window(window_seconds, wav):
34+
num_windows = round(wav.metadata.num_seconds / window_seconds)
35+
for i in range(num_windows):
36+
begin_seconds = i * window_seconds
37+
end_seconds = begin_seconds + window_seconds
38+
channels = wav.channels_sliced(begin_seconds, end_seconds)
39+
yield np.mean(tuple(channels), axis=0)
40+
41+
42+
def animate(filename, seconds, windows):
43+
try:
44+
plt.style.use("dark_background")
45+
except OSError:
46+
pass # Fall back to the default style
47+
48+
fig, ax = plt.subplots(figsize=(16, 9))
49+
fig.canvas.manager.set_window_title(filename)
50+
51+
plt.tight_layout()
52+
plt.box(False)
53+
54+
for window in windows:
55+
plt.cla()
56+
ax.set_xticks([])
57+
ax.set_yticks([])
58+
ax.set_ylim(-1.0, 1.0)
59+
plt.plot(window)
60+
plt.pause(seconds)
61+
62+
63+
if __name__ == "__main__":
64+
try:
65+
main()
66+
except KeyboardInterrupt:
67+
print("Aborted")
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
from argparse import ArgumentParser
2+
from pathlib import Path
3+
4+
import matplotlib.pyplot as plt
5+
import numpy as np
6+
7+
from waveio import WAVReader
8+
9+
10+
def main():
11+
args = parse_args()
12+
with WAVReader(args.path) as wav:
13+
animate(
14+
args.path.name,
15+
args.seconds,
16+
args.overlap,
17+
fft(slide_window(args.seconds, args.overlap, wav), wav),
18+
)
19+
20+
21+
def parse_args():
22+
parser = ArgumentParser(description="Animate WAV file spectrogram")
23+
parser.add_argument("path", type=Path, help="path to the WAV file")
24+
parser.add_argument(
25+
"-s",
26+
"--seconds",
27+
type=float,
28+
default=0.0015,
29+
help="sliding window size in seconds",
30+
)
31+
parser.add_argument(
32+
"-o",
33+
"--overlap",
34+
choices=range(100),
35+
default=50,
36+
type=int,
37+
help="sliding window overlap as a percentage",
38+
)
39+
return parser.parse_args()
40+
41+
42+
def slide_window(window_seconds, overlap_percentage, wav):
43+
step_seconds = window_seconds * (1 - overlap_percentage / 100)
44+
num_windows = round(wav.metadata.num_seconds / step_seconds)
45+
for i in range(num_windows):
46+
begin_seconds = i * step_seconds
47+
end_seconds = begin_seconds + window_seconds
48+
channels = wav.channels_sliced(begin_seconds, end_seconds)
49+
yield np.mean(tuple(channels), axis=0)
50+
51+
52+
def fft(windows, wav):
53+
sampling_period = 1 / wav.metadata.frames_per_second
54+
for window in windows:
55+
frequencies = np.fft.rfftfreq(window.size, sampling_period)
56+
magnitudes = np.abs(
57+
np.fft.rfft((window - np.mean(window)) * np.blackman(window.size))
58+
)
59+
yield frequencies, magnitudes
60+
61+
62+
def animate(filename, seconds, overlap_percentage, windows):
63+
try:
64+
plt.style.use("dark_background")
65+
except OSError:
66+
pass # Fall back to the default style
67+
68+
fig, ax = plt.subplots(figsize=(16, 9))
69+
fig.canvas.manager.set_window_title(filename)
70+
71+
plt.tight_layout()
72+
plt.box(False)
73+
74+
bar_gap = 0.25
75+
for frequencies, magnitudes in windows:
76+
bar_width = (frequencies[-1] / frequencies.size) * (1 - bar_gap)
77+
plt.cla()
78+
ax.set_xticks([])
79+
ax.set_yticks([])
80+
ax.set_xlim(-bar_width / 2, frequencies[-1] - bar_width / 2)
81+
ax.set_ylim(0, np.max(magnitudes))
82+
ax.bar(frequencies, magnitudes, width=bar_width)
83+
plt.pause(seconds * (1 - overlap_percentage / 100))
84+
85+
86+
if __name__ == "__main__":
87+
try:
88+
main()
89+
except KeyboardInterrupt:
90+
print("Aborted")

python-wav-files/plot_waveform.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
from argparse import ArgumentParser
2+
from pathlib import Path
3+
4+
import matplotlib.pyplot as plt
5+
import numpy as np
6+
from matplotlib.ticker import FuncFormatter
7+
8+
from waveio import WAVReader
9+
10+
11+
def main():
12+
args = parse_args()
13+
with WAVReader(args.path) as wav:
14+
plot(
15+
args.path.name,
16+
wav.metadata,
17+
wav.channels_sliced(args.start, args.end),
18+
)
19+
20+
21+
def parse_args():
22+
parser = ArgumentParser(description="Plot the waveform of a WAV file")
23+
parser.add_argument("path", type=Path, help="path to the WAV file")
24+
parser.add_argument(
25+
"-s",
26+
"--start",
27+
type=float,
28+
default=0.0,
29+
help="start time in seconds (default: 0.0)",
30+
)
31+
parser.add_argument(
32+
"-e",
33+
"--end",
34+
type=float,
35+
default=None,
36+
help="end time in seconds (default: end of file)",
37+
)
38+
return parser.parse_args()
39+
40+
41+
def plot(filename, metadata, channels):
42+
try:
43+
plt.style.use("fivethirtyeight")
44+
except OSError:
45+
pass # Fall back to the default style
46+
47+
fig, ax = plt.subplots(
48+
nrows=metadata.num_channels,
49+
ncols=1,
50+
figsize=(16, 9),
51+
sharex=True,
52+
)
53+
54+
if isinstance(ax, plt.Axes):
55+
ax = [ax]
56+
57+
time_formatter = FuncFormatter(format_time)
58+
timeline = np.linspace(
59+
channels.frames_range.start / metadata.frames_per_second,
60+
channels.frames_range.stop / metadata.frames_per_second,
61+
len(channels.frames_range),
62+
)
63+
64+
for i, channel in enumerate(channels):
65+
ax[i].set_title(f"Channel #{i + 1}")
66+
ax[i].set_yticks([-1, -0.5, 0, 0.5, 1])
67+
ax[i].xaxis.set_major_formatter(time_formatter)
68+
ax[i].plot(timeline, channel)
69+
70+
fig.canvas.manager.set_window_title(filename)
71+
plt.tight_layout()
72+
plt.show()
73+
74+
75+
def format_time(instant, _):
76+
if instant < 60:
77+
return f"{instant:g}s"
78+
minutes, seconds = divmod(instant, 60)
79+
return f"{minutes:g}m {seconds:02g}s"
80+
81+
82+
if __name__ == "__main__":
83+
main()

python-wav-files/record_stream.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from argparse import ArgumentParser
2+
3+
from stream import RadioStream
4+
from waveio import WAVWriter
5+
6+
7+
def main():
8+
args = parse_args()
9+
with RadioStream(args.stream_url) as radio_stream:
10+
with WAVWriter(radio_stream.metadata, args.output) as writer:
11+
for channels_chunk in radio_stream:
12+
writer.append_channels(channels_chunk)
13+
14+
15+
def parse_args():
16+
parser = ArgumentParser(description="Record an Internet radio stream")
17+
parser.add_argument("stream_url", help="URL address of the stream")
18+
parser.add_argument(
19+
"-o",
20+
"--output",
21+
metavar="path",
22+
required=True,
23+
type=str,
24+
help="path to the output WAV file",
25+
)
26+
return parser.parse_args()
27+
28+
29+
if __name__ == "__main__":
30+
try:
31+
main()
32+
except KeyboardInterrupt:
33+
print("Aborted")

python-wav-files/requirements.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
matplotlib
2+
numpy
3+
pyav
108 KB
Binary file not shown.
215 KB
Binary file not shown.
323 KB
Binary file not shown.

0 commit comments

Comments
 (0)