-
Notifications
You must be signed in to change notification settings - Fork 531
Expand file tree
/
Copy pathtest_audio.py
More file actions
164 lines (128 loc) · 5.83 KB
/
Copy pathtest_audio.py
File metadata and controls
164 lines (128 loc) · 5.83 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
import io
import struct
import wave
import numpy as np
import pytest
from theater.support.audio import (
_MIN_CAPACITY,
as_samples,
AudioWriter,
read_samples_from_wav_bytes,
truncate_samples,
)
from theater.support.constants import MAX_AUDIO_SECONDS, SAMPLE_RATE
def _make_wav_bytes(samples, channels, frame_rate=SAMPLE_RATE):
int_samples = (np.asarray(samples) * 32768).astype("<i2")
buffer = io.BytesIO()
with wave.open(buffer, "wb") as writer:
writer.setnchannels(channels)
writer.setsampwidth(2)
writer.setframerate(frame_rate)
writer.writeframes(int_samples.tobytes())
return buffer.getvalue()
def test_read_mono_wav():
samples = read_samples_from_wav_bytes(_make_wav_bytes([0.0, 0.5, -0.5], 1))
assert np.allclose(samples, [0.0, 0.5, -0.5], atol=1e-4)
def test_read_stereo_wav_averages_channels():
# Interleaved L/R: (0.2,0.6) and (0.4,-0.4) average to 0.4 and 0.0.
samples = read_samples_from_wav_bytes(_make_wav_bytes([0.2, 0.6, 0.4, -0.4], 2))
assert np.allclose(samples, [0.4, 0.0], atol=1e-4)
@pytest.mark.parametrize("frame_rate", [8000, 22050, 48000, 88200])
def test_read_resamples_to_the_output_rate(frame_rate):
# One second in, one second out: without this the samples were spliced onto
# the timeline verbatim, so 8 kHz input played 5.5x too fast.
samples = read_samples_from_wav_bytes(
_make_wav_bytes(np.full(frame_rate, 0.5), 1, frame_rate)
)
assert len(samples) == SAMPLE_RATE
# A constant interpolates to itself, so resampling must not alter the level.
assert np.allclose(samples, 0.5, atol=1e-4)
def test_read_leaves_output_rate_input_alone():
samples = read_samples_from_wav_bytes(_make_wav_bytes([0.25, -0.25], 1))
assert np.allclose(samples, [0.25, -0.25], atol=1e-4)
def test_read_rejects_a_missing_sample_rate():
# The wave module refuses to write a zero rate, so patch the field directly.
# Sample rate sits at bytes 24:28 of the canonical header it emits.
wav = bytearray(_make_wav_bytes([0.5], 1))
assert wav[24:28] == struct.pack("<I", SAMPLE_RATE)
wav[24:28] = struct.pack("<I", 0)
with pytest.raises(ValueError):
read_samples_from_wav_bytes(bytes(wav))
def test_read_rejects_a_sound_past_the_length_ceiling():
# Refused from the header, before any frame is read: a file this long would
# not fit on the timeline anyway.
frame_rate = 8000
long_wav = _make_wav_bytes(
np.zeros(frame_rate * (MAX_AUDIO_SECONDS + 1)), 1, frame_rate
)
with pytest.raises(ValueError):
read_samples_from_wav_bytes(long_wav)
@pytest.mark.parametrize(
"sound", [[0.5, 0.25], (0.5, 0.25), np.array([0.5, 0.25]), iter([0.5, 0.25])]
)
def test_as_samples_accepts_any_sequence_or_iterator(sound):
samples = as_samples(sound)
assert samples.dtype == np.float32
assert np.allclose(samples, [0.5, 0.25])
def test_as_samples_copies_what_it_is_given():
# numpy hands back the same array for a same-dtype input unless told to copy,
# which would leave the caller holding the scene's samples.
original = np.array([0.5, 0.5], dtype=np.float32)
samples = as_samples(original)
original[0] = -1.0
assert samples[0] == 0.5
def test_truncate_shortens_but_never_extends():
samples = np.ones(SAMPLE_RATE)
assert len(truncate_samples(samples, 0.5)) == SAMPLE_RATE // 2
assert len(truncate_samples(samples, 2.0)) == SAMPLE_RATE
@pytest.mark.parametrize("length_seconds", [0.0, -0.001, -1.0, -1e6])
def test_truncate_never_trims_from_the_end(length_seconds):
# A negative length reaching numpy unclamped would slice off the tail and
# play most of the sample instead of none of it.
assert len(truncate_samples(np.ones(SAMPLE_RATE), length_seconds)) == 0
def test_blend_adds_and_clamps():
writer = AudioWriter()
writer.write_audio_samples([0.8, 0.8])
writer.write_audio_samples([0.8, -0.8]) # blended at cursor 0
wav = writer.to_wav_bytes()
samples = read_samples_from_wav_bytes(wav)
# 0.8 + 0.8 clamps to 1.0; 0.8 + -0.8 = 0.0
assert samples[0] > 0.99
assert abs(samples[1]) < 0.01
def test_delay_inserts_silence():
writer = AudioWriter()
writer.add_delay_milliseconds(1000)
writer.write_audio_samples([1.0])
samples = read_samples_from_wav_bytes(writer.to_wav_bytes())
assert len(samples) == SAMPLE_RATE + 1
assert samples[0] == 0.0
@pytest.mark.parametrize("milliseconds", [10, 120, 430, 1000, 59000])
def test_delay_lands_on_a_whole_sample(milliseconds):
# The gif counts centiseconds, so the cursor has to sit on the same sample a
# frame delay of this length does, exactly, however long the scene runs.
writer = AudioWriter()
writer.add_delay_milliseconds(milliseconds)
writer.write_audio_samples([1.0])
samples = read_samples_from_wav_bytes(writer.to_wav_bytes())
assert len(samples) - 1 == milliseconds * SAMPLE_RATE // 1000
def test_length_is_what_was_written_not_what_was_reserved():
# The timeline is allocated ahead of the samples, so a writer measuring its
# capacity would pad every program with a second of trailing silence.
writer = AudioWriter()
writer.write_audio_samples([1.0, 1.0])
assert writer.get_total_audio_length() == 2 / SAMPLE_RATE
assert len(read_samples_from_wav_bytes(writer.to_wav_bytes())) == 2
def test_a_long_melody_does_not_recopy_the_timeline():
# Reserving exactly what each note needs re-copies everything written so far,
# once per note: quadratic, and about 6 GB of copying over a 600-note melody.
# Doubling keeps the reserved space within a constant factor of the track.
note_samples = [0.5] * 100
writer = AudioWriter()
for _ in range(2000):
writer.write_audio_samples(note_samples)
writer.add_delay_milliseconds(10)
written = 1999 * (10 * SAMPLE_RATE // 1000) + len(note_samples)
assert writer.get_total_audio_length() == written / SAMPLE_RATE
assert len(writer._samples) < 2 * written + _MIN_CAPACITY
def test_empty_writer_returns_none():
assert AudioWriter().to_wav_bytes() is None