Skip to content

Commit

Permalink
Update materials
Browse files Browse the repository at this point in the history
  • Loading branch information
bzaczynski committed Feb 18, 2024
1 parent 34c881c commit 3a46e6c
Show file tree
Hide file tree
Showing 51 changed files with 531 additions and 317 deletions.
67 changes: 67 additions & 0 deletions python-wav-files/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,70 @@
# Reading and Writing WAV Files in Python

Sample code and sounds for the [Reading and Writing WAV Files in Python](https://realpython.com/python-wav-files/) tutorial on Real Python.

## Setup

Create and activate a new virtual environment:

```
$ python3 -m venv venv/ --prompt wave
$ source venv/bin/activate
```

Install the required dependencies:

```
(wave) $ python -m pip install -r requirements.txt -c constraints.txt
```

## Usage

### Synthesize Sounds

```
(wave) $ python synth_mono.py
(wave) $ python synth_stereo.py
(wave) $ python synth_beat.py
```

### Synthesize 16-bit Stereo Sounds

```
(wave) $ python synth_stereo_16bits_array.py
(wave) $ python synth_stereo_16bits_bytearray.py
(wave) $ python synth_stereo_16bits_ndarray.py
```

### Plot a Static Waveform

```
(wave) $ python plot_waveform.py sounds/Bicycle-bell.wav
(wave) $ python plot_waveform.py sounds/Bongo_sound.wav -s 3.5 -e 3.65
```

### Animate an Oscilloscope

```
(wave) $ python plot_oscilloscope.py sounds/Bicycle-bell.wav
(wave) $ python plot_oscilloscope.py sounds/Bongo_sound.wav -s 0.005
```

### Animate a Spectrogram

```
(wave) $ python plot_spectrogram.py sounds/Bicycle-bell.wav
(wave) $ python plot_spectrogram.py sounds/Bongo_sound.wav -s 0.0005 -o 95
```

### Record a Radio Stream

```
$ RADIO_URL=http://prem2.di.fm:80/classiceurodance?your-secret-token
(wave) $ python record_stream.py "$RADIO_URL" -o ripped.wav
```

### Boost the Stereo Field

```
(wave) $ python stereo_booster.py -i sounds/Bicycle-bell.wav -o boosted.wav -s 5
```
12 changes: 12 additions & 0 deletions python-wav-files/constraints.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
contourpy==1.2.0
cycler==0.12.1
fonttools==4.49.0
kiwisolver==1.4.5
matplotlib==3.8.3
numpy==1.26.4
packaging==23.2
pillow==10.2.0
pyav==12.0.2
pyparsing==3.1.1
python-dateutil==2.8.2
six==1.16.0
Original file line number Diff line number Diff line change
@@ -1,58 +1,57 @@
from argparse import ArgumentParser
from pathlib import Path
from typing import TypeAlias

import matplotlib.pyplot as plt
import numpy as np
from matplotlib import pyplot as plt

from waveio.reader import WaveReader

Samples: TypeAlias = np.ndarray
from waveio import WAVReader


def main():
args = parse_args()
with WaveReader(args.path) as reader:
animate(args.seconds, slide_window(args.seconds, reader))
with WAVReader(args.path) as wav:
animate(
args.path.name,
args.seconds,
slide_window(args.seconds, wav),
)


def parse_args():
parser = ArgumentParser(
description="Animate WAV file waveform",
epilog="Example: oscilloscope.py sounds/Bongo_sound.wav -s 0.1",
)
parser = ArgumentParser(description="Animate WAV file waveform")
parser.add_argument("path", type=Path, help="path to the WAV file")
parser.add_argument(
"-s",
"--seconds",
type=float,
default=0.01,
help="size of the sliding window in seconds",
default=0.05,
help="sliding window size in seconds",
)
return parser.parse_args()


def slide_window(seconds, reader):
num_windows = round(reader.metadata.num_seconds / seconds)
def slide_window(window_seconds, wav):
num_windows = round(wav.metadata.num_seconds / window_seconds)
for i in range(num_windows):
begin_seconds = i * seconds
end_seconds = begin_seconds + seconds
channels = reader.channels_sliced(begin_seconds, end_seconds)
begin_seconds = i * window_seconds
end_seconds = begin_seconds + window_seconds
channels = wav.channels_sliced(begin_seconds, end_seconds)
yield np.mean(tuple(channels), axis=0)


def animate(seconds, slider):
def animate(filename, seconds, windows):
try:
plt.style.use("dark_background")
except OSError:
pass # Fall back to the default style

fig, ax = plt.subplots(figsize=(16, 9))
fig.canvas.manager.set_window_title(filename)

plt.tight_layout()
plt.box(False)

for window in slider:
for window in windows:
plt.cla()
ax.set_xticks([])
ax.set_yticks([])
Expand Down
90 changes: 90 additions & 0 deletions python-wav-files/plot_spectrogram.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
from argparse import ArgumentParser
from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np

from waveio import WAVReader


def main():
args = parse_args()
with WAVReader(args.path) as wav:
animate(
args.path.name,
args.seconds,
args.overlap,
fft(slide_window(args.seconds, args.overlap, wav), wav),
)


def parse_args():
parser = ArgumentParser(description="Animate WAV file spectrogram")
parser.add_argument("path", type=Path, help="path to the WAV file")
parser.add_argument(
"-s",
"--seconds",
type=float,
default=0.0015,
help="sliding window size in seconds",
)
parser.add_argument(
"-o",
"--overlap",
choices=range(100),
default=50,
type=int,
help="sliding window overlap as a percentage",
)
return parser.parse_args()


def slide_window(window_seconds, overlap_percentage, wav):
step_seconds = window_seconds * (1 - overlap_percentage / 100)
num_windows = round(wav.metadata.num_seconds / step_seconds)
for i in range(num_windows):
begin_seconds = i * step_seconds
end_seconds = begin_seconds + window_seconds
channels = wav.channels_sliced(begin_seconds, end_seconds)
yield np.mean(tuple(channels), axis=0)


def fft(windows, wav):
sampling_period = 1 / wav.metadata.frames_per_second
for window in windows:
frequencies = np.fft.rfftfreq(window.size, sampling_period)
magnitudes = np.abs(
np.fft.rfft((window - np.mean(window)) * np.blackman(window.size))
)
yield frequencies, magnitudes


def animate(filename, seconds, overlap_percentage, windows):
try:
plt.style.use("dark_background")
except OSError:
pass # Fall back to the default style

fig, ax = plt.subplots(figsize=(16, 9))
fig.canvas.manager.set_window_title(filename)

plt.tight_layout()
plt.box(False)

bar_gap = 0.25
for frequencies, magnitudes in windows:
bar_width = (frequencies[-1] / frequencies.size) * (1 - bar_gap)
plt.cla()
ax.set_xticks([])
ax.set_yticks([])
ax.set_xlim(-bar_width / 2, frequencies[-1] - bar_width / 2)
ax.set_ylim(0, np.max(magnitudes))
ax.bar(frequencies, magnitudes, width=bar_width)
plt.pause(seconds * (1 - overlap_percentage / 100))


if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print("Aborted")
48 changes: 27 additions & 21 deletions python-wav-files/plot_waveform.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,29 @@
from argparse import ArgumentParser
from pathlib import Path

from matplotlib import pyplot as plt
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.ticker import FuncFormatter

from waveio.reader import WaveReader
from waveio import WAVReader


def main():
args = parse_args()
with WaveReader(args.path) as reader:
plot(reader.channels_sliced(args.begin, args.end))
with WAVReader(args.path) as wav:
plot(
args.path.name,
wav.metadata,
wav.channels_sliced(args.start, args.end),
)


def parse_args():
parser = ArgumentParser(
description="Plot WAV file waveform",
epilog="Example: plot_waveform.py sounds/Bongo_sound.wav -b -2.5",
)
parser = ArgumentParser(description="Plot the waveform of a WAV file")
parser.add_argument("path", type=Path, help="path to the WAV file")
parser.add_argument(
"-b",
"--begin",
"-s",
"--start",
type=float,
default=0.0,
help="start time in seconds (default: 0.0)",
Expand All @@ -36,14 +38,14 @@ def parse_args():
return parser.parse_args()


def plot(channels):
def plot(filename, metadata, channels):
try:
plt.style.use("fivethirtyeight")
except OSError:
pass # Fall back to the default style

fig, ax = plt.subplots(
nrows=len(channels),
nrows=metadata.num_channels,
ncols=1,
figsize=(16, 9),
sharex=True,
Expand All @@ -53,25 +55,29 @@ def plot(channels):
ax = [ax]

time_formatter = FuncFormatter(format_time)
timeline = np.linspace(
channels.frames_range.start / metadata.frames_per_second,
channels.frames_range.stop / metadata.frames_per_second,
len(channels.frames_range),
)

for i, channel in enumerate(channels):
ax[i].plot(channels.x_range, channel)
ax[i].set_title(f"Channel #{i + 1}")
ax[i].set_yticks([-1, -0.5, 0, 0.5, 1])
ax[i].xaxis.set_major_formatter(time_formatter)
ax[i].plot(timeline, channel)

fig.canvas.manager.set_window_title(filename)
plt.tight_layout()
plt.show()


def format_time(seconds, _):
minutes = int(seconds // 60)
seconds = int(seconds % 60)
return f"{minutes:02}:{seconds:02}"
def format_time(instant, _):
if instant < 60:
return f"{instant:g}s"
minutes, seconds = divmod(instant, 60)
return f"{minutes:g}m {seconds:02g}s"


if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print("Aborted")
main()
17 changes: 7 additions & 10 deletions python-wav-files/ripper.py → python-wav-files/record_stream.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
from argparse import ArgumentParser

from radio import RadioStream
from waveio.writer import WaveWriter
from stream import RadioStream
from waveio import WAVWriter


def main():
args = parse_args()
with RadioStream(args.stream_url) as stream:
with WaveWriter(stream.metadata, args.output) as writer:
for chunk in stream:
writer.append_bytes(chunk)
with RadioStream(args.stream_url) as radio_stream:
with WAVWriter(radio_stream.metadata, args.output) as writer:
for channels_chunk in radio_stream:
writer.append_channels(channels_chunk)


def parse_args():
parser = ArgumentParser(
description="Record an Internet radio stream",
epilog="Example: ripper.py http://prem2.di.fm/lounge -o output.wav",
)
parser = ArgumentParser(description="Record an Internet radio stream")
parser.add_argument("stream_url", help="URL address of the stream")
parser.add_argument(
"-o",
Expand Down
3 changes: 0 additions & 3 deletions python-wav-files/requirements.in

This file was deleted.

15 changes: 3 additions & 12 deletions python-wav-files/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,12 +1,3 @@
contourpy==1.2.0
cycler==0.12.1
fonttools==4.47.2
kiwisolver==1.4.5
matplotlib==3.8.2
numpy==1.26.3
packaging==23.2
pillow==10.2.0
pyav==12.0.2
pyparsing==3.1.1
python-dateutil==2.8.2
six==1.16.0
matplotlib
numpy
pyav
Binary file removed python-wav-files/sounds/44100_pcm08_ch3.wav
Binary file not shown.
Binary file modified python-wav-files/sounds/44100_pcm08_mono.wav
Binary file not shown.
Binary file modified python-wav-files/sounds/44100_pcm08_stereo.wav
Binary file not shown.
Binary file added python-wav-files/sounds/44100_pcm08_surround.wav
Binary file not shown.
Binary file removed python-wav-files/sounds/44100_pcm16_ch3.wav
Binary file not shown.
Binary file modified python-wav-files/sounds/44100_pcm16_mono.wav
Binary file not shown.
Binary file modified python-wav-files/sounds/44100_pcm16_stereo.wav
Binary file not shown.
Binary file added python-wav-files/sounds/44100_pcm16_surround.wav
Binary file not shown.
Binary file removed python-wav-files/sounds/44100_pcm24_ch3.wav
Binary file not shown.
Binary file modified python-wav-files/sounds/44100_pcm24_mono.wav
Binary file not shown.
Binary file modified python-wav-files/sounds/44100_pcm24_stereo.wav
Binary file not shown.
Binary file added python-wav-files/sounds/44100_pcm24_surround.wav
Binary file not shown.
Binary file removed python-wav-files/sounds/44100_pcm32_ch3.wav
Binary file not shown.
Binary file modified python-wav-files/sounds/44100_pcm32_mono.wav
Binary file not shown.
Binary file modified python-wav-files/sounds/44100_pcm32_stereo.wav
Binary file not shown.
Binary file added python-wav-files/sounds/44100_pcm32_surround.wav
Binary file not shown.
Binary file modified python-wav-files/sounds/8000_pcm08_mono.wav
Binary file not shown.
Binary file modified python-wav-files/sounds/8000_pcm08_stereo.wav
Binary file not shown.
Binary file added python-wav-files/sounds/8000_pcm08_surround.wav
Binary file not shown.
Binary file modified python-wav-files/sounds/8000_pcm16_mono.wav
Binary file not shown.
Binary file modified python-wav-files/sounds/8000_pcm16_stereo.wav
Binary file not shown.
Binary file added python-wav-files/sounds/8000_pcm16_surround.wav
Binary file not shown.
Binary file modified python-wav-files/sounds/8000_pcm24_mono.wav
Binary file not shown.
Binary file modified python-wav-files/sounds/8000_pcm24_stereo.wav
Binary file not shown.
Binary file added python-wav-files/sounds/8000_pcm24_surround.wav
Binary file not shown.
Binary file modified python-wav-files/sounds/8000_pcm32_mono.wav
Binary file not shown.
Binary file modified python-wav-files/sounds/8000_pcm32_stereo.wav
Binary file not shown.
Binary file added python-wav-files/sounds/8000_pcm32_surround.wav
Binary file not shown.
Binary file added python-wav-files/sounds/Bicycle-bell.wav
Binary file not shown.
Loading

0 comments on commit 3a46e6c

Please sign in to comment.