audiowaveform 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/COPYING +674 -0
- data/Cargo.lock +1108 -0
- data/Cargo.toml +28 -0
- data/README.md +255 -0
- data/bindings/ruby/CHANGELOG.md +29 -0
- data/bindings/ruby/Cargo.lock +621 -0
- data/bindings/ruby/Cargo.toml +8 -0
- data/bindings/ruby/README.md +193 -0
- data/bindings/ruby/ext/audiowaveform/Cargo.toml +20 -0
- data/bindings/ruby/ext/audiowaveform/build.rs +4 -0
- data/bindings/ruby/ext/audiowaveform/extconf.rb +6 -0
- data/bindings/ruby/ext/audiowaveform/src/lib.rs +239 -0
- data/bindings/ruby/lib/audiowaveform/version.rb +5 -0
- data/bindings/ruby/lib/audiowaveform.rb +131 -0
- data/crates/audiowaveform/Cargo.toml +73 -0
- data/crates/audiowaveform/examples/generate_from_pcm.rs +25 -0
- data/crates/audiowaveform/examples/generate_waveform.rs +18 -0
- data/crates/audiowaveform/examples/render_waveform.rs +18 -0
- data/crates/audiowaveform/examples/resample_waveform.rs +19 -0
- data/crates/audiowaveform/src/audio.rs +938 -0
- data/crates/audiowaveform/src/color.rs +201 -0
- data/crates/audiowaveform/src/error.rs +89 -0
- data/crates/audiowaveform/src/format.rs +215 -0
- data/crates/audiowaveform/src/lib.rs +79 -0
- data/crates/audiowaveform/src/render.rs +802 -0
- data/crates/audiowaveform/src/wav.rs +95 -0
- data/crates/audiowaveform/src/waveform.rs +790 -0
- data/crates/audiowaveform/tests/formats.rs +223 -0
- data/crates/audiowaveform/tests/generate.rs +233 -0
- data/crates/audiowaveform/tests/render.rs +263 -0
- data/crates/audiowaveform/tests/support/mod.rs +125 -0
- data/crates/audiowaveform/tests/wav.rs +54 -0
- data/crates/audiowaveform/tests/waveform_io.rs +255 -0
- data/crates/audiowaveform-cli/Cargo.toml +43 -0
- data/crates/audiowaveform-cli/src/main.rs +803 -0
- data/crates/audiowaveform-cli/tests/cli.rs +483 -0
- data/crates/audiowaveform-cli/tests/support/mod.rs +111 -0
- data/sig/audiowaveform.rbs +33 -0
- metadata +100 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
mod support;
|
|
2
|
+
|
|
3
|
+
use audiowaveform::{
|
|
4
|
+
AmplitudeScale, Color, ColorScheme, RenderOptions, RenderStyle, ScaleSpec, WaveformColors,
|
|
5
|
+
render_waveform,
|
|
6
|
+
};
|
|
7
|
+
#[cfg(feature = "format-wav")]
|
|
8
|
+
use audiowaveform::{GenerateOptions, generate_waveform_from_path};
|
|
9
|
+
|
|
10
|
+
use self::support::{assert_png_image_matches_fixture, load_waveform};
|
|
11
|
+
|
|
12
|
+
#[test]
|
|
13
|
+
fn renders_default_waveform_image_fixture() {
|
|
14
|
+
let waveform = load_waveform("test_file_stereo_8bit_64spp_wav.dat")
|
|
15
|
+
.resample(ScaleSpec::SamplesPerPixel(128))
|
|
16
|
+
.expect("resample waveform");
|
|
17
|
+
let image = render_waveform(&waveform, &RenderOptions::default()).expect("render image");
|
|
18
|
+
|
|
19
|
+
assert_png_image_matches_fixture(&image, "test_file_stereo_dat_128spp.png");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
#[test]
|
|
23
|
+
fn renders_waveform_images_with_custom_render_options() {
|
|
24
|
+
let waveform = load_waveform("test_file_stereo_8bit_64spp_wav.dat")
|
|
25
|
+
.resample(ScaleSpec::SamplesPerPixel(128))
|
|
26
|
+
.expect("resample waveform");
|
|
27
|
+
let two_channel = load_waveform("test_file_2channel_8bit_64spp_wav.dat")
|
|
28
|
+
.resample(ScaleSpec::SamplesPerPixel(128))
|
|
29
|
+
.expect("resample two-channel waveform");
|
|
30
|
+
|
|
31
|
+
let cases = [
|
|
32
|
+
(
|
|
33
|
+
"test_file_stereo_dat_128spp_no_axis_labels.png",
|
|
34
|
+
RenderOptions {
|
|
35
|
+
axis_labels: false,
|
|
36
|
+
..RenderOptions::default()
|
|
37
|
+
},
|
|
38
|
+
&waveform,
|
|
39
|
+
),
|
|
40
|
+
(
|
|
41
|
+
"test_file_stereo_dat_128spp_scale_1.5.png",
|
|
42
|
+
RenderOptions {
|
|
43
|
+
amplitude_scale: AmplitudeScale::Fixed(1.5),
|
|
44
|
+
..RenderOptions::default()
|
|
45
|
+
},
|
|
46
|
+
&waveform,
|
|
47
|
+
),
|
|
48
|
+
(
|
|
49
|
+
"test_file_stereo_dat_128spp_scale_auto.png",
|
|
50
|
+
RenderOptions {
|
|
51
|
+
amplitude_scale: AmplitudeScale::Auto,
|
|
52
|
+
..RenderOptions::default()
|
|
53
|
+
},
|
|
54
|
+
&waveform,
|
|
55
|
+
),
|
|
56
|
+
(
|
|
57
|
+
"test_file_stereo_dat_128spp_offset.png",
|
|
58
|
+
RenderOptions {
|
|
59
|
+
start_time: 0.5,
|
|
60
|
+
..RenderOptions::default()
|
|
61
|
+
},
|
|
62
|
+
&waveform,
|
|
63
|
+
),
|
|
64
|
+
(
|
|
65
|
+
"test_file_stereo_dat_128spp_square_bars.png",
|
|
66
|
+
RenderOptions {
|
|
67
|
+
style: RenderStyle::Bars {
|
|
68
|
+
width: 8,
|
|
69
|
+
gap: 4,
|
|
70
|
+
style: audiowaveform::BarStyle::Square,
|
|
71
|
+
},
|
|
72
|
+
..RenderOptions::default()
|
|
73
|
+
},
|
|
74
|
+
&waveform,
|
|
75
|
+
),
|
|
76
|
+
(
|
|
77
|
+
"test_file_stereo_dat_128spp_rounded_bars.png",
|
|
78
|
+
RenderOptions {
|
|
79
|
+
style: RenderStyle::Bars {
|
|
80
|
+
width: 8,
|
|
81
|
+
gap: 4,
|
|
82
|
+
style: audiowaveform::BarStyle::Rounded,
|
|
83
|
+
},
|
|
84
|
+
..RenderOptions::default()
|
|
85
|
+
},
|
|
86
|
+
&waveform,
|
|
87
|
+
),
|
|
88
|
+
(
|
|
89
|
+
"test_file_stereo_dat_128spp_square_bars_offset.png",
|
|
90
|
+
RenderOptions {
|
|
91
|
+
start_time: 0.5,
|
|
92
|
+
style: RenderStyle::Bars {
|
|
93
|
+
width: 8,
|
|
94
|
+
gap: 4,
|
|
95
|
+
style: audiowaveform::BarStyle::Square,
|
|
96
|
+
},
|
|
97
|
+
..RenderOptions::default()
|
|
98
|
+
},
|
|
99
|
+
&waveform,
|
|
100
|
+
),
|
|
101
|
+
(
|
|
102
|
+
"test_file_2channel_8bit_64spp_wav.png",
|
|
103
|
+
RenderOptions::default(),
|
|
104
|
+
&two_channel,
|
|
105
|
+
),
|
|
106
|
+
(
|
|
107
|
+
"test_file_2channel_8bit_64spp_wav_bars.png",
|
|
108
|
+
RenderOptions {
|
|
109
|
+
style: RenderStyle::Bars {
|
|
110
|
+
width: 8,
|
|
111
|
+
gap: 4,
|
|
112
|
+
style: audiowaveform::BarStyle::Square,
|
|
113
|
+
},
|
|
114
|
+
..RenderOptions::default()
|
|
115
|
+
},
|
|
116
|
+
&two_channel,
|
|
117
|
+
),
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
for (fixture, options, source) in cases {
|
|
121
|
+
let image = render_waveform(source, &options).expect("render image");
|
|
122
|
+
assert_png_image_matches_fixture(&image, fixture);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
#[test]
|
|
127
|
+
fn renders_waveforms_with_custom_colors() {
|
|
128
|
+
let waveform = load_waveform("test_file_stereo_8bit_64spp_wav.dat")
|
|
129
|
+
.resample(ScaleSpec::SamplesPerPixel(128))
|
|
130
|
+
.expect("resample waveform");
|
|
131
|
+
let audition = render_waveform(
|
|
132
|
+
&waveform,
|
|
133
|
+
&RenderOptions {
|
|
134
|
+
colors: ColorScheme::Audition.palette(),
|
|
135
|
+
..RenderOptions::default()
|
|
136
|
+
},
|
|
137
|
+
)
|
|
138
|
+
.expect("render audition waveform");
|
|
139
|
+
assert_png_image_matches_fixture(&audition, "test_file_stereo_dat_128spp_audition.png");
|
|
140
|
+
|
|
141
|
+
let custom = render_waveform(
|
|
142
|
+
&waveform,
|
|
143
|
+
&RenderOptions {
|
|
144
|
+
colors: WaveformColors {
|
|
145
|
+
border: Color::opaque(255, 0, 0),
|
|
146
|
+
background: Color::rgba(0, 255, 0, 128),
|
|
147
|
+
waveform: vec![Color::opaque(0, 0, 255)],
|
|
148
|
+
axis_label: Color::opaque(255, 255, 255),
|
|
149
|
+
},
|
|
150
|
+
..RenderOptions::default()
|
|
151
|
+
},
|
|
152
|
+
)
|
|
153
|
+
.expect("render custom waveform");
|
|
154
|
+
assert_png_image_matches_fixture(&custom, "test_file_stereo_dat_128spp_colors.png");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
#[cfg(feature = "format-wav")]
|
|
158
|
+
#[test]
|
|
159
|
+
fn renders_audio_pipeline_waveform_images() {
|
|
160
|
+
let stereo = generate_waveform_from_path(
|
|
161
|
+
support::fixture_path("test_file_stereo.wav"),
|
|
162
|
+
&GenerateOptions {
|
|
163
|
+
scale: ScaleSpec::SamplesPerPixel(128),
|
|
164
|
+
split_channels: false,
|
|
165
|
+
amplitude_scale: None,
|
|
166
|
+
},
|
|
167
|
+
)
|
|
168
|
+
.expect("generate stereo waveform");
|
|
169
|
+
let stereo_image = render_waveform(&stereo, &RenderOptions::default()).expect("render stereo");
|
|
170
|
+
assert_png_image_matches_fixture(&stereo_image, "test_file_stereo_wav_128spp.png");
|
|
171
|
+
|
|
172
|
+
let split = generate_waveform_from_path(
|
|
173
|
+
support::fixture_path("test_file_stereo.wav"),
|
|
174
|
+
&GenerateOptions {
|
|
175
|
+
scale: ScaleSpec::SamplesPerPixel(128),
|
|
176
|
+
split_channels: true,
|
|
177
|
+
amplitude_scale: None,
|
|
178
|
+
},
|
|
179
|
+
)
|
|
180
|
+
.expect("generate split waveform");
|
|
181
|
+
let split_image = render_waveform(
|
|
182
|
+
&split,
|
|
183
|
+
&RenderOptions {
|
|
184
|
+
colors: WaveformColors {
|
|
185
|
+
border: Color::opaque(255, 0, 0),
|
|
186
|
+
background: Color::rgba(0, 255, 0, 128),
|
|
187
|
+
waveform: vec![Color::opaque(0, 0, 255), Color::opaque(0, 0, 128)],
|
|
188
|
+
axis_label: Color::opaque(255, 255, 255),
|
|
189
|
+
},
|
|
190
|
+
..RenderOptions::default()
|
|
191
|
+
},
|
|
192
|
+
)
|
|
193
|
+
.expect("render split waveform");
|
|
194
|
+
assert_png_image_matches_fixture(&split_image, "test_file_stereo_wav_split_channels.png");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
#[cfg(all(feature = "format-wav", feature = "format-mp3"))]
|
|
198
|
+
#[test]
|
|
199
|
+
fn renders_fit_width_waveform_images_from_audio() {
|
|
200
|
+
let wav = generate_waveform_from_path(
|
|
201
|
+
support::fixture_path("test_file_stereo.wav"),
|
|
202
|
+
&GenerateOptions {
|
|
203
|
+
scale: ScaleSpec::FitWidth {
|
|
204
|
+
width_pixels: 500,
|
|
205
|
+
time_range: None,
|
|
206
|
+
},
|
|
207
|
+
split_channels: false,
|
|
208
|
+
amplitude_scale: None,
|
|
209
|
+
},
|
|
210
|
+
)
|
|
211
|
+
.expect("generate fit-width wav");
|
|
212
|
+
let wav_image = render_waveform(
|
|
213
|
+
&wav,
|
|
214
|
+
&RenderOptions {
|
|
215
|
+
width: 500,
|
|
216
|
+
height: 150,
|
|
217
|
+
..RenderOptions::default()
|
|
218
|
+
},
|
|
219
|
+
)
|
|
220
|
+
.expect("render fit-width wav");
|
|
221
|
+
assert_png_image_matches_fixture(&wav_image, "test_file_stereo_wav_500.png");
|
|
222
|
+
|
|
223
|
+
let mp3 = generate_waveform_from_path(
|
|
224
|
+
support::fixture_path("test_file_stereo.mp3"),
|
|
225
|
+
&GenerateOptions {
|
|
226
|
+
scale: ScaleSpec::FitWidth {
|
|
227
|
+
width_pixels: 500,
|
|
228
|
+
time_range: None,
|
|
229
|
+
},
|
|
230
|
+
split_channels: false,
|
|
231
|
+
amplitude_scale: None,
|
|
232
|
+
},
|
|
233
|
+
)
|
|
234
|
+
.expect("generate fit-width mp3");
|
|
235
|
+
let mp3_image = render_waveform(
|
|
236
|
+
&mp3,
|
|
237
|
+
&RenderOptions {
|
|
238
|
+
width: 500,
|
|
239
|
+
height: 150,
|
|
240
|
+
..RenderOptions::default()
|
|
241
|
+
},
|
|
242
|
+
)
|
|
243
|
+
.expect("render fit-width mp3");
|
|
244
|
+
assert_png_image_matches_fixture(&mp3_image, "test_file_stereo_mp3_500.png");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
#[test]
|
|
248
|
+
fn renders_reference_images_for_amplitude_scale_fixtures() {
|
|
249
|
+
let single = load_waveform("test_file_image_amplitude_scale_1channel.dat")
|
|
250
|
+
.resample(ScaleSpec::SamplesPerPixel(64))
|
|
251
|
+
.expect("resample single");
|
|
252
|
+
let single_image = render_waveform(&single, &RenderOptions::default()).expect("render single");
|
|
253
|
+
assert_png_image_matches_fixture(
|
|
254
|
+
&single_image,
|
|
255
|
+
"test_file_image_amplitude_scale_1channel.png",
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
let dual = load_waveform("test_file_image_amplitude_scale_2channel.dat")
|
|
259
|
+
.resample(ScaleSpec::SamplesPerPixel(64))
|
|
260
|
+
.expect("resample dual");
|
|
261
|
+
let dual_image = render_waveform(&dual, &RenderOptions::default()).expect("render dual");
|
|
262
|
+
assert_png_image_matches_fixture(&dual_image, "test_file_image_amplitude_scale_2channel.png");
|
|
263
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#![allow(dead_code)]
|
|
2
|
+
|
|
3
|
+
use std::fs;
|
|
4
|
+
use std::path::{Path, PathBuf};
|
|
5
|
+
|
|
6
|
+
use audiowaveform::Waveform;
|
|
7
|
+
#[cfg(feature = "wav-output")]
|
|
8
|
+
use hound::WavReader;
|
|
9
|
+
use image::{RgbaImage, load_from_memory};
|
|
10
|
+
use tempfile::{Builder, NamedTempFile};
|
|
11
|
+
|
|
12
|
+
pub fn fixture_path(name: &str) -> PathBuf {
|
|
13
|
+
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
|
14
|
+
.join("../../fixtures")
|
|
15
|
+
.join(name)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
pub fn read_fixture(name: &str) -> Vec<u8> {
|
|
19
|
+
fs::read(fixture_path(name)).expect("fixture bytes")
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
pub fn load_waveform(name: &str) -> Waveform {
|
|
23
|
+
Waveform::load_from_path(fixture_path(name), None).expect("load waveform fixture")
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
pub fn named_temp_file(suffix: &str) -> NamedTempFile {
|
|
27
|
+
Builder::new()
|
|
28
|
+
.suffix(suffix)
|
|
29
|
+
.tempfile()
|
|
30
|
+
.expect("create temp file")
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
pub fn assert_bytes_eq(actual: &[u8], fixture: &str) {
|
|
34
|
+
assert_eq!(actual, read_fixture(fixture), "fixture mismatch: {fixture}");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
pub fn assert_file_bytes_eq(actual: impl AsRef<Path>, fixture: &str) {
|
|
38
|
+
let actual = fs::read(actual).expect("read output file");
|
|
39
|
+
assert_bytes_eq(&actual, fixture);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
#[cfg(feature = "wav-output")]
|
|
43
|
+
pub fn assert_wav_file_matches_fixture(
|
|
44
|
+
actual: impl AsRef<Path>,
|
|
45
|
+
fixture: &str,
|
|
46
|
+
sample_tolerance: i16,
|
|
47
|
+
) {
|
|
48
|
+
let actual = WavReader::open(actual).expect("open actual wav");
|
|
49
|
+
let expected = WavReader::open(fixture_path(fixture)).expect("open expected wav");
|
|
50
|
+
assert_wav_eq(actual, expected, fixture, sample_tolerance);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
pub fn assert_png_bytes_match_fixture(actual: &[u8], fixture: &str) {
|
|
54
|
+
let actual = load_from_memory(actual)
|
|
55
|
+
.expect("decode actual png")
|
|
56
|
+
.into_rgba8();
|
|
57
|
+
assert_png_image_matches_fixture(&actual, fixture);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
pub fn assert_png_file_matches_fixture(actual: impl AsRef<Path>, fixture: &str) {
|
|
61
|
+
let actual = image::open(actual).expect("open actual png").into_rgba8();
|
|
62
|
+
assert_png_image_matches_fixture(&actual, fixture);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
pub fn assert_png_image_matches_fixture(actual: &RgbaImage, fixture: &str) {
|
|
66
|
+
let expected = image::open(fixture_path(fixture))
|
|
67
|
+
.expect("open expected png")
|
|
68
|
+
.into_rgba8();
|
|
69
|
+
assert_eq!(
|
|
70
|
+
actual.dimensions(),
|
|
71
|
+
expected.dimensions(),
|
|
72
|
+
"png dimensions mismatch for {fixture}"
|
|
73
|
+
);
|
|
74
|
+
assert_eq!(
|
|
75
|
+
actual.as_raw(),
|
|
76
|
+
expected.as_raw(),
|
|
77
|
+
"png pixels mismatch for {fixture}"
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
#[cfg(feature = "wav-output")]
|
|
82
|
+
fn assert_wav_eq<R1: std::io::Read, R2: std::io::Read>(
|
|
83
|
+
mut actual: WavReader<R1>,
|
|
84
|
+
mut expected: WavReader<R2>,
|
|
85
|
+
fixture: &str,
|
|
86
|
+
sample_tolerance: i16,
|
|
87
|
+
) {
|
|
88
|
+
assert_eq!(
|
|
89
|
+
actual.spec(),
|
|
90
|
+
expected.spec(),
|
|
91
|
+
"wav spec mismatch for {fixture}"
|
|
92
|
+
);
|
|
93
|
+
assert_eq!(
|
|
94
|
+
actual.duration(),
|
|
95
|
+
expected.duration(),
|
|
96
|
+
"wav duration mismatch for {fixture}"
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
let actual_samples = actual
|
|
100
|
+
.samples::<i16>()
|
|
101
|
+
.collect::<Result<Vec<_>, _>>()
|
|
102
|
+
.expect("read actual wav samples");
|
|
103
|
+
let expected_samples = expected
|
|
104
|
+
.samples::<i16>()
|
|
105
|
+
.collect::<Result<Vec<_>, _>>()
|
|
106
|
+
.expect("read expected wav samples");
|
|
107
|
+
|
|
108
|
+
assert_eq!(
|
|
109
|
+
actual_samples.len(),
|
|
110
|
+
expected_samples.len(),
|
|
111
|
+
"wav sample count mismatch for {fixture}"
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
for (index, (actual, expected)) in actual_samples
|
|
115
|
+
.iter()
|
|
116
|
+
.zip(expected_samples.iter())
|
|
117
|
+
.enumerate()
|
|
118
|
+
{
|
|
119
|
+
let difference = i32::from(*actual) - i32::from(*expected);
|
|
120
|
+
assert!(
|
|
121
|
+
difference.abs() <= i32::from(sample_tolerance),
|
|
122
|
+
"wav sample mismatch for {fixture} at index {index}: actual={actual}, expected={expected}, tolerance={sample_tolerance}"
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#![cfg(feature = "wav-output")]
|
|
2
|
+
|
|
3
|
+
mod support;
|
|
4
|
+
|
|
5
|
+
use std::io::Cursor;
|
|
6
|
+
|
|
7
|
+
use audiowaveform::{PcmAudio, write_pcm_as_wav};
|
|
8
|
+
|
|
9
|
+
#[cfg(feature = "format-mp3")]
|
|
10
|
+
use self::support::{assert_wav_file_matches_fixture, fixture_path, named_temp_file};
|
|
11
|
+
|
|
12
|
+
#[test]
|
|
13
|
+
fn writes_empty_wav_header_for_empty_pcm() {
|
|
14
|
+
let pcm = PcmAudio::new(44_100, 1, Vec::new()).expect("pcm");
|
|
15
|
+
let mut wav = Vec::new();
|
|
16
|
+
write_pcm_as_wav(&pcm, &mut wav).expect("write wav");
|
|
17
|
+
|
|
18
|
+
assert_eq!(wav.len(), 44);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
#[test]
|
|
22
|
+
fn writes_expected_pcm_wav_structure() {
|
|
23
|
+
let pcm = PcmAudio::new(44_100, 1, vec![0; 1024]).expect("pcm");
|
|
24
|
+
let mut wav = Vec::new();
|
|
25
|
+
write_pcm_as_wav(&pcm, &mut wav).expect("write wav");
|
|
26
|
+
|
|
27
|
+
assert_eq!(wav.len(), 44 + 1024 * 2);
|
|
28
|
+
|
|
29
|
+
let mut reader = hound::WavReader::new(Cursor::new(wav)).expect("open wav");
|
|
30
|
+
assert_eq!(reader.spec().channels, 1);
|
|
31
|
+
assert_eq!(reader.spec().sample_rate, 44_100);
|
|
32
|
+
assert_eq!(reader.spec().bits_per_sample, 16);
|
|
33
|
+
assert_eq!(reader.duration(), 1024);
|
|
34
|
+
assert!(
|
|
35
|
+
reader
|
|
36
|
+
.samples::<i16>()
|
|
37
|
+
.all(|sample| sample.expect("sample") == 0)
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
#[cfg(feature = "format-mp3")]
|
|
42
|
+
#[test]
|
|
43
|
+
fn transcodes_audio_to_expected_wav_fixture() {
|
|
44
|
+
let output = named_temp_file(".wav");
|
|
45
|
+
audiowaveform::transcode_audio_path_to_wav_path(
|
|
46
|
+
fixture_path("test_file_mono.mp3"),
|
|
47
|
+
output.path(),
|
|
48
|
+
)
|
|
49
|
+
.expect("transcode wav");
|
|
50
|
+
|
|
51
|
+
// MP3 decode can differ by a least-significant bit across platforms, so
|
|
52
|
+
// compare the decoded PCM stream semantically instead of byte-for-byte.
|
|
53
|
+
assert_wav_file_matches_fixture(output.path(), "test_file_mono_converted.wav", 1);
|
|
54
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
mod support;
|
|
2
|
+
|
|
3
|
+
use audiowaveform::{Waveform, WaveformFormat};
|
|
4
|
+
use byteorder::{LittleEndian, WriteBytesExt};
|
|
5
|
+
|
|
6
|
+
use self::support::{assert_bytes_eq, fixture_path, load_waveform, named_temp_file};
|
|
7
|
+
|
|
8
|
+
#[test]
|
|
9
|
+
fn round_trips_binary_waveform_fixture() {
|
|
10
|
+
let fixture = fixture_path("test_file_stereo_8bit_64spp_wav.dat");
|
|
11
|
+
let waveform = Waveform::load_from_path(&fixture, None).expect("load waveform");
|
|
12
|
+
|
|
13
|
+
assert_eq!(waveform.sample_rate(), 16_000);
|
|
14
|
+
assert_eq!(waveform.samples_per_pixel(), 64);
|
|
15
|
+
assert_eq!(waveform.channels(), 1);
|
|
16
|
+
assert_eq!(waveform.storage_bits(), 8);
|
|
17
|
+
assert_eq!(waveform.len(), 1774);
|
|
18
|
+
|
|
19
|
+
let mut output = Vec::new();
|
|
20
|
+
waveform
|
|
21
|
+
.write_to_writer(&mut output, WaveformFormat::Dat, None)
|
|
22
|
+
.expect("write dat");
|
|
23
|
+
|
|
24
|
+
assert_bytes_eq(&output, "test_file_stereo_8bit_64spp_wav.dat");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
#[test]
|
|
28
|
+
fn emits_expected_json_and_text_fixture_bytes() {
|
|
29
|
+
let fixture = fixture_path("test_file_stereo_8bit_64spp_wav.dat");
|
|
30
|
+
let waveform = Waveform::load_from_path(&fixture, None).expect("load waveform");
|
|
31
|
+
|
|
32
|
+
let mut json = Vec::new();
|
|
33
|
+
waveform
|
|
34
|
+
.write_to_writer(&mut json, WaveformFormat::Json, Some(8))
|
|
35
|
+
.expect("write json");
|
|
36
|
+
assert_bytes_eq(&json, "test_file_stereo_8bit_64spp_wav.json");
|
|
37
|
+
|
|
38
|
+
let mut txt = Vec::new();
|
|
39
|
+
waveform
|
|
40
|
+
.write_to_writer(&mut txt, WaveformFormat::Txt, Some(8))
|
|
41
|
+
.expect("write txt");
|
|
42
|
+
assert_bytes_eq(&txt, "test_file_stereo_8bit_64spp_wav.txt");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
#[test]
|
|
46
|
+
fn loads_version1_and_version2_waveform_fixtures() {
|
|
47
|
+
let cases = [
|
|
48
|
+
(
|
|
49
|
+
"test_file_stereo_8bit_64spp_wav.dat",
|
|
50
|
+
8_u8,
|
|
51
|
+
1_u16,
|
|
52
|
+
1774_usize,
|
|
53
|
+
),
|
|
54
|
+
(
|
|
55
|
+
"test_file_stereo_8bit_64spp_wav_v2.dat",
|
|
56
|
+
8_u8,
|
|
57
|
+
1_u16,
|
|
58
|
+
1774_usize,
|
|
59
|
+
),
|
|
60
|
+
(
|
|
61
|
+
"test_file_stereo_16bit_64spp_wav.dat",
|
|
62
|
+
16_u8,
|
|
63
|
+
1_u16,
|
|
64
|
+
1774_usize,
|
|
65
|
+
),
|
|
66
|
+
(
|
|
67
|
+
"test_file_stereo_16bit_64spp_wav_v2.dat",
|
|
68
|
+
16_u8,
|
|
69
|
+
1_u16,
|
|
70
|
+
1774_usize,
|
|
71
|
+
),
|
|
72
|
+
("07023003_8bit_64spp_2channel.dat", 8_u8, 2_u16, 513_usize),
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
for (fixture, bits, channels, length) in cases {
|
|
76
|
+
let waveform = load_waveform(fixture);
|
|
77
|
+
let sample_rate = if fixture == "07023003_8bit_64spp_2channel.dat" {
|
|
78
|
+
44_100
|
|
79
|
+
} else {
|
|
80
|
+
16_000
|
|
81
|
+
};
|
|
82
|
+
assert_eq!(waveform.sample_rate(), sample_rate, "{fixture}");
|
|
83
|
+
assert_eq!(waveform.samples_per_pixel(), 64, "{fixture}");
|
|
84
|
+
assert_eq!(waveform.storage_bits(), bits, "{fixture}");
|
|
85
|
+
assert_eq!(waveform.channels(), channels, "{fixture}");
|
|
86
|
+
let expected_length = if fixture == "07023003_8bit_64spp_2channel.dat" {
|
|
87
|
+
10_438
|
|
88
|
+
} else {
|
|
89
|
+
length
|
|
90
|
+
};
|
|
91
|
+
assert_eq!(waveform.len(), expected_length, "{fixture}");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
#[test]
|
|
96
|
+
fn loads_zero_length_waveform_fixture() {
|
|
97
|
+
let waveform = load_waveform("zero_length.dat");
|
|
98
|
+
assert_eq!(waveform.sample_rate(), 16_000);
|
|
99
|
+
assert_eq!(waveform.samples_per_pixel(), 64);
|
|
100
|
+
assert!(waveform.is_empty());
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
#[test]
|
|
104
|
+
fn loads_truncated_waveform_fixture_using_available_points() {
|
|
105
|
+
let waveform = load_waveform("size_mismatch.dat");
|
|
106
|
+
assert_eq!(waveform.sample_rate(), 16_000);
|
|
107
|
+
assert_eq!(waveform.samples_per_pixel(), 64);
|
|
108
|
+
assert_eq!(waveform.channels(), 1);
|
|
109
|
+
assert_eq!(waveform.storage_bits(), 8);
|
|
110
|
+
assert_eq!(waveform.len(), 1_800);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
#[test]
|
|
114
|
+
fn rejects_invalid_waveform_fixtures() {
|
|
115
|
+
let cases = [
|
|
116
|
+
("version3.dat", "Cannot load data file version: 3"),
|
|
117
|
+
(
|
|
118
|
+
"sample_rate_too_low.dat",
|
|
119
|
+
"Invalid sample rate: minimum 1 Hz",
|
|
120
|
+
),
|
|
121
|
+
(
|
|
122
|
+
"samples_per_pixel_too_low.dat",
|
|
123
|
+
"Invalid samples per pixel: minimum 2",
|
|
124
|
+
),
|
|
125
|
+
(
|
|
126
|
+
"too_many_channels.dat",
|
|
127
|
+
"Invalid channels: must be between 1 and 24",
|
|
128
|
+
),
|
|
129
|
+
(
|
|
130
|
+
"not_enough_channels.dat",
|
|
131
|
+
"Invalid channels: must be between 1 and 24",
|
|
132
|
+
),
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
for (fixture, expected) in cases {
|
|
136
|
+
let error = Waveform::load_from_path(fixture_path(fixture), None).expect_err("invalid dat");
|
|
137
|
+
assert!(error.to_string().contains(expected), "{fixture}: {}", error);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
#[test]
|
|
142
|
+
fn rejects_dat_channel_counts_that_do_not_fit_the_internal_type() {
|
|
143
|
+
let mut dat = Vec::new();
|
|
144
|
+
dat.write_i32::<LittleEndian>(2).expect("version");
|
|
145
|
+
dat.write_u32::<LittleEndian>(0).expect("flags");
|
|
146
|
+
dat.write_u32::<LittleEndian>(44_100).expect("sample rate");
|
|
147
|
+
dat.write_u32::<LittleEndian>(256)
|
|
148
|
+
.expect("samples per pixel");
|
|
149
|
+
dat.write_u32::<LittleEndian>(0).expect("length");
|
|
150
|
+
dat.write_i32::<LittleEndian>(65_537).expect("channels");
|
|
151
|
+
|
|
152
|
+
let error = Waveform::load_from_reader(dat.as_slice(), WaveformFormat::Dat)
|
|
153
|
+
.expect_err("out-of-range channel count");
|
|
154
|
+
assert_eq!(
|
|
155
|
+
error.to_string(),
|
|
156
|
+
"Invalid channels: must be between 1 and 24"
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
#[test]
|
|
161
|
+
fn rejects_invalid_json_waveform_payloads() {
|
|
162
|
+
let error = Waveform::load_from_reader(
|
|
163
|
+
br#"{"version":2,"channels":1,"sample_rate":44100,"samples_per_pixel":256,"bits":8,"length":2,"data":[1,2,3]}"#
|
|
164
|
+
.as_slice(),
|
|
165
|
+
WaveformFormat::Json,
|
|
166
|
+
)
|
|
167
|
+
.expect_err("json length mismatch");
|
|
168
|
+
assert_eq!(
|
|
169
|
+
error.to_string(),
|
|
170
|
+
"Length mismatch: expected 4 values, found 3"
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
let error = Waveform::load_from_reader(
|
|
174
|
+
br#"{"version":2,"channels":1,"sample_rate":44100,"samples_per_pixel":256,"bits":8,"length":1,"data":[999,0]}"#
|
|
175
|
+
.as_slice(),
|
|
176
|
+
WaveformFormat::Json,
|
|
177
|
+
)
|
|
178
|
+
.expect_err("json range mismatch");
|
|
179
|
+
assert_eq!(error.to_string(), "Data value out of range: 999");
|
|
180
|
+
|
|
181
|
+
let overflowing_length = format!(
|
|
182
|
+
r#"{{"version":2,"channels":1,"sample_rate":44100,"samples_per_pixel":256,"bits":8,"length":{},"data":[]}}"#,
|
|
183
|
+
usize::MAX
|
|
184
|
+
);
|
|
185
|
+
let error = Waveform::load_from_reader(overflowing_length.as_bytes(), WaveformFormat::Json)
|
|
186
|
+
.expect_err("overflowing json length");
|
|
187
|
+
assert_eq!(error.to_string(), "Waveform length is too large");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
#[test]
|
|
191
|
+
fn saves_empty_waveform_as_header_only_dat_file() {
|
|
192
|
+
let waveform = Waveform::new(44_100, 256, 1).expect("new waveform");
|
|
193
|
+
let mut dat = Vec::new();
|
|
194
|
+
waveform
|
|
195
|
+
.write_to_writer(&mut dat, WaveformFormat::Dat, None)
|
|
196
|
+
.expect("write dat");
|
|
197
|
+
assert_eq!(dat.len(), 20);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
#[test]
|
|
201
|
+
fn writes_expected_text_and_json_for_two_channel_waveforms() {
|
|
202
|
+
let waveform = load_waveform("07023003_8bit_64spp_2channel.dat");
|
|
203
|
+
|
|
204
|
+
let mut json = Vec::new();
|
|
205
|
+
waveform
|
|
206
|
+
.write_to_writer(&mut json, WaveformFormat::Json, Some(8))
|
|
207
|
+
.expect("write json");
|
|
208
|
+
assert_bytes_eq(&json, "07023003_8bit_64spp_2channel.json");
|
|
209
|
+
|
|
210
|
+
let mut txt = Vec::new();
|
|
211
|
+
waveform
|
|
212
|
+
.write_to_writer(&mut txt, WaveformFormat::Txt, Some(8))
|
|
213
|
+
.expect("write txt");
|
|
214
|
+
assert_bytes_eq(&txt, "07023003_8bit_64spp_2channel.txt");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
#[test]
|
|
218
|
+
fn saves_waveform_to_path_and_reloads_it() {
|
|
219
|
+
let waveform = load_waveform("test_file_stereo_8bit_64spp_wav.dat");
|
|
220
|
+
let file = named_temp_file(".dat");
|
|
221
|
+
waveform
|
|
222
|
+
.save_to_path(file.path(), Some(WaveformFormat::Dat))
|
|
223
|
+
.expect("save waveform");
|
|
224
|
+
|
|
225
|
+
let reloaded =
|
|
226
|
+
Waveform::load_from_path(file.path(), Some(WaveformFormat::Dat)).expect("reload waveform");
|
|
227
|
+
assert_eq!(reloaded, waveform);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
#[test]
|
|
231
|
+
fn rejects_invalid_storage_bits_and_txt_loading() {
|
|
232
|
+
let waveform = load_waveform("test_file_stereo_8bit_64spp_wav.dat");
|
|
233
|
+
|
|
234
|
+
let error = waveform
|
|
235
|
+
.write_to_writer(Vec::new(), WaveformFormat::Dat, Some(10))
|
|
236
|
+
.expect_err("invalid bits");
|
|
237
|
+
assert_eq!(error.to_string(), "Invalid bits: must be either 8 or 16");
|
|
238
|
+
|
|
239
|
+
let error = Waveform::load_from_reader(b"-1,1\n".as_slice(), WaveformFormat::Txt)
|
|
240
|
+
.expect_err("txt input unsupported");
|
|
241
|
+
assert_eq!(error.to_string(), "Unsupported format: txt");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
#[test]
|
|
245
|
+
fn rescales_waveforms_to_a_coarser_scale() {
|
|
246
|
+
let waveform = load_waveform("test_file_stereo_8bit_64spp_wav.dat");
|
|
247
|
+
let resampled = waveform
|
|
248
|
+
.resample(audiowaveform::ScaleSpec::SamplesPerPixel(128))
|
|
249
|
+
.expect("resample");
|
|
250
|
+
|
|
251
|
+
assert_eq!(resampled.sample_rate(), 16_000);
|
|
252
|
+
assert_eq!(resampled.samples_per_pixel(), 128);
|
|
253
|
+
assert_eq!(resampled.channels(), 1);
|
|
254
|
+
assert_eq!(resampled.len(), 887);
|
|
255
|
+
}
|