216 Commits

Author SHA1 Message Date
Jamie Hardt
8e341990fa Rustfmt 2024-12-10 17:41:50 -08:00
Jamie Hardt
2dfddff0b5 Update Cargo.toml
Nudge version
2023-06-02 21:04:12 -07:00
Jamie Hardt
aa8365a38d Documentation 2023-06-02 20:37:44 -07:00
Jamie Hardt
b8a428e757 Documentation and exposure
Documentation in the fmt module has been improved.

ReadWaveData trait is now public.

The
2023-06-02 11:06:53 -07:00
Jamie Hardt
c1d2b2c836 Documentation fixes. Made a trait public. 2023-06-02 09:53:23 -07:00
Jamie Hardt
f41b7ea575 Update lib.rs 2023-06-02 08:53:27 -07:00
Jamie Hardt
9a62bdc375 Update issue templates 2023-06-02 08:47:38 -07:00
Jamie Hardt
368ef4366d Delete bwavefile.code-workspace 2023-06-02 08:42:55 -07:00
Jamie Hardt
bfa51a4e4c Update issue templates 2023-06-02 08:42:27 -07:00
Jamie Hardt
4270dc9866 Merge pull request #16 from iluvcapra/release
Committing lock file
2023-05-26 22:31:27 -07:00
Jamie Hardt
92d76289e4 Update README.md 2023-05-26 12:29:28 -07:00
Jamie Hardt
15f9a240c0 Committing lock file 2023-05-22 18:04:53 -07:00
Jamie Hardt
da2e6f61ee Merge branch 'master' of https://github.com/iluvcapra/bwavfile 2023-05-22 17:53:44 -07:00
Jamie Hardt
eff4fe199f Update Cargo.toml
Bumping version 2.0.0
2023-05-22 17:45:28 -07:00
Jamie Hardt
4e84756417 Merge pull request #14 from irh/performance-improvements
Performance improvements
2023-05-22 17:44:43 -07:00
Ian Hobson
5592e87f0e Update Cargo.toml author list 2023-05-19 11:05:22 +02:00
Ian Hobson
72fea1a493 Expose and rename the supported wave tags and UUIDs 2023-05-19 10:57:39 +02:00
Ian Hobson
bd09ed207a Re-use the write buffer between calls to write_frames
This resulted in a ~10% speedup in test runs of the deinter example.
2023-05-19 10:57:39 +02:00
Ian Hobson
3d1826007e Avoid panics when incorrect buffer sizes are provided to read/write_frames 2023-05-19 10:57:39 +02:00
Ian Hobson
3400778991 Introduce generic read_frames and write_frames functions 2023-05-19 10:57:39 +02:00
Ian Hobson
7290d5ec81 Rework the deinter example, create writers before reading through the input file 2023-05-19 10:57:39 +02:00
Ian Hobson
0074e13bff Avoid flushing output on each write 2023-05-19 10:57:39 +02:00
Ian Hobson
60a3eac072 clippy: Collapse nested else/if into else if 2023-05-19 10:57:39 +02:00
Ian Hobson
0c46a0e21f clippy: Remove unnecessary import 2023-05-19 10:57:39 +02:00
Ian Hobson
8d34a517fc clippy: Don't warn about the LUFS type name 2023-05-19 10:57:39 +02:00
Ian Hobson
86ffd4310a clippy: Prefer if let Some(_) over Option::map 2023-05-19 10:57:39 +02:00
Ian Hobson
ceb8c4371e clippy: Avoid unnecessary conversion 2023-05-19 10:57:39 +02:00
Ian Hobson
22e8dc79d1 clippy: Use a slice argument instead of the owned equivalent 2023-05-19 10:57:39 +02:00
Ian Hobson
f6f4869b5b clippy: Avoid () as return type 2023-05-19 10:57:39 +02:00
Ian Hobson
cf5ec121da clippy: Avoid unnecessary closures 2023-05-19 10:57:39 +02:00
Ian Hobson
7c01778029 clippy: Use .write_u8() when writing a single value 2023-05-19 10:57:39 +02:00
Ian Hobson
4e370c57c1 clippy: Use .write_all() when writing a slice 2023-05-19 10:57:39 +02:00
Ian Hobson
fc3b411717 clippy: Use .read_exact() when reading a fixed number of bytes 2023-05-19 10:57:39 +02:00
Ian Hobson
4d81ef36cd clippy: Use the += operator 2023-05-19 10:57:39 +02:00
Ian Hobson
651009c96a clippy: Avoid making unnecessary references 2023-05-19 10:57:39 +02:00
Ian Hobson
566ad07247 clippy: Remove unnecessary use of .into() 2023-05-19 10:57:39 +02:00
Ian Hobson
6989520743 clippy: Use Vec::get() instead of iter().nth() 2023-05-19 10:57:39 +02:00
Ian Hobson
b6c20ae410 clippy: Remove unnecessary casts 2023-05-19 10:57:39 +02:00
Ian Hobson
9eb655cd27 clippy: Remove unnecessary use of return 2023-05-19 10:57:39 +02:00
Ian Hobson
bbd84b7bbb clippy: Remove unnecessary uses of ? 2023-05-19 10:57:39 +02:00
Ian Hobson
04d282ccd6 clippy: Use Avoid unnecessary indexing in for loops 2023-05-19 10:57:39 +02:00
Ian Hobson
1234c897a3 clippy: Use stream_position() to get current position 2023-05-19 10:57:39 +02:00
Ian Hobson
313f5408ca Update the comment in wave-deinter.rs 2023-05-19 10:57:38 +02:00
Ian Hobson
e1b93b24ad Run cargo format 2023-05-19 10:57:18 +02:00
Ian Hobson
369d261ba0 Fix build warnings 2023-05-19 10:57:18 +02:00
Jamie Hardt
72d3c4abef Tweaked documentation 2023-05-18 10:41:18 -07:00
Jamie Hardt
6191adf30a Removed dead comment 2023-05-18 10:19:46 -07:00
Jamie Hardt
a69755d8aa Fixed misspelling 2023-05-18 10:18:57 -07:00
Jamie Hardt
11d701b687 More readme tweak 2023-05-18 10:16:33 -07:00
Jamie Hardt
0fe11a48ee Reqording readme 2023-05-18 09:54:12 -07:00
Jamie Hardt
4877aa0458 Update rust.yml
Adding clippy
2023-05-18 09:23:15 -07:00
Jamie Hardt
b9fe3b9701 Merge branch 'master' of https://github.com/iluvcapra/bwavfile 2023-05-13 11:02:52 -07:00
Jamie Hardt
4925f95b68 Removed old src/wavebuffer.rs file 2023-05-13 11:02:02 -07:00
Jamie Hardt
f1ec1cf43a Update README.md
master branch not main branch!
2023-05-13 10:50:02 -07:00
Jamie Hardt
30b3e83fd4 Update README.md
Updated workflow shield
2023-05-13 10:43:17 -07:00
Jamie Hardt
e30d60ff0d Merge branch 'master' of https://github.com/Wuelle/bwavfile into Wuelle-master 2021-12-31 13:28:55 -08:00
Jamie Hardt
442394ad5e Removed dead-code warnings 2021-12-31 12:57:46 -08:00
Jamie Hardt
52c26156a9 Merge branch 'master' of https://github.com/iluvcapra/bwavfile 2021-12-31 12:56:11 -08:00
Jamie Hardt
4ddffa0b4a Read trait 2021-12-31 12:56:08 -08:00
Wuelle
9a010ca0c4 prettify code 2021-12-30 21:59:29 +01:00
Wuelle
5e563cddf8 allow more types to be used as paths 2021-12-30 21:59:17 +01:00
Jamie Hardt
69fd5f310c Fixed some typos in this 2021-09-28 13:49:04 -07:00
Jamie Hardt
a8bb5e8c8e Update to version 1.1 2021-09-28 13:46:34 -07:00
Jamie Hardt
468ed12304 Merge pull request #10 from atoav/cuepoints
Surface cue frame_offset field in UI and test for sounddevices files.
2021-09-28 13:39:09 -07:00
Jamie Hardt
a015d7cf8a Added sounddevices test WAV
And unit tests for cue metadata.
2021-09-28 13:18:07 -07:00
Jamie Hardt
e89e87045c Merge pull request #11 from EaterLabs/misc-fixes
implement basic f32le support and some other misc fixes
2021-09-28 12:30:45 -07:00
Jamie Hardt
ecc1db703d Update Cargo.lock 2021-09-28 12:27:53 -07:00
Jamie Hardt
4cd28c9efe Create wavebuffer.rs 2021-09-28 11:59:56 -07:00
eater
663b9fad43 derive Debug for ADMAudioID and ChannelDescriptor and implement f32
reading
2021-08-22 23:40:07 +02:00
eater
9fe741d913 implement std::error::Error for Error 2021-08-22 23:35:47 +02:00
David Huss
2ecae51e8d Adding sounddevices testfile for cue points
As described in issue #9 the function cue_points() didn't display the
expected frame values for cue points created on a Sound Devices
MixPre-Series recorder. The file added by this commit
(sounddevices_6_cue_points.wav) is short and mono channel, but has 6 cue
points in it.

The expected frames for the cue points are:
0: 90112
1: 176128
2: 237568
3: 294912
4: 380928
5: 385024
2021-08-17 09:08:07 +02:00
David Huss
56dc4d1048 Try fix for cuepoints 2021-08-16 17:03:57 +02:00
Jamie Hardt
1af4c675c7 Merge commit 'a625b485d993d912556cdf72605bf7b2c48c18c2' 2021-06-23 10:21:41 -07:00
Jamie Hardt
a625b485d9 Merge branch 'candidate-v1' into HEAD 2021-06-23 10:19:48 -07:00
Jamie Hardt
d17c6badfa Removed unused import 2021-06-23 10:18:44 -07:00
Jamie Hardt
bc6be79715 Merge commit '53fcecfc451f9d38e2b541a728ea444162d04125' into candidate-v1 2021-06-23 10:13:05 -07:00
Jamie Hardt
763e9a10a3 Merge commit 'eee3c3f62592f024dd1308edeedc1b98565bbcd3' into candidate-v1 2021-06-23 10:08:48 -07:00
Jamie Hardt
319845ae49 In progress format plumbing 2021-01-05 00:32:09 -08:00
Jamie Hardt
53fcecfc45 Nudge version 2021-01-04 19:13:47 -08:00
Jamie Hardt
6776820e16 Working, but very slow 2021-01-04 19:12:03 -08:00
Jamie Hardt
eee3c3f625 exmaple impo 2021-01-03 23:43:32 -08:00
Jamie Hardt
6375567af1 Update README.md 2021-01-03 18:41:37 -08:00
Jamie Hardt
1a169da48f removed inapplicable code 2021-01-03 13:27:35 -08:00
Jamie Hardt
50b7b7b640 Merge commit '72d6be406e75ca0c40c134311f558cbec9c9f356' into candidate-v1 2021-01-03 13:26:50 -08:00
Jamie Hardt
72d6be406e Twiddles 2021-01-03 13:20:04 -08:00
Jamie Hardt
814b54a6e8 Cue writing impl
Needs tests
2021-01-03 13:16:47 -08:00
Jamie Hardt
e826628096 FIXME comments 2021-01-03 13:16:36 -08:00
Jamie Hardt
0a307fbf05 Removed Cue.ident field from interface 2021-01-03 11:27:49 -08:00
Jamie Hardt
e941358c8d reorganized use 2021-01-03 11:05:02 -08:00
Jamie Hardt
c2cff2a893 Consolidated AudioFrameWriter 2021-01-03 10:49:05 -08:00
Jamie Hardt
94eb36bbb5 Merge branch 'master' into candidate-v1 2021-01-02 18:14:14 -08:00
Jamie Hardt
f2c53061dc Made a TODO note 2021-01-02 18:13:34 -08:00
Jamie Hardt
190e8bc677 Fixed bug in decibels code, was too loud 2021-01-02 18:09:39 -08:00
Jamie Hardt
80be74c8fb Features 2021-01-02 16:04:44 -08:00
Jamie Hardt
5057862258 Bump license year 2021-01-02 15:45:56 -08:00
Jamie Hardt
75fc40d638 Bump version and license year 2021-01-02 15:45:24 -08:00
Jamie Hardt
2780bfb31b Structuring Readme 2021-01-02 15:44:05 -08:00
Jamie Hardt
b160284e21 Reorganized imports 2021-01-02 15:16:38 -08:00
Jamie Hardt
8d55b126ae Examples interface
Notes on Bext
2021-01-02 13:38:56 -08:00
Jamie Hardt
b1c806598e Tweaked dependencies 2021-01-02 13:38:32 -08:00
Jamie Hardt
58e43902cd Updated README 2021-01-02 12:30:48 -08:00
Jamie Hardt
48517c22bc Merge branch 'master' of https://github.com/iluvcapra/bwavfile 2021-01-02 12:23:14 -08:00
Jamie Hardt
634c21dc9a Added buffered IO
And updated tests to reflect this
2021-01-02 12:23:08 -08:00
Jamie Hardt
b7f82a9d63 blits example impl is basically done 2021-01-02 12:00:47 -08:00
Jamie Hardt
3b88b95065 Update blits.rs 2021-01-02 09:25:17 -08:00
Jamie Hardt
a21e17ee78 Added todos 2021-01-01 23:20:39 -08:00
Jamie Hardt
e495ad704e Added CLI options 2021-01-01 23:13:17 -08:00
Jamie Hardt
bbab81746f Twiddle varname silence error 2021-01-01 22:37:01 -08:00
Jamie Hardt
ca1d5eb714 Goofing around with examples 2021-01-01 22:36:08 -08:00
Jamie Hardt
e652722250 Update dependencies
Added docopt
2021-01-01 22:04:00 -08:00
Jamie Hardt
d0b4582dca More docs, example ideas 2021-01-01 21:59:53 -08:00
Jamie Hardt
4a0a9b7ec1 Update blits.rs
TODO note
2021-01-01 21:51:58 -08:00
Jamie Hardt
a851ed7a7f Blits file generator implementation 2021-01-01 21:49:42 -08:00
Jamie Hardt
fc21173312 Renamed example 2021-01-01 21:32:01 -08:00
Jamie Hardt
350599411f Fixed WaveFmt writing for extended 2021-01-01 21:31:44 -08:00
Jamie Hardt
ec462a0b96 Merge remote-tracking branch 'origin/release' 2021-01-01 12:42:51 -08:00
Jamie Hardt
f0b1c51bd8 Merge branch 'master' into release 2021-01-01 12:26:50 -08:00
Jamie Hardt
e14bcd8c76 Update version 2021-01-01 12:26:16 -08:00
Jamie Hardt
1cb7174861 Comment 2021-01-01 12:14:51 -08:00
Jamie Hardt
a855410d6f Added rf64 test back in
...and will push to github see if the action completes
2021-01-01 12:14:06 -08:00
Jamie Hardt
28b9272456 Updated doucmentation to proper comment style 2021-01-01 12:09:26 -08:00
Jamie Hardt
62c9aa7262 Merge branch 'master' of https://github.com/iluvcapra/bwavfile 2021-01-01 11:59:33 -08:00
Jamie Hardt
1f56e0f380 Documentation 2021-01-01 11:59:28 -08:00
Jamie Hardt
ef8a3adf69 Removed example folders 2020-12-31 15:31:55 -08:00
Jamie Hardt
daeb69c08c added main functions 2020-12-31 14:22:31 -08:00
Jamie Hardt
aca56558bc Added examples 2020-12-31 14:19:57 -08:00
Jamie Hardt
b11e1d7354 Merge branch 'master' of https://github.com/iluvcapra/bwavfile 2020-12-31 14:17:32 -08:00
Jamie Hardt
dfaf55955d Update README.md 2020-12-29 21:10:50 -08:00
Jamie Hardt
7ea7ac5ce7 DS64 constant 2020-12-29 14:42:11 -08:00
Jamie Hardt
41977adb83 Fixed a bug in write_chunk 2020-12-29 14:38:05 -08:00
Jamie Hardt
f63d6279c3 Update README.md
iXML/AXML writing support noted
2020-12-29 14:27:13 -08:00
Jamie Hardt
84942a4186 Implented AXML/iXML writing functions 2020-12-29 14:26:34 -08:00
Jamie Hardt
78ad1ae114 More docs, improved bext writer interface 2020-12-29 13:03:20 -08:00
Jamie Hardt
5e4c2c7da9 Doc 2020-12-29 12:17:31 -08:00
Jamie Hardt
d43ddf6338 Reorganized documentation 2020-12-29 12:09:00 -08:00
Jamie Hardt
84366089ba Implementation 2020-12-27 12:05:21 -08:00
Jamie Hardt
cbfcce235c Frame writing methods to make go faster 2020-12-27 11:34:12 -08:00
Jamie Hardt
087d98b228 Reduced commenting of rf64 test case
...to just the line we want to comment-out
2020-12-26 22:22:59 -08:00
Jamie Hardt
f978eb95ed Update README.md
Twiddles, RF64 writing test case note.
2020-12-26 21:28:47 -08:00
Jamie Hardt
1f8542a7ef Update README.md 2020-12-26 21:25:41 -08:00
Jamie Hardt
25589ea848 Version 0.9.1 2020-12-26 21:16:36 -08:00
Jamie Hardt
d242dff686 Added to README 2020-12-26 21:11:56 -08:00
Jamie Hardt
155a26ace0 RF64 implementation 2020-12-26 19:01:32 -08:00
Jamie Hardt
1d2edcb675 Documentation and RF64 impl 2020-12-26 18:50:16 -08:00
Jamie Hardt
213a856e41 Documentation 2020-12-26 18:29:59 -08:00
Jamie Hardt
70bf402776 Ambisonic format create/write 2020-12-26 13:52:52 -08:00
Jamie Hardt
3ab3a28d0e Format implementation 2020-12-26 13:41:08 -08:00
Jamie Hardt
620ca8a968 Bext writing 2020-12-26 12:12:46 -08:00
Jamie Hardt
bb6390a95c Write ds64 reservation 2020-12-26 11:29:09 -08:00
Jamie Hardt
15b4ccf851 Documentation 2020-12-26 00:24:56 -08:00
Jamie Hardt
ea9a0b6cbe Twiddles 2020-12-25 23:57:09 -08:00
Jamie Hardt
95700b642d Removed confusing emoji 2020-12-25 23:51:36 -08:00
Jamie Hardt
f3b646868f Update to v0.9.0 2020-12-25 22:23:48 -08:00
Jamie Hardt
b303a74d45 Wave writer 2020-12-25 22:21:47 -08:00
Jamie Hardt
73d2cf8cd9 Writer impl 2020-12-25 21:58:22 -08:00
Jamie Hardt
e4fc4732b5 Wavewriter impl 2020-12-25 20:55:38 -08:00
Jamie Hardt
e8b030bd1e WaveWriter impl, a new approach 2020-12-25 20:26:56 -08:00
Jamie Hardt
1219a4162f Wavewriter impl 2020-12-25 18:31:17 -08:00
Jamie Hardt
9a275a69c3 Writer impl in progress 2020-12-25 12:30:08 -08:00
Jamie Hardt
3d7c74fc94 Updated sample code in readme
To reflect changes in create_frame_buffer()
2020-12-24 23:02:55 -08:00
Jamie Hardt
e6e42f1c09 Merge branch 'master' of https://github.com/iluvcapra/bwavfile 2020-12-24 22:58:38 -08:00
Jamie Hardt
465bad40fc Writer implementation
creaet_frame_buffer() interface has been moved to WaveFmt
2020-12-24 22:57:54 -08:00
Jamie Hardt
388c46d0ee Update README.md 2020-12-24 21:59:31 -08:00
Jamie Hardt
5dba30fe23 Update README.md 2020-12-24 21:57:28 -08:00
Jamie Hardt
84e4ed1e50 Update README.md 2020-12-24 21:56:33 -08:00
Jamie Hardt
23dccecedb Update README.md
Changed presentation of features
2020-12-24 21:55:32 -08:00
Jamie Hardt
e2e029d3a3 Wavewriter implementation in progress 2020-12-24 21:41:10 -08:00
Jamie Hardt
1edfa7384d Removed raw_chunk_reader from implementation 2020-12-24 20:56:19 -08:00
Jamie Hardt
c73c9ad81d Added a workspace 2020-12-24 20:08:12 -08:00
Jamie Hardt
1edce82525 Revert "Update wavereader.rs"
This reverts commit f4b27f8545.
2020-12-13 12:40:53 -08:00
Jamie Hardt
f4b27f8545 Update wavereader.rs 2020-12-13 12:30:43 -08:00
Jamie Hardt
4cb6105073 Got rid of old code, will rewrite this 2020-12-11 01:58:01 -08:00
Jamie Hardt
ad0736dcc6 Documentation 2020-12-10 18:46:53 -08:00
Jamie Hardt
45311ad748 Documentation fix
Angle brackets were getting swallowed
2020-12-10 18:35:25 -08:00
Jamie Hardt
e502913f4f Updated readme 2020-12-10 18:11:48 -08:00
Jamie Hardt
4890483dcd Cue lists implemented 2020-12-10 18:09:28 -08:00
Jamie Hardt
8519854596 Cue lists in progress
Need to clean up zero-terminated strings in adtl
2020-12-10 14:48:25 -08:00
Jamie Hardt
c947904d0f Cue point implementation 2020-12-10 12:26:49 -08:00
Jamie Hardt
2323d61ee9 Added test media from izotope RX Editor 2020-12-09 20:35:06 -08:00
Jamie Hardt
60af2f43ff Update README.md 2020-12-06 12:16:49 -08:00
Jamie Hardt
eb431f865f Update README.md 2020-12-06 11:47:53 -08:00
Jamie Hardt
8b049e4245 Chanegd reader interface
broadcast_extension returns an Option<> now
2020-12-04 22:22:40 -08:00
Jamie Hardt
dfe90fba4a Documentation 2020-12-04 22:11:25 -08:00
Jamie Hardt
b2d4bff28b Update lib.rs 2020-12-04 20:39:58 -08:00
Jamie Hardt
79173b0576 Update README.md 2020-12-04 20:39:04 -08:00
Jamie Hardt
e15e36d65d Update README.md 2020-12-04 20:35:39 -08:00
Jamie Hardt
1dc5f153b0 Update README.md 2020-12-04 20:34:08 -08:00
Jamie Hardt
7a9f9c8bb3 Update README.md
Roadmap updates to "Features"
2020-12-04 20:33:30 -08:00
Jamie Hardt
0dd7ee4d18 Merge branch 'master' of https://github.com/iluvcapra/bwavfile 2020-12-03 23:57:43 -08:00
Jamie Hardt
a213e748db Sampler resources 2020-12-03 23:47:41 -08:00
Jamie Hardt
d221629b3e More notes on things to do 2020-12-02 22:39:02 -08:00
Jamie Hardt
2fcb211a67 Docs 2020-12-02 22:28:33 -08:00
Jamie Hardt
dfe513b596 iXML and axml cleanup impl 2020-12-02 22:27:14 -08:00
Jamie Hardt
7e1c368862 Nudged version 2020-12-02 21:45:40 -08:00
Jamie Hardt
8985361029 AudioFrameReader consumes the file of WaveReader 2020-12-02 21:36:50 -08:00
Jamie Hardt
e23529e7b6 Note 2020-12-02 17:05:03 -08:00
Jamie Hardt
538aedd413 Playing with comment 2020-12-02 12:52:58 -08:00
Jamie Hardt
09a9413ff2 Changed audio_frame_reader interface
to hide RawChunkReader
2020-12-02 12:21:20 -08:00
Jamie Hardt
016f5e3e3b Update wavereader.rs
Fixed typo
2020-11-29 14:06:16 -08:00
Jamie Hardt
11b834be76 Update fmt.rs
Documentation
2020-11-29 14:04:52 -08:00
Jamie Hardt
a4ded50112 Merge branch 'master' of https://github.com/iluvcapra/bwavfile 2020-11-28 16:36:59 -08:00
Jamie Hardt
9b2a9783a0 Updated Cargo.toml 2020-11-28 16:35:55 -08:00
Jamie Hardt
1e53436d4b Update README.md 2020-11-28 15:57:34 -08:00
Jamie Hardt
a9d9081fad Update README.md 2020-11-28 15:55:12 -08:00
Jamie Hardt
c43144c9db Update README.md 2020-11-28 15:53:53 -08:00
Jamie Hardt
082a8596af Made changes to eliminate warnings 2020-11-28 11:30:21 -08:00
Jamie Hardt
208aa7f064 Fixed typo 2020-11-28 11:24:05 -08:00
Jamie Hardt
69da33b3dc Renamed axml method 2020-11-28 11:01:50 -08:00
Jamie Hardt
9cbb2664d4 axml and iXML access methods 2020-11-27 22:47:48 -08:00
Jamie Hardt
a4c4936665 Fixed thinko in documentation 2020-11-27 22:32:55 -08:00
Jamie Hardt
5f69ec8c61 Nudged version 0.1.5 2020-11-27 22:14:52 -08:00
Jamie Hardt
ac6bd9c1e8 Added tests for channel info 2020-11-27 22:10:01 -08:00
Jamie Hardt
010f261598 Channel descriptor implementation 2020-11-27 22:03:54 -08:00
Jamie Hardt
b40e8edf23 Have created read tests 2020-11-27 21:42:53 -08:00
Jamie Hardt
3c221bce40 Update common_format.rs 2020-11-23 23:15:27 -08:00
Jamie Hardt
883b6c73e8 Documentation 2020-11-23 22:56:46 -08:00
31 changed files with 3351 additions and 993 deletions

28
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,28 @@
---
name: Bug report
about: Create a report to help us improve
title: "[Bug] __Enter a title here__"
labels: bug
assignees: iluvcapra
---
**Describe the bug**
A clear and concise description of what the bug is.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Example Code**:
```rust
// Give an example of code that is failing here.
```
**Platform**:
- OS
- `rustc` version: ____ (from `rustup -V`)
- Host: ____ (from `rustup show` e.g. x86_64-apple-darwin)
- Rust toolchain: ____ (e.g. stable-x86_64-apple-darwin)
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,25 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[Feature] __Name the Feature__"
labels: enhancement
assignees: iluvcapra
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like:**
A clear and concise description of what you want to happen.
**Code Example:**
```rust
// Give an example of how you would like your feature to work.
```
**Describe alternatives you've considered:**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context:**
Add any other context or screenshots about the feature request here.

View File

@@ -11,13 +11,32 @@ env:
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
# - name: Install ffmpeg # - name: Install ffmpeg
# run: sudo apt-get install ffmpeg # run: sudo apt-get install ffmpeg
- name: clippy-check
# You may pin to the exact commit or the version.
# uses: LoliGothick/clippy-check@9cd01de4387f9c644b6489355bcff686da6a00f2
uses: LoliGothick/clippy-check@v0.2.11
with:
# GitHub token
token: ${{ secrets.GITHUB_TOKEN }}
# Arguments for the `cargo clippy` command
# options: # optional
# Sequence of lint warnings (without `clippy::` prefix)
# warn: # optional
# # Sequence of lint allowed (without `clippy::` prefix)
# allow: # optional
# # Sequence of lint denied (without `clippy::` prefix)
# deny: # optional
# # Sequence of lint forbidden (without `clippy::` prefix)
# forbid: # optional
# # Display name of the created GitHub check. Must be unique across several LoliGothick/clippy-check invocations.
# name: # optional, default is clippy
# # working directory where to execute `cargo clippy`.
# working-directory: # optional, default is .
- name: Create Test Media - name: Create Test Media
run: cd tests; sh create_test_media.sh run: cd tests; sh create_test_media.sh
- name: Build - name: Build

141
Cargo.lock generated
View File

@@ -1,10 +1,40 @@
# This file is automatically @generated by Cargo. # This file is automatically @generated by Cargo.
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3
[[package]]
name = "ansi_term"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
dependencies = [
"winapi",
]
[[package]]
name = "atty"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi",
"libc",
"winapi",
]
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]] [[package]]
name = "bwavfile" name = "bwavfile"
version = "0.1.4" version = "2.0.0"
dependencies = [ dependencies = [
"byteorder", "byteorder",
"clap",
"dasp_sample",
"encoding", "encoding",
"serde_json", "serde_json",
"uuid", "uuid",
@@ -12,9 +42,30 @@ dependencies = [
[[package]] [[package]]
name = "byteorder" name = "byteorder"
version = "1.3.4" version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]]
name = "clap"
version = "2.33.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"
dependencies = [
"ansi_term",
"atty",
"bitflags",
"strsim",
"textwrap",
"unicode-width",
"vec_map",
]
[[package]]
name = "dasp_sample"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f"
[[package]] [[package]]
name = "encoding" name = "encoding"
@@ -81,10 +132,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569"
[[package]] [[package]]
name = "itoa" name = "hermit-abi"
version = "0.4.6" version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
dependencies = [
"libc",
]
[[package]]
name = "itoa"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
[[package]]
name = "libc"
version = "0.2.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8f7255a17a627354f321ef0055d63b898c6fb27eff628af4d1b66b7331edf6"
[[package]] [[package]]
name = "ryu" name = "ryu"
@@ -94,15 +160,15 @@ checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.117" version = "1.0.130"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b88fa983de7720629c9387e9f517353ed404164b1e482c970a90c1a4aaf7dc1a" checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913"
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.59" version = "1.0.68"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcac07dbffa1c65e7f816ab9eba78eb142c6d44410f4eeba1e26e4f5dfa56b95" checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8"
dependencies = [ dependencies = [
"itoa", "itoa",
"ryu", "ryu",
@@ -110,7 +176,56 @@ dependencies = [
] ]
[[package]] [[package]]
name = "uuid" name = "strsim"
version = "0.8.1" version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fde2f6a4bea1d6e007c4ad38c6839fa71cbb63b6dbf5b595aa38dc9b1093c11" checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
[[package]]
name = "textwrap"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
dependencies = [
"unicode-width",
]
[[package]]
name = "unicode-width"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
[[package]]
name = "uuid"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
[[package]]
name = "vec_map"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "bwavfile" name = "bwavfile"
version = "0.1.4" version = "2.0.1"
authors = ["Jamie Hardt <jamiehardt@me.com>"] authors = ["Jamie Hardt <jamiehardt@me.com>", "Ian Hobson <ian.r.hobson@gmail.com>"]
edition = "2018" edition = "2018"
license = "MIT" license = "MIT"
description = "Rust Wave File Reader/Writer with Broadcast-WAV, MBWF and RF64 Support" description = "Rust Wave File Reader/Writer with Broadcast-WAV, MBWF and RF64 Support"
@@ -15,6 +15,14 @@ keywords = ["audio", "broadcast", "multimedia","smpte"]
[dependencies] [dependencies]
byteorder = "1.3.4" byteorder = "1.3.4"
dasp_sample = "0.11.0"
encoding = "0.2.33" encoding = "0.2.33"
uuid = "0.8.1" uuid = "0.8.1"
serde_json = "1.0.59" clap = "2.33.3"
[dev-dependencies]
serde_json = "1.0.61"
[profile.release]
debug = true

View File

@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2020 Jamie Hardt Copyright (c) 2021 Jamie Hardt
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,34 +1,53 @@
[![Crates.io](https://img.shields.io/crates/l/bwavfile)](LICENSE) [![Crates.io](https://img.shields.io/crates/l/bwavfile)](LICENSE)
[![Crates.io](https://img.shields.io/crates/v/bwavfile)](https://crates.io/crates/bwavfile/) [![Crates.io](https://img.shields.io/crates/v/bwavfile)](https://crates.io/crates/bwavfile/)
![GitHub last commit](https://img.shields.io/github/last-commit/iluvcapra/bwavfile) ![GitHub last commit](https://img.shields.io/github/last-commit/iluvcapra/bwavfile)
[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/iluvcapra/bwavfile/Rust)](https://github.com/iluvcapra/bwavfile/actions?query=workflow%3ARust) [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/iluvcapra/bwavfile/rust.yml?branch=master)](https://github.com/iluvcapra/bwavfile/actions?query=workflow%3ARust)
# bwavfile # bwavfile
Rust Wave File Reader/Writer with Broadcast-WAV, MBWF and RF64 Support Wave File Reader/Writer library in Rust, with Broadcast-WAV, RF64 and production metadata support
## Features
__bwavfile__ provides a reader `WaveReader` and writer type `WaveWriter` for
reading and creating new wave audio files.
`WaveReader` and `WaveWriter` support:
* A unified interface for standard RIFF and RF64/BW64 64-bit Wave files.
* When using `WaveWriter`, wave files are transparently upgraded from RIFF
to RF64 when required.
* Unpacked reading and writing of Integer PCM and IEEE float audio data
formats.
* A unified interface for standard `WaveFormat` and extended `WaveFormatEx`
wave data format.
The library has extensive metadata support, with emphasis on film and video
production metadata:
* Broadcast-Wave metadata extension, including long description, originator,
SMPTE UMID and coding history.
* Reading and writing of embedded iXML and axml/ADM metadata.
* Reading and writing of timed cues and and timed cue regions.
* Multichannel, surround, and ambisonic audio data description including
surround channel maps, ADM `AudioTrackFormat`, `AudioChannelFormatRef` and
`AudioPackRef` data structures.
## Feature Roadmap
Some features that may be included in the future include:
* Broadcast-Wave `levl` waveform overview data reading and writing.
* Sampler and Instrument metadata.
* Performance improvements.
This is currently a work-in-progress!
## Use Examples ## Use Examples
### Reading a File * [blits](examples/blits.rs) shows how to use `WaveWriter` to create a new
file with BLITS alignment tones.
```rust * [wave-inter](examples/wave-inter.rs) uses `WaveReader` and `WaveWriter` to
interleave several input Wave files into a single polyphonic Wave file.
use bwavfile::WaveReader; * [wave-deinter](examples/wave-deinter.rs) uses `WaveReader` and `WaveWriter`
let mut r = WaveReader::open("tests/media/ff_silence.wav").unwrap(); to de-interleave an input Wave file into several monoarual Wave files.
let format = r.format().unwrap();
assert_eq!(format.sample_rate, 44100);
assert_eq!(format.channel_count, 1);
let mut frame_reader = r.audio_frame_reader().unwrap();
let mut buffer = frame_reader.create_frame_buffer();
let read = frame_reader.read_integer_frame(&mut buffer).unwrap();
assert_eq!(buffer, [0i32]);
assert_eq!(read, 1);
```
## Note on Testing ## Note on Testing
@@ -36,3 +55,5 @@ All of the media for the integration tests is committed to the respository
in zipped form. Before you can run tests, you need to `cd` into the `tests` in zipped form. Before you can run tests, you need to `cd` into the `tests`
directory and run the `create_test_media.sh` script. Note that one of the directory and run the `create_test_media.sh` script. Note that one of the
test files (the RF64 test case) is over four gigs in size. test files (the RF64 test case) is over four gigs in size.
[rf64test]: https://github.com/iluvcapra/bwavfile/blob/1f8542a7efb481da076120bf8107032c5b48889d/src/wavewriter.rs#L399

263
examples/blits.rs Normal file
View File

@@ -0,0 +1,263 @@
//! bilts.rs
//! (c) 2021 Jamie Hardt. All rights reserved.
//!
//! This program demonstrates the creation of a wave file with a BLITS
//! ("Black and Lanes' Ident Tones for Surround") channel identification and
//! alignment signal.
//!
//! TODO: Pre-calculate the sine waves to speed up generation
//! TODO: Make tone onsets less snappy
use std::f64;
use std::io;
extern crate bwavfile;
use bwavfile::{Error, WaveFmt, WaveWriter};
#[macro_use]
extern crate clap;
use clap::{App, Arg};
fn sine_wave(t: u64, amplitude: i32, wavelength: u32) -> i32 {
//I did it this way because I'm weird
Some(t)
.map(|i| (i as f64) * 2f64 * f64::consts::PI / wavelength as f64)
.map(|f| f.sin())
.map(|s| (s * amplitude as f64) as i32)
.unwrap()
}
/// Return the corresponding f32 gain for a dbfs.
///
/// Retval will always be positive
fn dbfs_to_f32(dbfs: f32) -> f32 {
10f32.powf(dbfs / 20f32)
}
fn dbfs_to_signed_int(dbfs: f32, bit_depth: u16) -> i32 {
let full_code: i32 = (1i32 << bit_depth - 1) - 1;
((full_code as f32) * dbfs_to_f32(dbfs)) as i32
}
#[derive(Clone, Copy, PartialEq)]
enum ToneBurst {
/// Tone of .0 frequency (hz) for .1 duration (ms) at .2 dBfs
Tone(f32, u64, f32),
/// Silence of .0 Duration (ms)
Silence(u64),
}
impl ToneBurst {
fn duration(&self, sample_rate: u32) -> u64 {
match self {
Self::Tone(_, dur, _) => *dur * sample_rate as u64 / 1000,
Self::Silence(dur) => *dur * sample_rate as u64 / 1000,
}
}
}
trait ToneBurstSignal {
fn duration(&self, sample_rate: u32) -> u64;
fn signal(&self, t: u64, sample_rate: u32, bit_depth: u16) -> i32;
}
impl ToneBurstSignal for Vec<ToneBurst> {
fn duration(&self, sample_rate: u32) -> u64 {
self.iter()
.fold(0u64, |accum, &item| accum + &item.duration(sample_rate))
}
fn signal(&self, t: u64, sample_rate: u32, bit_depth: u16) -> i32 {
self.iter()
.scan(0u64, |accum, &item| {
let dur = item.duration(sample_rate);
let this_time_range = *accum..(*accum + dur);
*accum = *accum + dur;
Some((this_time_range, item))
})
.find(|(range, _)| range.contains(&t))
.map(|(_, item)| match item {
ToneBurst::Tone(freq, _, dbfs) => {
let gain = dbfs_to_signed_int(dbfs, bit_depth);
sine_wave(t, gain, (sample_rate as f32 / freq) as u32)
}
ToneBurst::Silence(_) => 0,
})
.unwrap_or(0i32)
}
}
fn create_blits_file(file_name: &str, sample_rate: u32, bits_per_sample: u16) -> Result<(), Error> {
// BLITS Tone signal format
// From EBU Tech 3304 §4 - https://tech.ebu.ch/docs/tech/tech3304.pdf
let left_channel_sequence: Vec<ToneBurst> = vec![
// channel ident
ToneBurst::Tone(880.0, 600, -18.0),
ToneBurst::Silence(200),
ToneBurst::Silence(4000),
// LR ident
ToneBurst::Tone(1000.0, 1000, -18.0),
ToneBurst::Silence(300),
ToneBurst::Tone(1000.0, 300, -18.0),
ToneBurst::Silence(300),
ToneBurst::Tone(1000.0, 300, -18.0),
ToneBurst::Silence(300),
ToneBurst::Tone(1000.0, 300, -18.0),
ToneBurst::Silence(300),
ToneBurst::Tone(1000.0, 2000, -18.0),
ToneBurst::Silence(300),
// Phase check,
ToneBurst::Tone(2000.0, 3000, -24.0),
ToneBurst::Silence(200),
];
let right_channel_sequence: Vec<ToneBurst> = vec![
// channel ident
ToneBurst::Silence(800),
ToneBurst::Tone(880.0, 600, -18.0),
ToneBurst::Silence(200),
ToneBurst::Silence(3200),
// LR ident
ToneBurst::Tone(1000.0, 5100, -18.0),
ToneBurst::Silence(300),
// Phase check,
ToneBurst::Tone(2000.0, 3000, -24.0),
ToneBurst::Silence(200),
];
let center_channel_sequence: Vec<ToneBurst> = vec![
// channel ident
ToneBurst::Silence(1600),
ToneBurst::Tone(1320.0, 600, -18.0),
ToneBurst::Silence(200),
ToneBurst::Silence(2400),
// LR ident
ToneBurst::Silence(5400),
// Phase check,
ToneBurst::Tone(2000.0, 3000, -24.0),
ToneBurst::Silence(200),
];
let lfe_channel_sequence: Vec<ToneBurst> = vec![
// channel ident
ToneBurst::Silence(2400),
ToneBurst::Tone(82.5, 600, -18.0),
ToneBurst::Silence(200),
ToneBurst::Silence(1600),
// LR ident
ToneBurst::Silence(5400),
// Phase check,
ToneBurst::Tone(2000.0, 3000, -24.0),
ToneBurst::Silence(200),
];
let ls_channel_sequence: Vec<ToneBurst> = vec![
// channel ident
ToneBurst::Silence(3200),
ToneBurst::Tone(660.0, 600, -18.0),
ToneBurst::Silence(200),
ToneBurst::Silence(800),
// LR ident
ToneBurst::Silence(5400),
// Phase check,
ToneBurst::Tone(2000.0, 3000, -24.0),
ToneBurst::Silence(200),
];
let rs_channel_sequence: Vec<ToneBurst> = vec![
// channel ident
ToneBurst::Silence(4000),
ToneBurst::Tone(660.0, 600, -18.0),
ToneBurst::Silence(200),
// LR ident
ToneBurst::Silence(5400),
// Phase check,
ToneBurst::Tone(2000.0, 3000, -24.0),
ToneBurst::Silence(200),
];
let length = [
&left_channel_sequence,
&right_channel_sequence,
&center_channel_sequence,
&lfe_channel_sequence,
&ls_channel_sequence,
&rs_channel_sequence,
]
.iter()
.map(|i| i.duration(sample_rate))
.max()
.unwrap_or(0);
let frames = (0..=length).map(|frame| {
(
left_channel_sequence.signal(frame, sample_rate, bits_per_sample),
right_channel_sequence.signal(frame, sample_rate, bits_per_sample),
center_channel_sequence.signal(frame, sample_rate, bits_per_sample),
lfe_channel_sequence.signal(frame, sample_rate, bits_per_sample),
ls_channel_sequence.signal(frame, sample_rate, bits_per_sample),
rs_channel_sequence.signal(frame, sample_rate, bits_per_sample),
)
});
let format = WaveFmt::new_pcm_multichannel(sample_rate, bits_per_sample, 0b111111);
let file = WaveWriter::create(file_name, format)?;
let mut fw = file.audio_frame_writer()?;
for frame in frames {
let buf = vec![frame.0, frame.1, frame.2, frame.3, frame.4, frame.5];
fw.write_frames(&buf)?;
}
fw.end()?;
Ok(())
}
fn main() -> io::Result<()> {
let matches = App::new("blits")
.version(crate_version!())
.author(crate_authors!())
.about("Generate a BLITS 5.1 alignment tone.")
.arg(
Arg::with_name("sample_rate")
.long("sample-rate")
.short("s")
.help("Sample rate of output")
.default_value("48000"),
)
.arg(
Arg::with_name("bit_depth")
.long("bit-depth")
.short("b")
.help("Bit depth of output")
.default_value("24"),
)
.arg(
Arg::with_name("OUTPUT")
.help("Output wave file")
.default_value("blits.wav"),
)
.get_matches();
let sample_rate = matches
.value_of("sample_rate")
.unwrap()
.parse::<u32>()
.expect("Failed to read sample rate");
let bits_per_sample = matches
.value_of("bit_depth")
.unwrap()
.parse::<u16>()
.expect("Failed to read bit depth");
let filename = matches.value_of("OUTPUT").unwrap();
match create_blits_file(&filename, sample_rate, bits_per_sample) {
Err(Error::IOError(x)) => panic!("IO Error: {:?}", x),
Err(err) => panic!("Error: {:?}", err),
Ok(()) => Ok(()),
}
}

201
examples/wave-deinter.rs Normal file
View File

@@ -0,0 +1,201 @@
//! wave-deinter.rs
//! (c) 2021 Jamie Hardt. All rights reserved.
//!
//! This program demonstrates splitting a multichannel file into separate monophonic files for each
//! individual channel.
use std::io::{Read, Seek};
use std::path::Path;
extern crate bwavfile;
use bwavfile::{
ChannelDescriptor, ChannelMask, CommonFormat, Error, Sample, WaveFmt, WaveReader, WaveWriter,
I24,
};
#[macro_use]
extern crate clap;
use clap::{App, Arg};
fn name_suffix(
force_numeric: bool,
delim: &str,
index: usize,
channel_descriptor: &ChannelDescriptor,
) -> String {
if force_numeric || channel_descriptor.speaker == ChannelMask::DirectOut {
format!("{}A{:02}", delim, index)
} else {
let chan_name = match channel_descriptor.speaker {
ChannelMask::FrontLeft => "L",
ChannelMask::FrontCenter => "C",
ChannelMask::FrontRight => "R",
ChannelMask::BackLeft => "Ls",
ChannelMask::BackRight => "Rs",
ChannelMask::BackCenter => "S",
ChannelMask::TopCenter => "Tc",
ChannelMask::LowFrequency => "Lfe",
ChannelMask::SideLeft => "Lss",
ChannelMask::SideRight => "Rss",
ChannelMask::FrontCenterLeft => "Lc",
ChannelMask::FrontCenterRight => "Rc",
ChannelMask::TopFrontLeft => "Ltf",
ChannelMask::TopFrontCenter => "Ctf",
ChannelMask::TopFrontRight => "Rtf",
ChannelMask::TopBackLeft => "Ltb",
ChannelMask::TopBackCenter => "Ctb",
ChannelMask::TopBackRight => "Rtb",
ChannelMask::DirectOut => panic!("Error, can't get here"),
};
format!("{}{}", delim, chan_name)
}
}
fn deinterleave_file<S, R>(
mut input_file: WaveReader<R>,
input_format: WaveFmt,
settings: Settings,
) -> Result<(), Error>
where
S: Sample,
R: Read + Seek,
{
let frames_per_read = 4096;
let channel_desc = input_file.channels()?;
let channel_count = channel_desc.len();
if channel_desc.len() == 1 {
println!("Input file in monoaural, exiting.");
return Ok(());
}
let infile_path = Path::new(&settings.input_path);
let basename = infile_path
.file_stem()
.expect("Unable to extract file basename")
.to_str()
.unwrap();
let output_dir = infile_path
.parent()
.expect("Unable to derive parent directory");
let output_block_alignment = input_format.bits_per_sample / 8;
let output_format = WaveFmt {
channel_count: 1,
block_alignment: output_block_alignment,
bytes_per_second: output_block_alignment as u32 * input_format.sample_rate,
..input_format
};
let mut reader = input_file.audio_frame_reader()?;
let mut writers = channel_desc
.iter()
.enumerate()
.map(|(n, channel)| {
let suffix = name_suffix(
settings.use_numeric_names,
&settings.delimiter,
n + 1,
channel,
);
let outfile_name = output_dir
.join(format!("{}{}.wav", basename, suffix))
.into_os_string()
.into_string()
.unwrap();
println!("Will create file {}", outfile_name);
WaveWriter::create(&outfile_name, output_format)
.expect("Failed to create new file")
.audio_frame_writer()
})
.collect::<Result<Vec<_>, _>>()?;
let mut input_buffer = vec![S::EQUILIBRIUM; frames_per_read * channel_count];
let mut output_buffer = vec![S::EQUILIBRIUM; frames_per_read];
loop {
let frames_read = reader.read_frames(&mut input_buffer)? as usize;
if frames_read == 0 {
break;
}
output_buffer.resize(frames_read, S::EQUILIBRIUM);
for (n, writer) in writers.iter_mut().enumerate() {
for (output, input) in output_buffer
.iter_mut()
.zip(input_buffer.iter().skip(n).step_by(channel_count))
{
*output = *input;
}
writer.write_frames(&output_buffer)?;
}
}
for writer in writers.drain(..) {
writer.end()?;
}
Ok(())
}
fn process_file<R>(mut input: WaveReader<R>, settings: Settings) -> Result<(), Error>
where
R: Read + Seek,
{
let format = input.format()?;
use CommonFormat::*;
match (format.common_format(), format.bits_per_sample) {
(IntegerPCM, 8) => deinterleave_file::<u8, R>(input, format, settings),
(IntegerPCM, 16) => deinterleave_file::<i16, R>(input, format, settings),
(IntegerPCM, 24) => deinterleave_file::<I24, R>(input, format, settings),
(IntegerPCM, 32) => deinterleave_file::<i32, R>(input, format, settings),
(IeeeFloatPCM, 32) => deinterleave_file::<f32, R>(input, format, settings),
other => panic!("Unsupported format: {:?}", other),
}
}
struct Settings {
input_path: String,
delimiter: String,
use_numeric_names: bool,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let matches = App::new("wave-deinter")
.version(crate_version!())
.author(crate_authors!())
.about("Extract each channel of a polyphonic wave file as a new monoaural wave file.")
.arg(
Arg::with_name("numeric_names")
.long("numeric")
.short("n")
.help("Use numeric channel names \"01\" \"02\" etc.")
.takes_value(false),
)
.arg(
Arg::with_name("channel_delimiter")
.long("delim")
.short("d")
.help("Channel label delimiter.")
.default_value("."),
)
.arg(
Arg::with_name("INPUT")
.help("Input wave file")
.required(true)
.multiple(true),
)
.get_matches();
let settings = Settings {
input_path: matches.value_of("INPUT").unwrap().into(),
delimiter: matches.value_of("channel_delimiter").unwrap().into(),
use_numeric_names: matches.is_present("numeric_names"),
};
process_file(WaveReader::open(&settings.input_path)?, settings)?;
Ok(())
}

35
examples/wave-inter.rs Normal file
View File

@@ -0,0 +1,35 @@
//! wave-inter.rs
//! (c) 2021 Jamie Hardt. All rights reserved.
//!
//! This program demonstrates combining several wave files into a single
//! polyphonic wave file.
use std::io;
extern crate bwavfile;
#[macro_use]
extern crate clap;
use clap::{App, Arg};
fn main() -> io::Result<()> {
let matches = App::new("wave-inter")
.version(crate_version!())
.author(crate_authors!())
.about("Combine several wave files into a single polyphonic wave file.")
.arg(Arg::with_name("OUTPUT")
.long("output")
.short("o")
.help("Output file name. If absent, will be basename, minus any channel extension, of first INPUT.")
)
.arg(Arg::with_name("INPUT")
.help("Input wave file")
.required(true)
.multiple(true)
)
.get_matches();
println!("Command line opts: {:?}", matches);
todo!("Finish implementation");
}

View File

@@ -1,89 +0,0 @@
use std::io::{Read, Seek};
use std::io::SeekFrom::{Start,};
use byteorder::LittleEndian;
use byteorder::ReadBytesExt;
use super::fmt::{WaveFmt};
use super::errors::Error;
/// Read audio frames
///
/// The inner reader is interpreted as a raw audio data
/// bitstream having a format specified by `format`.
///
#[derive(Debug)]
pub struct AudioFrameReader<R: Read + Seek> {
inner : R,
format: WaveFmt
}
impl<R: Read + Seek> AudioFrameReader<R> {
/// Create a new `AudioFrameReader`
///
/// ### Panics
///
/// This method does a few sanity checks on the provided format
/// parameter to confirm the `block_alignment` law is fulfilled
/// and the format tag is readable by this implementation (only
/// format 0x01 is supported at this time.)
pub fn new(inner: R, format: WaveFmt) -> Self {
assert!(format.block_alignment * 8 == format.bits_per_sample * format.channel_count,
"Unable to read audio frames from packed formats: block alignment is {}, should be {}",
format.block_alignment, (format.bits_per_sample / 8 ) * format.channel_count);
assert!(format.tag == 0x01 ,
"Unsupported format tag {:?}", format.tag);
AudioFrameReader { inner , format }
}
/// Locate the read position to a different frame
///
/// Seeks within the audio stream.
pub fn locate(&mut self, to :u64) -> Result<u64,Error> {
let position = to * self.format.block_alignment as u64;
let seek_result = self.inner.seek(Start(position))?;
Ok( seek_result / self.format.block_alignment as u64 )
}
/// Create a frame buffer sized to hold frames of the reader
///
/// This is a conveneince method that creates a `Vec<i32>` with
/// as many elements as there are channels in the underlying stream.
pub fn create_frame_buffer(&self) -> Vec<i32> {
vec![0i32; self.format.channel_count as usize]
}
/// Read a frame
///
/// A single frame is read from the audio stream and the read location
/// is advanced one frame.
///
/// ### Panics
///
/// The `buffer` must have a number of elements equal to the number of
/// channels and this method will panic if this is not the case.
pub fn read_integer_frame(&mut self, buffer:&mut [i32]) -> Result<u64,Error> {
assert!(buffer.len() as u16 == self.format.channel_count,
"read_integer_frame was called with a mis-sized buffer, expected {}, was {}",
self.format.channel_count, buffer.len());
let framed_bits_per_sample = self.format.block_alignment * 8 / self.format.channel_count;
for n in 0..(self.format.channel_count as usize) {
buffer[n] = match (self.format.bits_per_sample, framed_bits_per_sample) {
(0..=8,8) => self.inner.read_u8()? as i32 - 0x80_i32, // EBU 3285 §A2.2
(9..=16,16) => self.inner.read_i16::<LittleEndian>()? as i32,
(10..=24,24) => self.inner.read_i24::<LittleEndian>()?,
(25..=32,32) => self.inner.read_i32::<LittleEndian>()?,
(b,_)=> panic!("Unrecognized integer format, bits per sample {}, channels {}, block_alignment {}",
b, self.format.channel_count, self.format.block_alignment)
}
}
Ok( 1 )
}
}

View File

@@ -1,32 +1,33 @@
pub type LU = f32; pub type LU = f32;
#[allow(clippy::upper_case_acronyms)]
pub type LUFS = f32; pub type LUFS = f32;
pub type Decibels = f32; pub type Decibels = f32;
/** /// Broadcast-WAV metadata record.
* Broadcast-WAV metadata record. ///
* /// The `bext` record contains information about the original recording of the
* The `bext` record contains information about the original recording of the /// Wave file, including a longish (256 ASCII chars) description field,
* Wave file, including a longish (256 ASCII chars) description field, /// originator identification fields, creation calendar date and time, a
* originator identification fields, creation calendar date and time, a /// sample-accurate recording time field, and a SMPTE UMID.
* sample-accurate recording time field, and a SMPTE UMID. ///
* /// For a Wave file to be a complaint "Broadcast-WAV" file, it must contain
* For a Wave file to be a complaint "Broadcast-WAV" file, it must contain /// a `bext` metadata record.
* a `bext` metadata record. ///
* /// ## Resources
* For reference on the structure and use of the BEXT record /// - [EBU Tech 3285](https://tech.ebu.ch/docs/tech/tech3285.pdf) "Specification of the Broadcast Wave Format (BWF)"
* check out [EBU Tech 3285](https://tech.ebu.ch/docs/tech/tech3285.pdf). /// - [EBU Tech R098](https://tech.ebu.ch/docs/r/r098.pdf) (1999) "Format for the &lt;CodingHistory&gt; field in Broadcast Wave Format files, BWF"
*/ /// - [EBU Tech R099](https://tech.ebu.ch/docs/r/r099.pdf) (October 2011) "Unique Source Identifier (USID) for use in the
/// &lt;OriginatorReference&gt; field of the Broadcast Wave Format"
#[derive(Debug)] #[derive(Debug)]
pub struct Bext { pub struct Bext {
/// 0..256 ASCII character field with free text.
/// 256 ASCII character field with free text.
pub description: String, pub description: String,
/// Originating application. /// 0..32 ASCII character Originating application.
pub originator: String, pub originator: String,
/// Application-specific UID. /// 0..32 ASCII character application-specific UID or EBU R099-formatted UID.
pub originator_reference: String, pub originator_reference: String,
/// Creation date in format `YYYY-MM-DD`. /// Creation date in format `YYYY-MM-DD`.
@@ -35,7 +36,7 @@ pub struct Bext {
/// Creation time in format `HH:MM:SS`. /// Creation time in format `HH:MM:SS`.
pub origination_time: String, pub origination_time: String,
/// Time of the start of this wave file, expressed as the number of samples /// Start timestamp of this wave file, in number of samples
/// since local midnight. /// since local midnight.
pub time_reference: u64, pub time_reference: u64,
@@ -47,7 +48,6 @@ pub struct Bext {
/// SMPTE 330M UMID /// SMPTE 330M UMID
/// ///
///
/// This field is `None` if the version is less than 1. /// This field is `None` if the version is less than 1.
pub umid: Option<[u8; 64]>, pub umid: Option<[u8; 64]>,
@@ -76,7 +76,6 @@ pub struct Bext {
/// This field is `None` if the version is less than 2. /// This field is `None` if the version is less than 2.
pub max_short_term_loudness: Option<LUFS>, pub max_short_term_loudness: Option<LUFS>,
// 180 bytes of nothing // 180 bytes of nothing
/// Coding History. /// Coding History.
pub coding_history: String pub coding_history: String,
} }

View File

@@ -1,17 +1,17 @@
use std::io::{Read, Write}; use std::io::{Read, Write};
use encoding::{DecoderTrap, EncoderTrap};
use encoding::{Encoding};
use encoding::all::ASCII; use encoding::all::ASCII;
use encoding::Encoding;
use encoding::{DecoderTrap, EncoderTrap};
use byteorder::LittleEndian; use byteorder::LittleEndian;
use byteorder::{ReadBytesExt, WriteBytesExt}; use byteorder::{ReadBytesExt, WriteBytesExt};
use uuid::Uuid; use uuid::Uuid;
use super::bext::Bext;
use super::errors::Error as ParserError; use super::errors::Error as ParserError;
use super::fmt::{WaveFmt, WaveFmtExtended}; use super::fmt::{WaveFmt, WaveFmtExtended};
use super::bext::Bext;
pub trait ReadBWaveChunks: Read { pub trait ReadBWaveChunks: Read {
fn read_bext(&mut self) -> Result<Bext, ParserError>; fn read_bext(&mut self) -> Result<Bext, ParserError>;
@@ -21,24 +21,36 @@ pub trait ReadBWaveChunks: Read {
pub trait WriteBWaveChunks: Write { pub trait WriteBWaveChunks: Write {
fn write_wave_fmt(&mut self, format: &WaveFmt) -> Result<(), ParserError>; fn write_wave_fmt(&mut self, format: &WaveFmt) -> Result<(), ParserError>;
fn write_bext_string_field(&mut self, string: &String, length: usize) -> Result<(),ParserError>; fn write_bext_string_field(&mut self, string: &str, length: usize) -> Result<(), ParserError>;
fn write_bext(&mut self, bext: &Bext) -> Result<(), ParserError>; fn write_bext(&mut self, bext: &Bext) -> Result<(), ParserError>;
} }
impl<T> WriteBWaveChunks for T where T: Write { impl<T> WriteBWaveChunks for T
where
T: Write,
{
fn write_wave_fmt(&mut self, format: &WaveFmt) -> Result<(), ParserError> { fn write_wave_fmt(&mut self, format: &WaveFmt) -> Result<(), ParserError> {
self.write_u16::<LittleEndian>(format.tag as u16 )?; self.write_u16::<LittleEndian>(format.tag)?;
self.write_u16::<LittleEndian>(format.channel_count)?; self.write_u16::<LittleEndian>(format.channel_count)?;
self.write_u32::<LittleEndian>(format.sample_rate)?; self.write_u32::<LittleEndian>(format.sample_rate)?;
self.write_u32::<LittleEndian>(format.bytes_per_second)?; self.write_u32::<LittleEndian>(format.bytes_per_second)?;
self.write_u16::<LittleEndian>(format.block_alignment)?; self.write_u16::<LittleEndian>(format.block_alignment)?;
self.write_u16::<LittleEndian>(format.bits_per_sample)?; self.write_u16::<LittleEndian>(format.bits_per_sample)?;
// self.write_u8(0)?; if let Some(ext) = format.extended_format {
let cb_size = 24u16;
self.write_u16::<LittleEndian>(cb_size)?;
self.write_u16::<LittleEndian>(ext.valid_bits_per_sample)?;
self.write_u32::<LittleEndian>(ext.channel_mask)?;
let uuid = ext.type_guid.as_bytes();
self.write_all(uuid)?;
}
Ok(()) Ok(())
} }
fn write_bext_string_field(&mut self, string: &String, length: usize) -> Result<(),ParserError> { fn write_bext_string_field(&mut self, string: &str, length: usize) -> Result<(), ParserError> {
let mut buf = ASCII.encode(&string, EncoderTrap::Ignore).expect("Error encoding text"); let mut buf = ASCII
.encode(string, EncoderTrap::Ignore)
.expect("Error encoding text");
buf.truncate(length); buf.truncate(length);
let filler_length = length - buf.len(); let filler_length = length - buf.len();
if filler_length > 0 { if filler_length > 0 {
@@ -62,21 +74,21 @@ impl<T> WriteBWaveChunks for T where T: Write {
let buf = bext.umid.unwrap_or([0u8; 64]); let buf = bext.umid.unwrap_or([0u8; 64]);
self.write_all(&buf)?; self.write_all(&buf)?;
self.write_i16::<LittleEndian>((bext.loudness_value.unwrap_or(0.0) * 100.0) as i16)?;
self.write_i16::<LittleEndian>((bext.loudness_range.unwrap_or(0.0) * 100.0) as i16)?;
self.write_i16::<LittleEndian>((bext.max_true_peak_level.unwrap_or(0.0) * 100.0) as i16)?;
self.write_i16::<LittleEndian>( self.write_i16::<LittleEndian>(
(bext.loudness_value.unwrap_or(0.0) * 100.0) as i16 )?; (bext.max_momentary_loudness.unwrap_or(0.0) * 100.0) as i16,
)?;
self.write_i16::<LittleEndian>( self.write_i16::<LittleEndian>(
(bext.loudness_range.unwrap_or(0.0) * 100.0) as i16 )?; (bext.max_short_term_loudness.unwrap_or(0.0) * 100.0) as i16,
self.write_i16::<LittleEndian>( )?;
(bext.max_true_peak_level.unwrap_or(0.0) * 100.0) as i16 )?;
self.write_i16::<LittleEndian>(
(bext.max_momentary_loudness.unwrap_or(0.0) * 100.0) as i16 )?;
self.write_i16::<LittleEndian>(
(bext.max_short_term_loudness.unwrap_or(0.0) * 100.0) as i16 )?;
let padding = [0u8; 180]; let padding = [0u8; 180];
self.write_all(&padding)?; self.write_all(&padding)?;
let coding = ASCII.encode(&bext.coding_history, EncoderTrap::Ignore) let coding = ASCII
.encode(&bext.coding_history, EncoderTrap::Ignore)
.expect("Error"); .expect("Error");
self.write_all(&coding)?; self.write_all(&coding)?;
@@ -84,8 +96,10 @@ impl<T> WriteBWaveChunks for T where T: Write {
} }
} }
impl<T> ReadBWaveChunks for T where T: Read { impl<T> ReadBWaveChunks for T
where
T: Read,
{
fn read_wave_fmt(&mut self) -> Result<WaveFmt, ParserError> { fn read_wave_fmt(&mut self) -> Result<WaveFmt, ParserError> {
let tag_value: u16; let tag_value: u16;
Ok(WaveFmt { Ok(WaveFmt {
@@ -109,20 +123,22 @@ impl<T> ReadBWaveChunks for T where T: Read {
let mut buf: [u8; 16] = [0; 16]; let mut buf: [u8; 16] = [0; 16];
self.read_exact(&mut buf)?; self.read_exact(&mut buf)?;
Uuid::from_slice(&buf)? Uuid::from_slice(&buf)?
} },
}) })
} else { } else {
None None
} }
} },
}) })
} }
fn read_bext_string_field(&mut self, length: usize) -> Result<String, ParserError> { fn read_bext_string_field(&mut self, length: usize) -> Result<String, ParserError> {
let mut buffer: Vec<u8> = vec![0; length]; let mut buffer: Vec<u8> = vec![0; length];
self.read(&mut buffer)?; self.read_exact(&mut buffer)?;
let trimmed : Vec<u8> = buffer.iter().take_while(|c| **c != 0 as u8).cloned().collect(); let trimmed: Vec<u8> = buffer.iter().take_while(|c| **c != 0_u8).cloned().collect();
Ok(ASCII.decode(&trimmed, DecoderTrap::Ignore).expect("Error decoding text")) Ok(ASCII
.decode(&trimmed, DecoderTrap::Ignore)
.expect("Error decoding text"))
} }
fn read_bext(&mut self) -> Result<Bext, ParserError> { fn read_bext(&mut self) -> Result<Bext, ParserError> {
@@ -140,43 +156,71 @@ impl<T> ReadBWaveChunks for T where T: Read {
}, },
umid: { umid: {
let mut buf = [0u8; 64]; let mut buf = [0u8; 64];
self.read(&mut buf)?; self.read_exact(&mut buf)?;
if version > 0 { Some(buf) } else { None } if version > 0 {
Some(buf)
} else {
None
}
}, },
loudness_value: { loudness_value: {
let val = (self.read_i16::<LittleEndian>()? as f32) / 100f32; let val = (self.read_i16::<LittleEndian>()? as f32) / 100f32;
if version > 1 { Some(val) } else { None } if version > 1 {
Some(val)
} else {
None
}
}, },
loudness_range: { loudness_range: {
let val = self.read_i16::<LittleEndian>()? as f32 / 100f32; let val = self.read_i16::<LittleEndian>()? as f32 / 100f32;
if version > 1 { Some(val) } else { None } if version > 1 {
Some(val)
} else {
None
}
}, },
max_true_peak_level: { max_true_peak_level: {
let val = self.read_i16::<LittleEndian>()? as f32 / 100f32; let val = self.read_i16::<LittleEndian>()? as f32 / 100f32;
if version > 1 { Some(val) } else { None } if version > 1 {
Some(val)
} else {
None
}
}, },
max_momentary_loudness: { max_momentary_loudness: {
let val = self.read_i16::<LittleEndian>()? as f32 / 100f32; let val = self.read_i16::<LittleEndian>()? as f32 / 100f32;
if version > 1 { Some(val) } else { None } if version > 1 {
Some(val)
} else {
None
}
}, },
max_short_term_loudness: { max_short_term_loudness: {
let val = self.read_i16::<LittleEndian>()? as f32 / 100f32; let val = self.read_i16::<LittleEndian>()? as f32 / 100f32;
if version > 1 { Some(val) } else { None } if version > 1 {
Some(val)
} else {
None
}
}, },
coding_history: { coding_history: {
for _ in 0..180 { self.read_u8()?; } for _ in 0..180 {
self.read_u8()?;
}
let mut buf = vec![]; let mut buf = vec![];
self.read_to_end(&mut buf)?; self.read_to_end(&mut buf)?;
ASCII.decode(&buf, DecoderTrap::Ignore).expect("Error decoding text") ASCII
} .decode(&buf, DecoderTrap::Ignore)
.expect("Error decoding text")
},
}) })
} }
} }
#[test] #[test]
fn test_read_51_wav() { fn test_read_51_wav() {
use super::fmt::ChannelMask;
use super::common_format::CommonFormat; use super::common_format::CommonFormat;
use super::fmt::ChannelMask;
let path = "tests/media/pt_24bit_51.wav"; let path = "tests/media/pt_24bit_51.wav";
@@ -191,9 +235,17 @@ fn test_read_51_wav() {
let channels = ChannelMask::channels(extended.channel_mask, format.channel_count); let channels = ChannelMask::channels(extended.channel_mask, format.channel_count);
assert_eq!(channels, [ChannelMask::FrontLeft, ChannelMask::FrontRight, assert_eq!(
ChannelMask::FrontCenter, ChannelMask::LowFrequency, channels,
ChannelMask::BackLeft, ChannelMask::BackRight]); [
ChannelMask::FrontLeft,
ChannelMask::FrontRight,
ChannelMask::FrontCenter,
ChannelMask::LowFrequency,
ChannelMask::BackLeft,
ChannelMask::BackRight
]
);
assert_eq!(format.common_format(), CommonFormat::IntegerPCM); assert_eq!(format.common_format(), CommonFormat::IntegerPCM);
} }

View File

@@ -1,16 +1,17 @@
/// Format tags, UUIDs and utilities
use uuid::Uuid; use uuid::Uuid;
/** /// Format tag for integer LPCM
* References: pub const WAVE_TAG_PCM: u16 = 0x0001;
* - http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/Docs/multichaudP.pdf
*/
// http://dream.cs.bath.ac.uk/researchdev/wave-ex/bformat.html /// Format tag for float LPCM
pub const WAVE_TAG_FLOAT: u16 = 0x0003;
const BASIC_PCM: u16 = 0x0001; /// Format tag for MPEG1
const BASIC_FLOAT: u16 = 0x0003; pub const WAVE_TAG_MPEG: u16 = 0x0050;
const BASIC_MPEG: u16 = 0x0050;
const BASIC_EXTENDED: u16 = 0xFFFE; /// Format tag indicating extended format
pub const WAVE_TAG_EXTENDED: u16 = 0xFFFE;
/* RC 2361 §4: /* RC 2361 §4:
@@ -22,65 +23,93 @@ const BASIC_EXTENDED: u16 = 0xFFFE;
*/ */
const UUID_PCM: Uuid = Uuid::from_bytes([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, /// Extended format UUID for integer PCM
0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71]); pub const WAVE_UUID_PCM: Uuid = Uuid::from_bytes([
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71,
]);
const UUID_FLOAT: Uuid = Uuid::from_bytes([0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, /// Extended format UUID for float PCM
0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71]); pub const WAVE_UUID_FLOAT: Uuid = Uuid::from_bytes([
0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71,
]);
const UUID_MPEG: Uuid = Uuid::from_bytes([0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, /// Extended format UUID for MPEG1 data
0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71]); pub const WAVE_UUID_MPEG: Uuid = Uuid::from_bytes([
0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71,
]);
const UUID_BFORMAT_PCM: Uuid = Uuid::from_bytes([0x01, 0x00, 0x00, 0x00, 0x21, 0x07, 0xd3, 0x11, /// Extended format for integer Ambisonic B-Format
0x86, 0x44, 0xc8, 0xc1, 0xca, 0x00, 0x00, 0x00]); pub const WAVE_UUID_BFORMAT_PCM: Uuid = Uuid::from_bytes([
0x01, 0x00, 0x00, 0x00, 0x21, 0x07, 0xd3, 0x11, 0x86, 0x44, 0xc8, 0xc1, 0xca, 0x00, 0x00, 0x00,
const UUID_BFORMAT_FLOAT: Uuid = Uuid::from_bytes([0x03, 0x00, 0x00, 0x00, 0x21, 0x07, 0xd3, 0x11, ]);
0x86, 0x44, 0xc8, 0xc1, 0xca, 0x00, 0x00, 0x00]);
/// Extended format for float Ambisonic B-Format
pub const WAVE_UUID_BFORMAT_FLOAT: Uuid = Uuid::from_bytes([
0x03, 0x00, 0x00, 0x00, 0x21, 0x07, 0xd3, 0x11, 0x86, 0x44, 0xc8, 0xc1, 0xca, 0x00, 0x00, 0x00,
]);
/// Generate an extended format UUID for the given basic format tag from [WaveFmt::tag].
fn uuid_from_basic_tag(tag: u16) -> Uuid { fn uuid_from_basic_tag(tag: u16) -> Uuid {
let tail: [u8; 6] = [0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71]; let tail: [u8; 6] = [0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71];
Uuid::from_fields_le(tag as u32, 0x0000, 0x0010, &tail).unwrap() Uuid::from_fields_le(tag as u32, 0x0000, 0x0010, &tail).unwrap()
} }
/// Sample format of the Wave file. /// Sample format of the Wave file.
///
///
#[derive(Debug, Copy, Clone, PartialEq)] #[derive(Debug, Copy, Clone, PartialEq)]
pub enum CommonFormat { pub enum CommonFormat {
/// Integer linear PCM
IntegerPCM, IntegerPCM,
/// IEEE Floating-point Linear PCM
IeeeFloatPCM, IeeeFloatPCM,
/// MPEG
Mpeg, Mpeg,
/// Ambisonic B-Format Linear PCM
AmbisonicBFormatIntegerPCM, AmbisonicBFormatIntegerPCM,
/// Ambisonic B-Format Float PCM
AmbisonicBFormatIeeeFloatPCM, AmbisonicBFormatIeeeFloatPCM,
/// An unknown format identified by a basic format tag.
UnknownBasic(u16), UnknownBasic(u16),
/// An unknown format identified by an extension UUID.
UnknownExtended(Uuid), UnknownExtended(Uuid),
} }
impl CommonFormat { impl CommonFormat {
/// Resolve a tag and Uuid to a `CommonFormat`.
pub fn make(basic: u16, uuid: Option<Uuid>) -> Self { pub fn make(basic: u16, uuid: Option<Uuid>) -> Self {
match (basic, uuid) { match (basic, uuid) {
(BASIC_PCM, _) => Self::IntegerPCM, (WAVE_TAG_PCM, _) => Self::IntegerPCM,
(BASIC_FLOAT, _) => Self::IeeeFloatPCM, (WAVE_TAG_FLOAT, _) => Self::IeeeFloatPCM,
(BASIC_MPEG, _) => Self::Mpeg, (WAVE_TAG_MPEG, _) => Self::Mpeg,
(BASIC_EXTENDED, Some(UUID_PCM)) => Self::IntegerPCM, (WAVE_TAG_EXTENDED, Some(WAVE_UUID_PCM)) => Self::IntegerPCM,
(BASIC_EXTENDED, Some(UUID_FLOAT))=> Self::IeeeFloatPCM, (WAVE_TAG_EXTENDED, Some(WAVE_UUID_FLOAT)) => Self::IeeeFloatPCM,
(BASIC_EXTENDED, Some(UUID_BFORMAT_PCM)) => Self::AmbisonicBFormatIntegerPCM, (WAVE_TAG_EXTENDED, Some(WAVE_UUID_BFORMAT_PCM)) => Self::AmbisonicBFormatIntegerPCM,
(BASIC_EXTENDED, Some(UUID_BFORMAT_FLOAT)) => Self::AmbisonicBFormatIeeeFloatPCM, (WAVE_TAG_EXTENDED, Some(WAVE_UUID_BFORMAT_FLOAT)) => {
(BASIC_EXTENDED, Some(x)) => CommonFormat::UnknownExtended(x), Self::AmbisonicBFormatIeeeFloatPCM
(x, _) => CommonFormat::UnknownBasic(x) }
(WAVE_TAG_EXTENDED, Some(x)) => CommonFormat::UnknownExtended(x),
(x, _) => CommonFormat::UnknownBasic(x),
} }
} }
/// Get the appropriate tag and `Uuid` for the callee.
///
/// If there is no appropriate tag for the format of the callee, the
/// returned tag will be 0xFFFE and the `Uuid` will describe the format.
pub fn take(self) -> (u16, Uuid) { pub fn take(self) -> (u16, Uuid) {
match self { match self {
Self::IntegerPCM => (BASIC_PCM, UUID_PCM), Self::IntegerPCM => (WAVE_TAG_PCM, WAVE_UUID_PCM),
Self::IeeeFloatPCM => (BASIC_FLOAT, UUID_FLOAT), Self::IeeeFloatPCM => (WAVE_TAG_FLOAT, WAVE_UUID_FLOAT),
Self::Mpeg => (BASIC_MPEG, UUID_MPEG), Self::Mpeg => (WAVE_TAG_MPEG, WAVE_UUID_MPEG),
Self::AmbisonicBFormatIntegerPCM => (BASIC_EXTENDED, UUID_BFORMAT_PCM), Self::AmbisonicBFormatIntegerPCM => (WAVE_TAG_EXTENDED, WAVE_UUID_BFORMAT_PCM),
Self::AmbisonicBFormatIeeeFloatPCM => (BASIC_EXTENDED, UUID_BFORMAT_FLOAT), Self::AmbisonicBFormatIeeeFloatPCM => (WAVE_TAG_EXTENDED, WAVE_UUID_BFORMAT_FLOAT),
Self::UnknownBasic(x) => (x, uuid_from_basic_tag(x)), Self::UnknownBasic(x) => (x, uuid_from_basic_tag(x)),
Self::UnknownExtended(x) => ( BASIC_EXTENDED, x) Self::UnknownExtended(x) => (WAVE_TAG_EXTENDED, x),
} }
} }
} }

404
src/cue.rs Normal file
View File

@@ -0,0 +1,404 @@
#![allow(dead_code)]
use super::fourcc::{
FourCC, ReadFourCC, WriteFourCC, ADTL_SIG, DATA_SIG, LABL_SIG, LTXT_SIG, NOTE_SIG,
};
use super::list_form::collect_list_form;
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use encoding::all::ASCII;
use encoding::Encoding;
use encoding::{DecoderTrap, EncoderTrap};
use std::io::{Cursor, Error, Read, Write};
#[derive(Copy, Clone, Debug)]
struct RawCue {
cue_point_id: u32,
frame: u32,
chunk_id: FourCC,
chunk_start: u32,
block_start: u32,
frame_offset: u32,
}
impl RawCue {
fn write_to(cues: Vec<Self>) -> Vec<u8> {
let mut writer = Cursor::new(vec![0u8; 0]);
writer.write_u32::<LittleEndian>(cues.len() as u32).unwrap();
for cue in cues.iter() {
writer.write_u32::<LittleEndian>(cue.cue_point_id).unwrap();
writer.write_u32::<LittleEndian>(cue.frame).unwrap();
writer.write_fourcc(cue.chunk_id).unwrap();
writer.write_u32::<LittleEndian>(cue.chunk_start).unwrap();
writer.write_u32::<LittleEndian>(cue.block_start).unwrap();
writer.write_u32::<LittleEndian>(cue.frame_offset).unwrap();
}
writer.into_inner()
}
fn read_from(data: &[u8]) -> Result<Vec<Self>, Error> {
let mut rdr = Cursor::new(data);
let count = rdr.read_u32::<LittleEndian>()?;
let mut retval: Vec<Self> = vec![];
for _ in 0..count {
retval.push(Self {
cue_point_id: rdr.read_u32::<LittleEndian>()?,
frame: rdr.read_u32::<LittleEndian>()?,
chunk_id: rdr.read_fourcc()?,
chunk_start: rdr.read_u32::<LittleEndian>()?,
block_start: rdr.read_u32::<LittleEndian>()?,
frame_offset: rdr.read_u32::<LittleEndian>()?,
})
}
Ok(retval)
}
}
#[derive(Clone, Debug)]
struct RawLabel {
cue_point_id: u32,
text: Vec<u8>,
}
impl RawLabel {
fn write_to(&self) -> Vec<u8> {
let mut writer = Cursor::new(vec![0u8; 0]);
writer.write_u32::<LittleEndian>(self.cue_point_id).unwrap();
writer.write_all(&self.text).unwrap();
writer.into_inner()
}
fn read_from(data: &[u8]) -> Result<Self, Error> {
let mut rdr = Cursor::new(data);
let length = data.len();
Ok(Self {
cue_point_id: rdr.read_u32::<LittleEndian>()?,
text: {
let mut buf = vec![0u8; length - 4];
rdr.read_exact(&mut buf)?;
buf
},
})
}
}
#[derive(Clone, Debug)]
struct RawNote {
cue_point_id: u32,
text: Vec<u8>,
}
impl RawNote {
fn write_to(&self) -> Vec<u8> {
let mut writer = Cursor::new(vec![0u8; 0]);
writer.write_u32::<LittleEndian>(self.cue_point_id).unwrap();
writer.write_all(&self.text).unwrap();
writer.into_inner()
}
fn read_from(data: &[u8]) -> Result<Self, Error> {
let mut rdr = Cursor::new(data);
let length = data.len();
Ok(Self {
cue_point_id: rdr.read_u32::<LittleEndian>()?,
text: {
let mut buf = vec![0u8; length - 4];
rdr.read_exact(&mut buf)?;
buf
},
})
}
}
#[derive(Clone, Debug)]
struct RawLtxt {
cue_point_id: u32,
frame_length: u32,
purpose: FourCC,
country: u16,
language: u16,
dialect: u16,
code_page: u16,
text: Option<Vec<u8>>,
}
impl RawLtxt {
fn write_to(&self) -> Vec<u8> {
let mut writer = Cursor::new(vec![0u8; 0]);
writer.write_u32::<LittleEndian>(self.cue_point_id).unwrap();
writer.write_u32::<LittleEndian>(self.frame_length).unwrap();
writer.write_fourcc(self.purpose).unwrap();
writer.write_u16::<LittleEndian>(self.country).unwrap();
writer.write_u16::<LittleEndian>(self.language).unwrap();
writer.write_u16::<LittleEndian>(self.dialect).unwrap();
writer.write_u16::<LittleEndian>(self.code_page).unwrap();
if let Some(ext_text) = &self.text {
writer.write_all(ext_text).unwrap();
}
writer.into_inner()
}
fn read_from(data: &[u8]) -> Result<Self, Error> {
let mut rdr = Cursor::new(data);
let length = data.len();
Ok(Self {
cue_point_id: rdr.read_u32::<LittleEndian>()?,
frame_length: rdr.read_u32::<LittleEndian>()?,
purpose: rdr.read_fourcc()?,
country: rdr.read_u16::<LittleEndian>()?,
language: rdr.read_u16::<LittleEndian>()?,
dialect: rdr.read_u16::<LittleEndian>()?,
code_page: rdr.read_u16::<LittleEndian>()?,
text: {
if length - 20 > 0 {
let mut buf = vec![0u8; length - 20];
rdr.read_exact(&mut buf)?;
Some(buf)
} else {
None
}
},
})
}
}
#[derive(Clone, Debug)]
enum RawAdtlMember {
Label(RawLabel),
Note(RawNote),
LabeledText(RawLtxt),
Unrecognized(FourCC),
}
impl RawAdtlMember {
fn compile_adtl(members: &[Self]) -> Vec<u8> {
let mut w = Cursor::new(vec![0u8; 0]);
// It seems like all this casing could be done with traits
for member in members.iter() {
let (fcc, buf) = match member {
RawAdtlMember::Label(l) => (LABL_SIG, l.write_to()),
RawAdtlMember::Note(n) => (NOTE_SIG, n.write_to()),
RawAdtlMember::LabeledText(t) => (LTXT_SIG, t.write_to()),
RawAdtlMember::Unrecognized(f) => (*f, vec![0u8; 0]), // <-- this is a dopey case but here for completeness
};
w.write_fourcc(fcc).unwrap();
w.write_u32::<LittleEndian>(buf.len() as u32).unwrap();
w.write_all(&buf).unwrap();
if buf.len() % 2 == 1 {
w.write_u8(0).unwrap();
}
}
let chunk_content = w.into_inner();
let mut writer = Cursor::new(vec![0u8; 0]);
writer.write_fourcc(ADTL_SIG).unwrap();
writer
.write_u32::<LittleEndian>(chunk_content.len() as u32)
.unwrap();
writer.write_all(&chunk_content).unwrap();
writer.into_inner()
}
fn collect_from(chunk: &[u8]) -> Result<Vec<RawAdtlMember>, Error> {
let chunks = collect_list_form(chunk)?;
let mut retval: Vec<RawAdtlMember> = vec![];
for chunk in chunks.iter() {
retval.push(match chunk.signature {
LABL_SIG => RawAdtlMember::Label(RawLabel::read_from(&chunk.contents)?),
NOTE_SIG => RawAdtlMember::Note(RawNote::read_from(&chunk.contents)?),
LTXT_SIG => RawAdtlMember::LabeledText(RawLtxt::read_from(&chunk.contents)?),
x => RawAdtlMember::Unrecognized(x),
})
}
Ok(retval)
}
}
trait AdtlMemberSearch {
fn labels_for_cue_point(&self, id: u32) -> Vec<&RawLabel>;
fn notes_for_cue_point(&self, id: u32) -> Vec<&RawNote>;
fn ltxt_for_cue_point(&self, id: u32) -> Vec<&RawLtxt>;
}
impl AdtlMemberSearch for Vec<RawAdtlMember> {
fn labels_for_cue_point(&self, id: u32) -> Vec<&RawLabel> {
self.iter()
.filter_map(|item| match item {
RawAdtlMember::Label(x) if x.cue_point_id == id => Some(x),
_ => None,
})
.collect()
}
fn notes_for_cue_point(&self, id: u32) -> Vec<&RawNote> {
self.iter()
.filter_map(|item| match item {
RawAdtlMember::Note(x) if x.cue_point_id == id => Some(x),
_ => None,
})
.collect()
}
fn ltxt_for_cue_point(&self, id: u32) -> Vec<&RawLtxt> {
self.iter()
.filter_map(|item| match item {
RawAdtlMember::LabeledText(x) if x.cue_point_id == id => Some(x),
_ => None,
})
.collect()
}
}
/// A cue point recorded in the `cue` and `adtl` metadata.
///
/// ## Resources
/// - [Cue list, label and other metadata](https://sites.google.com/site/musicgapi/technical-documents/wav-file-format#smpl)
///
/// ### Not Implemented
/// - [EBU 3285 Supplement 2](https://tech.ebu.ch/docs/tech/tech3285s2.pdf) (July 2001): Quality chunk and cuesheet
pub struct Cue {
/// The time of this marker
pub frame: u32,
/// The length of this marker, if it is a range
pub length: Option<u32>,
/// The text "label"/name of this marker if provided
pub label: Option<String>,
/// The text "note"/comment of this marker if provided
pub note: Option<String>,
/// The offser of this marker
///
/// **Note:** Applications use the `frame` and `offset` fields
/// in different ways. iZotope RX Audio Editor writes the
/// marker position to *both* fields, while a Sound Devices
/// recorder writes the marker position to *only* the `offset`
/// field.
pub offset: u32,
}
fn convert_to_cue_string(buffer: &[u8]) -> String {
let trimmed: Vec<u8> = buffer.iter().take_while(|c| **c != 0_u8).cloned().collect();
ASCII
.decode(&trimmed, DecoderTrap::Ignore)
.expect("Error decoding text")
}
fn convert_from_cue_string(val: &str) -> Vec<u8> {
ASCII
.encode(val, EncoderTrap::Ignore)
.expect("Error encoding text")
}
impl Cue {
/// Take a list of `Cue`s and convert it into `RawCue` and `RawAdtlMember`s
fn compile_to(cues: &[Cue]) -> (Vec<RawCue>, Vec<RawAdtlMember>) {
cues.iter()
.enumerate()
.map(|(n, cue)| {
let raw_cue = RawCue {
cue_point_id: n as u32,
frame: cue.frame,
chunk_id: DATA_SIG,
chunk_start: 0,
block_start: 0,
frame_offset: cue.offset,
};
let raw_label = cue.label.as_ref().map(|val| RawLabel {
cue_point_id: n as u32,
text: convert_from_cue_string(val),
});
let raw_note = cue.note.as_ref().map(|val| RawNote {
cue_point_id: n as u32,
text: convert_from_cue_string(val),
});
let raw_ltxt = cue.length.map(|val| RawLtxt {
cue_point_id: n as u32,
frame_length: val,
purpose: FourCC::make(b"rgn "),
country: 0,
language: 0,
dialect: 0,
code_page: 0,
text: None,
});
(raw_cue, raw_label, raw_note, raw_ltxt)
})
.fold(
(Vec::<RawCue>::new(), Vec::<RawAdtlMember>::new()),
|(mut cues, mut adtls), (cue, label, note, ltxt)| {
cues.push(cue);
if let Some(l) = label {
adtls.push(RawAdtlMember::Label(l))
}
if let Some(n) = note {
adtls.push(RawAdtlMember::Note(n))
}
if let Some(m) = ltxt {
adtls.push(RawAdtlMember::LabeledText(m))
}
(cues, adtls)
},
)
}
pub fn collect_from(cue_chunk: &[u8], adtl_chunk: Option<&[u8]>) -> Result<Vec<Cue>, Error> {
let raw_cues = RawCue::read_from(cue_chunk)?;
let raw_adtl: Vec<RawAdtlMember>;
if let Some(adtl) = adtl_chunk {
raw_adtl = RawAdtlMember::collect_from(adtl)?;
} else {
raw_adtl = vec![];
}
Ok(raw_cues
.iter()
.map(|i| {
Cue {
//ident : i.cue_point_id,
frame: i.frame,
length: {
raw_adtl
.ltxt_for_cue_point(i.cue_point_id)
.first()
.filter(|x| x.purpose == FourCC::make(b"rgn "))
.map(|x| x.frame_length)
},
label: {
raw_adtl
.labels_for_cue_point(i.cue_point_id)
.iter()
.map(|s| convert_to_cue_string(&s.text))
.next()
},
note: {
raw_adtl
.notes_for_cue_point(i.cue_point_id)
.iter()
//.filter_map(|x| str::from_utf8(&x.text).ok())
.map(|s| convert_to_cue_string(&s.text))
.next()
},
offset: i.frame_offset,
}
})
.collect())
}
}

View File

@@ -1,12 +1,13 @@
use std::io;
use super::fourcc::FourCC; use super::fourcc::FourCC;
use std::error::Error as StdError;
use uuid; use std::{
fmt::{Debug, Display},
io,
};
/// Errors returned by methods in this crate. /// Errors returned by methods in this crate.
#[derive(Debug)] #[derive(Debug)]
pub enum Error { pub enum Error {
/// An `io::Error` occurred /// An `io::Error` occurred
IOError(io::Error), IOError(io::Error),
@@ -41,8 +42,20 @@ pub enum Error {
/// The file is not optimized for writing new data /// The file is not optimized for writing new data
DataChunkNotPreparedForAppend, DataChunkNotPreparedForAppend,
/// A buffer with a length that isn't a multiple of channel_count was provided
InvalidBufferSize {
buffer_size: usize,
channel_count: u16,
},
} }
impl StdError for Error {}
impl Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Debug::fmt(self, f)
}
}
impl From<io::Error> for Error { impl From<io::Error> for Error {
fn from(error: io::Error) -> Error { fn from(error: io::Error) -> Error {

View File

@@ -1,48 +1,54 @@
use std::convert::TryFrom; use crate::common_format::{CommonFormat, WAVE_UUID_BFORMAT_PCM, WAVE_UUID_PCM};
use crate::Sample;
use std::io::Cursor;
use uuid::Uuid; use uuid::Uuid;
use super::errors::Error;
use super::common_format::CommonFormat;
use byteorder::LittleEndian;
use byteorder::ReadBytesExt;
/// ADM Audio ID record // Need more test cases for ADMAudioID
#[allow(dead_code)]
/// ADM Audio ID record.
/// ///
/// This structure relates a channel in the wave file to either a common ADM /// This structure relates a channel in the wave file to either a common ADM
/// channel definition or further definition in the WAV file's ADM metadata /// channel definition or further definition in the WAV file's ADM metadata
/// chunk. /// chunk.
/// ///
/// An individual channel in a WAV file can have multiple Audio IDs in an ADM /// An individual channel in a WAV file can have multiple Audio IDs in an ADM
/// AudioProgramme. /// `AudioProgramme`.
/// ///
/// See BS.2088-1 § 8, also BS.2094, also blahblahblah... /// See BS.2088-1 § 8, also BS.2094, also blahblahblah...
#[derive(Debug)]
pub struct ADMAudioID { pub struct ADMAudioID {
track_uid: [char; 12], pub track_uid: [char; 12],
channel_format_ref: [char; 14], pub channel_format_ref: [char; 14],
pack_ref: [char; 11] pub pack_ref: [char; 11],
} }
/// Describes a single channel in a WAV file. /// Describes a single channel in a WAV file.
///
/// This information is correlated from the Wave format ChannelMap field and
/// the `chna` chunk, if present.
#[derive(Debug)]
pub struct ChannelDescriptor { pub struct ChannelDescriptor {
/// Index, the offset of this channel's samples in one frame. /// Index, the offset of this channel's samples in one frame.
index: u16, pub index: u16,
/// Channel assignment /// Channel assignment
/// ///
/// This is either implied (in the case of mono or stereo wave files) or /// This is either implied (in the case of mono or stereo wave files) or
/// explicitly given in `WaveFormatExtentended` for files with more tracks. /// explicitly given in `WaveFormatExtentended` for files with more tracks.
speaker: ChannelMask, pub speaker: ChannelMask,
/// ADM audioTrackUIDs /// ADM audioTrackUIDs
adm_track_audio_ids: Vec<ADMAudioID>, pub adm_track_audio_ids: Vec<ADMAudioID>,
} }
/// A bitmask indicating which channels are present in
/// the file.
/* ///
https://docs.microsoft.com/en-us/windows-hardware/drivers/audio/subformat-guids-for-compressed-audio-formats
These are from http://dream.cs.bath.ac.uk/researchdev/wave-ex/mulchaud.rtf
*/
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub enum ChannelMask { pub enum ChannelMask {
DirectOut = 0x0, DirectOut = 0x0,
@@ -67,7 +73,6 @@ pub enum ChannelMask {
} }
impl From<u32> for ChannelMask { impl From<u32> for ChannelMask {
fn from(value: u32) -> Self { fn from(value: u32) -> Self {
match value { match value {
0x1 => Self::FrontLeft, 0x1 => Self::FrontLeft,
@@ -88,7 +93,7 @@ impl From<u32> for ChannelMask {
0x8000 => Self::TopBackLeft, 0x8000 => Self::TopBackLeft,
0x10000 => Self::TopBackCenter, 0x10000 => Self::TopBackCenter,
0x20000 => Self::TopBackRight, 0x20000 => Self::TopBackRight,
_ => Self::DirectOut _ => Self::DirectOut,
} }
} }
} }
@@ -99,9 +104,10 @@ impl ChannelMask {
if (input_mask & reserved_mask) > 0 { if (input_mask & reserved_mask) > 0 {
vec![ChannelMask::DirectOut; channel_count as usize] vec![ChannelMask::DirectOut; channel_count as usize]
} else { } else {
(0..18).map(|i| 1 << i ) (0..18)
.map(|i| 1 << i)
.filter(|mask| mask & input_mask > 0) .filter(|mask| mask & input_mask > 0)
.map(|mask| Into::<ChannelMask>::into(mask)) .map(ChannelMask::from)
.collect() .collect()
} }
} }
@@ -110,11 +116,11 @@ impl ChannelMask {
/** /**
* Extended Wave Format * Extended Wave Format
* *
* https://docs.microsoft.com/en-us/windows/win32/api/mmreg/ns-mmreg-waveformatextensible * Resources:
* * [WAVEFORMATEXTENSIBLE structure](https://docs.microsoft.com/en-us/windows/win32/api/mmreg/ns-mmreg-waveformatextensible)
*/ */
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]
pub struct WaveFmtExtended { pub struct WaveFmtExtended {
/// Valid bits per sample /// Valid bits per sample
pub valid_bits_per_sample: u16, pub valid_bits_per_sample: u16,
@@ -129,27 +135,43 @@ pub struct WaveFmtExtended {
pub type_guid: Uuid, pub type_guid: Uuid,
} }
/** ///
* WAV file data format record. /// WAV file data format record.
* ///
* The `fmt` record contains essential information describing the binary /// The `fmt` record contains essential information describing the binary
* structure of the data segment of the WAVE file, such as sample /// structure of the data segment of the WAVE file, such as sample
* rate, sample binary format, channel count, etc. /// rate, sample binary format, channel count, etc.
* ///
*/ ///
/// ## Resources
///
/// ### Implementation of Wave format `fmt` chunk
/// - [MSDN WAVEFORMATEX](https://docs.microsoft.com/en-us/windows/win32/api/mmeapi/ns-mmeapi-waveformatex)
/// - [MSDN WAVEFORMATEXTENSIBLE](https://docs.microsoft.com/en-us/windows/win32/api/mmreg/ns-mmreg-waveformatextensible)
///
/// ### Other resources
/// - [RFC 3261][rfc3261] (June 1998) "WAVE and AVI Codec Registries"
/// - [Sampler Metadata](http://www.piclist.com/techref/io/serial/midi/wave.html)
/// - [Audio File Format Specifications](http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html) (September 2022) Prof. Peter Kabal, MMSP Lab, ECE, McGill University
/// - [Multimedia Programming Interface and Data Specifications 1.0](http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/Docs/riffmci.pdf)
/// (August 1991), IBM Corporation and Microsoft Corporation
///
/// [rfc3261]: https://tools.ietf.org/html/rfc2361
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]
pub struct WaveFmt { pub struct WaveFmt {
/// A tag identifying the codec in use. /// A tag identifying the codec in use.
/// ///
/// If this is 0xFFFE, the codec will be identified by a GUID /// If this is 0xFFFE, the codec will be identified by a GUID
/// in `extended_format` /// in [`extended_format`](WaveFmt::extended_format).
pub tag: u16, pub tag: u16,
/// Count of audio channels in each frame /// Count of audio channels in each frame
pub channel_count: u16, pub channel_count: u16,
/// Sample rate of the audio data /// Playback rate of the audio data
///
/// In frames per second.
pub sample_rate: u32, pub sample_rate: u32,
/// Count of bytes per second /// Count of bytes per second
@@ -163,80 +185,263 @@ pub struct WaveFmt {
pub block_alignment: u16, pub block_alignment: u16,
/// Count of bits stored in the file per sample /// Count of bits stored in the file per sample
///
/// By rule, `bits_per_sample % 8 == 0` for Broadcast-Wave files.
///
/// Modern clients will encode
/// unusual sample sizes in normal byte sizes but will set the valid_bits
/// flag in extended format record.
///
/// Generally speaking this will be true for all modern wave files, though
/// there was an historical "packed" stereo format of 20 bits per sample,
/// 5 bytes per frame, 5 bytes block alignment.
pub bits_per_sample: u16, pub bits_per_sample: u16,
/// Extended format description /// Extended format description
/// ///
/// Additional format metadata if `channel_count` is greater than 2, /// Additional format metadata if channel_count is greater than 2,
/// or if certain codecs are used. /// or if certain codecs are used.
pub extended_format: Option<WaveFmtExtended> pub extended_format: Option<WaveFmtExtended>,
} }
impl WaveFmt { impl WaveFmt {
pub fn valid_bits_per_sample(&self) -> u16 {
if let Some(ext) = self.extended_format {
ext.valid_bits_per_sample
} else {
self.bits_per_sample
}
}
/// Create a new integer PCM format `WaveFmt` /// Create a new integer PCM format for a monoaural audio stream.
pub fn new_pcm(sample_rate: u32, bits_per_sample: u16, channel_count: u16) -> Self { pub fn new_pcm_mono(sample_rate: u32, bits_per_sample: u16) -> Self {
Self::new_pcm_multichannel(sample_rate, bits_per_sample, 0x4)
}
/// Create a new integer PCM format for a standard Left-Right stereo audio
/// stream.
pub fn new_pcm_stereo(sample_rate: u32, bits_per_sample: u16) -> Self {
Self::new_pcm_multichannel(sample_rate, bits_per_sample, 0x3)
}
/// Create a new integer PCM format for ambisonic b-format.
pub fn new_pcm_ambisonic(sample_rate: u32, bits_per_sample: u16, channel_count: u16) -> Self {
let container_bits_per_sample = bits_per_sample + (bits_per_sample % 8); let container_bits_per_sample = bits_per_sample + (bits_per_sample % 8);
let container_bytes_per_sample = container_bits_per_sample / 8; let container_bytes_per_sample = container_bits_per_sample / 8;
let tag : u16 = match channel_count { WaveFmt {
1..=2 => 0x01, tag: 0xFFFE,
x if x > 2 => 0xFFFE, channel_count,
x => panic!("Invalid channel count {}", x) sample_rate,
bytes_per_second: container_bytes_per_sample as u32
* sample_rate
* channel_count as u32,
block_alignment: container_bytes_per_sample * channel_count,
bits_per_sample: container_bits_per_sample,
extended_format: Some(WaveFmtExtended {
valid_bits_per_sample: bits_per_sample,
channel_mask: ChannelMask::DirectOut as u32,
type_guid: WAVE_UUID_BFORMAT_PCM,
}),
}
}
/// Create a new integer PCM format [WaveFmt] with a custom channel bitmap.
///
/// The order of [channels](WaveFmt::channels) is not important. When reading or writing
/// audio frames you must use the standard multichannel order for Wave
/// files, the numerical order of the cases of [ChannelMask].
pub fn new_pcm_multichannel(
sample_rate: u32,
bits_per_sample: u16,
channel_bitmap: u32,
) -> Self {
let container_bits_per_sample = bits_per_sample + (bits_per_sample % 8);
let container_bytes_per_sample = container_bits_per_sample / 8;
let channel_count: u16 = (0..=31).fold(0u16, |accum, n| {
accum + (0x1 & (channel_bitmap >> n) as u16)
});
let result: (u16, Option<WaveFmtExtended>) = match channel_bitmap {
ch if bits_per_sample != container_bits_per_sample => (
0xFFFE,
Some(WaveFmtExtended {
valid_bits_per_sample: bits_per_sample,
channel_mask: ch,
type_guid: WAVE_UUID_PCM,
}),
),
0b0100 => (0x0001, None),
0b0011 => (0x0001, None),
ch => (
0xFFFE,
Some(WaveFmtExtended {
valid_bits_per_sample: bits_per_sample,
channel_mask: ch,
type_guid: WAVE_UUID_PCM,
}),
),
}; };
let (tag, extformat) = result;
WaveFmt { WaveFmt {
tag, tag,
channel_count, channel_count,
sample_rate, sample_rate,
bytes_per_second: container_bytes_per_sample as u32 * sample_rate * channel_count as u32, bytes_per_second: container_bytes_per_sample as u32
* sample_rate
* channel_count as u32,
block_alignment: container_bytes_per_sample * channel_count, block_alignment: container_bytes_per_sample * channel_count,
bits_per_sample: container_bits_per_sample, bits_per_sample: container_bits_per_sample,
extended_format: None extended_format: extformat,
} }
} }
/// Format or codec of the file's audio data.
///
/// The [CommonFormat] unifies the format tag and the format extension GUID. Use this
/// method to determine the codec.
pub fn common_format(&self) -> CommonFormat { pub fn common_format(&self) -> CommonFormat {
CommonFormat::make(self.tag, self.extended_format.map(|ext| ext.type_guid)) CommonFormat::make(self.tag, self.extended_format.map(|ext| ext.type_guid))
} }
/// Create a frame buffer sized to hold `length` frames for a reader or
/// writer
///
/// This is a conveneince method that creates a `Vec<i32>` with
/// as many elements as there are channels in the underlying stream.
pub fn create_frame_buffer<S: Sample>(&self, length: usize) -> Vec<S> {
vec![S::EQUILIBRIUM; self.channel_count as usize * length]
}
/// Create a raw byte buffer to hold `length` blocks from a reader or
/// writer
pub fn create_raw_buffer(&self, length: usize) -> Vec<u8> {
vec![0u8; self.block_alignment as usize * length]
}
/// Read bytes into frames
pub fn unpack_frames(&self, from_bytes: &[u8], into_frames: &mut [i32]) {
let mut rdr = Cursor::new(from_bytes);
for frame in into_frames {
*frame = match (self.valid_bits_per_sample(), self.bits_per_sample) {
(0..=8,8) => rdr.read_u8().unwrap() as i32 - 0x80_i32, // EBU 3285 §A2.2
(9..=16,16) => rdr.read_i16::<LittleEndian>().unwrap() as i32,
(10..=24,24) => rdr.read_i24::<LittleEndian>().unwrap(),
(25..=32,32) => rdr.read_i32::<LittleEndian>().unwrap(),
(b,_)=> panic!("Unrecognized integer format, bits per sample {}, channels {}, block_alignment {}",
b, self.channel_count, self.block_alignment)
}
}
}
/// Channel descriptors for each channel.
pub fn channels(&self) -> Vec<ChannelDescriptor> { pub fn channels(&self) -> Vec<ChannelDescriptor> {
match self.channel_count { match self.channel_count {
1 => vec![ 1 => vec![ChannelDescriptor {
ChannelDescriptor {
index: 0, index: 0,
speaker: ChannelMask::FrontCenter, speaker: ChannelMask::FrontCenter,
adm_track_audio_ids: vec![] adm_track_audio_ids: vec![],
} }],
],
2 => vec![ 2 => vec![
ChannelDescriptor { ChannelDescriptor {
index: 0, index: 0,
speaker: ChannelMask::FrontLeft, speaker: ChannelMask::FrontLeft,
adm_track_audio_ids: vec![] adm_track_audio_ids: vec![],
}, },
ChannelDescriptor { ChannelDescriptor {
index: 1, index: 1,
speaker: ChannelMask::FrontRight, speaker: ChannelMask::FrontRight,
adm_track_audio_ids: vec![] adm_track_audio_ids: vec![],
} },
], ],
x if x > 2 => { x if x > 2 => {
let channel_mask = self.extended_format.map(|x| x.channel_mask).unwrap_or(0); let channel_mask = self.extended_format.map(|x| x.channel_mask).unwrap_or(0);
let channels = ChannelMask::channels(channel_mask, self.channel_count); let channels = ChannelMask::channels(channel_mask, self.channel_count);
let channels_expanded = channels.iter().chain(std::iter::repeat(&ChannelMask::DirectOut)); let channels_expanded = channels
.iter()
.chain(std::iter::repeat(&ChannelMask::DirectOut));
(0..self.channel_count) (0..self.channel_count)
.zip(channels_expanded) .zip(channels_expanded)
.map(|(n, chan)| ChannelDescriptor { .map(|(n, chan)| ChannelDescriptor {
index: n, index: n,
speaker: *chan, speaker: *chan,
adm_track_audio_ids: vec![] adm_track_audio_ids: vec![],
}).collect() })
}, .collect()
}
x => panic!("Channel count ({}) was illegal!", x), x => panic!("Channel count ({}) was illegal!", x),
} }
} }
} }
pub trait ReadWavAudioData {
/// Read audio data from the receiver as interleaved [i32] samples.
fn read_i32_frames(
&mut self,
format: WaveFmt,
into: &mut [i32],
) -> Result<usize, std::io::Error>;
fn read_f32_frames(
&mut self,
format: WaveFmt,
into: &mut [f32],
) -> Result<usize, std::io::Error>;
}
impl<T> ReadWavAudioData for T
where
T: std::io::Read,
{
/// # Panics:
/// * If the format's [valid bits per sample](WaveFmt::valid_bits_per_sample) is
/// not compatible with the format's [bits per sample](WaveFmt::bits_per_sample).
fn read_i32_frames(
&mut self,
format: WaveFmt,
into: &mut [i32],
) -> Result<usize, std::io::Error> {
assert!(into.len() % format.channel_count as usize == 0);
for frame in into {
*frame = match (format.valid_bits_per_sample(), format.bits_per_sample) {
(0..=8,8) => self.read_u8().unwrap() as i32 - 0x80_i32, // EBU 3285 §A2.2
(9..=16,16) => self.read_i16::<LittleEndian>().unwrap() as i32,
(10..=24,24) => self.read_i24::<LittleEndian>().unwrap(),
(25..=32,32) => self.read_i32::<LittleEndian>().unwrap(),
(b,_)=> panic!("Unrecognized integer format, bits per sample {}, channels {}, block_alignment {}",
b, format.channel_count, format.block_alignment)
}
}
todo!()
}
fn read_f32_frames(
&mut self,
format: WaveFmt,
into: &mut [f32],
) -> Result<usize, std::io::Error> {
assert!(into.len() % format.channel_count as usize == 0);
todo!()
}
}
trait WriteWavAudioData {
fn write_i32_frames(&mut self, format: WaveFmt, from: &[i32]) -> Result<usize, std::io::Error>;
fn write_f32_frames(&mut self, format: WaveFmt, from: &[f32]) -> Result<usize, std::io::Error>;
}
impl<T> WriteWavAudioData for T
where
T: std::io::Write,
{
fn write_i32_frames(&mut self, _format: WaveFmt, _: &[i32]) -> Result<usize, std::io::Error> {
todo!()
}
fn write_f32_frames(&mut self, _format: WaveFmt, _: &[f32]) -> Result<usize, std::io::Error> {
todo!()
}
}

View File

@@ -16,7 +16,12 @@ impl FourCC {
impl From<[char; 4]> for FourCC { impl From<[char; 4]> for FourCC {
fn from(chars: [char; 4]) -> Self { fn from(chars: [char; 4]) -> Self {
Self([chars[0] as u8 , chars[1] as u8, chars[2] as u8, chars[3] as u8]) Self([
chars[0] as u8,
chars[1] as u8,
chars[2] as u8,
chars[3] as u8,
])
} }
} }
@@ -32,20 +37,28 @@ impl From<FourCC> for [u8; 4] {
} }
} }
impl From<&FourCC> for [char; 4] { impl From<&FourCC> for [char; 4] {
fn from(f: &FourCC) -> Self { fn from(f: &FourCC) -> Self {
[f.0[0] as char, f.0[1] as char, f.0[2] as char, f.0[3] as char,] [
f.0[0] as char,
f.0[1] as char,
f.0[2] as char,
f.0[3] as char,
]
} }
} }
impl From<FourCC> for [char; 4] { impl From<FourCC> for [char; 4] {
fn from(f: FourCC) -> Self { fn from(f: FourCC) -> Self {
[f.0[0] as char, f.0[1] as char, f.0[2] as char, f.0[3] as char,] [
f.0[0] as char,
f.0[1] as char,
f.0[2] as char,
f.0[3] as char,
]
} }
} }
impl From<&FourCC> for String { impl From<&FourCC> for String {
fn from(f: &FourCC) -> Self { fn from(f: &FourCC) -> Self {
let chars: [char; 4] = f.into(); let chars: [char; 4] = f.into();
@@ -75,7 +88,10 @@ pub trait WriteFourCC: io::Write {
fn write_fourcc(&mut self, fourcc: FourCC) -> Result<(), io::Error>; fn write_fourcc(&mut self, fourcc: FourCC) -> Result<(), io::Error>;
} }
impl<T> ReadFourCC for T where T: io::Read { impl<T> ReadFourCC for T
where
T: io::Read,
{
fn read_fourcc(&mut self) -> Result<FourCC, io::Error> { fn read_fourcc(&mut self) -> Result<FourCC, io::Error> {
let mut buf: [u8; 4] = [0; 4]; let mut buf: [u8; 4] = [0; 4];
self.read_exact(&mut buf)?; self.read_exact(&mut buf)?;
@@ -83,7 +99,10 @@ impl<T> ReadFourCC for T where T: io::Read {
} }
} }
impl<T> WriteFourCC for T where T: io::Write { impl<T> WriteFourCC for T
where
T: io::Write,
{
fn write_fourcc(&mut self, fourcc: FourCC) -> Result<(), io::Error> { fn write_fourcc(&mut self, fourcc: FourCC) -> Result<(), io::Error> {
let buf: [u8; 4] = fourcc.into(); let buf: [u8; 4] = fourcc.into();
self.write_all(&buf)?; self.write_all(&buf)?;
@@ -91,7 +110,6 @@ impl<T> WriteFourCC for T where T: io::Write {
} }
} }
pub const RIFF_SIG: FourCC = FourCC::make(b"RIFF"); pub const RIFF_SIG: FourCC = FourCC::make(b"RIFF");
pub const WAVE_SIG: FourCC = FourCC::make(b"WAVE"); pub const WAVE_SIG: FourCC = FourCC::make(b"WAVE");
pub const RF64_SIG: FourCC = FourCC::make(b"RF64"); pub const RF64_SIG: FourCC = FourCC::make(b"RF64");
@@ -102,10 +120,20 @@ pub const DATA_SIG: FourCC = FourCC::make(b"data");
pub const FMT__SIG: FourCC = FourCC::make(b"fmt "); pub const FMT__SIG: FourCC = FourCC::make(b"fmt ");
pub const BEXT_SIG: FourCC = FourCC::make(b"bext"); pub const BEXT_SIG: FourCC = FourCC::make(b"bext");
//pub const FACT_SIG: FourCC = FourCC::make(b"fact");
pub const IXML_SIG: FourCC = FourCC::make(b"iXML");
pub const AXML_SIG: FourCC = FourCC::make(b"axml");
pub const JUNK_SIG: FourCC = FourCC::make(b"JUNK"); pub const JUNK_SIG: FourCC = FourCC::make(b"JUNK");
pub const FLLR_SIG: FourCC = FourCC::make(b"FLLR"); pub const FLLR_SIG: FourCC = FourCC::make(b"FLLR");
pub const ELM1_SIG: FourCC = FourCC::make(b"elm1");
pub const LIST_SIG: FourCC = FourCC::make(b"LIST");
pub const CUE__SIG: FourCC = FourCC::make(b"cue ");
pub const ADTL_SIG: FourCC = FourCC::make(b"adtl");
pub const LABL_SIG: FourCC = FourCC::make(b"labl");
pub const NOTE_SIG: FourCC = FourCC::make(b"note");
pub const LTXT_SIG: FourCC = FourCC::make(b"ltxt");
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {

3
src/levl.rs Normal file
View File

@@ -0,0 +1,3 @@
/// Resources
///
/// [EBU 3285 Supplement 3](https://tech.ebu.ch/docs/tech/tech3285s3.pdf) (July 2001): Peak Metadata

View File

@@ -3,9 +3,8 @@
Rust Wave File Reader/Writer with Broadcast-WAV, MBWF and RF64 Support Rust Wave File Reader/Writer with Broadcast-WAV, MBWF and RF64 Support
__(Note: This crate is still in an alpha or pre-alpha stage of development. Reading of Refer to the individual modules for relevant documentation. For opening
files works however the interfaces may change significantly. Stay up-to-date on the and writing files begin with [WaveReader] and [WaveWriter] respectively.
status of this project at [Github][github].)__
## Objectives and Roadmap ## Objectives and Roadmap
@@ -15,112 +14,45 @@ production.
Apps we test against: Apps we test against:
- Avid Pro Tools - Avid Pro Tools
- iZotope RX Audio Editor
- FFMpeg - FFMpeg
- Audacity - Audacity
- Sound Devices field recorders: 702T, MixPre-10 II
Wave features we want to support with maximum reliability and ease of use:
- Large file size, RF64 support
- Multichannel audio formats
- Embedded metadata
In addition to reading the audio, we want to support all of the different
metadata planes you are liable to need to use.
- Broadcast-WAV metadata (including the SMPTE UMID and EBU v2 extensions)
- iXML Production recorder metadata
- ADM XML (with associated `chna` mappings)
- Dolby metadata block
Things that are _not_ necessarily in the scope of this package:
- Broad codec support. There are a little more than one-hundred
[registered wave codecs][rfc3261], but because this library is targeting
professional formats being created today, we only plan on supporting
two of them: tag 0x0001 (Integer Linear PCM) and tag 0x0003 (IEEE Float
Linear PCM).
- Music library metadata. There are several packages that can read ID3
metadata and it's not particuarly common in wave files in any case. INFO
metadata is more common though in professional applications it tends not
to be used by many applications.
## Resources
### Implementation of Broadcast Wave Files
- [EBU Tech 3285][ebu3285] (May 2011), "Specification of the Broadcast Wave Format (BWF)"
- [Supplement 1](https://tech.ebu.ch/docs/tech/tech3285s1.pdf) (July 1997): MPEG Audio
- [EBU Rec 68](https://tech.ebu.ch/docs/r/r068.pdf): Signal modulation and format constraints
### Implementation of 64-bit Wave Files
- [ITU-R 2088][itu2088] (October 2019), "Long-form file format for the international exchange of audio programme materials with metadata"
- Presently in force, adopted by the EBU in [EBU Tech 3306v2][ebu3306v2] (June 2018).
- [EBU Tech 3306v1][ebu3306v1] (July 2009), "MBWF / RF64: An extended File Format for Audio"
- No longer in force, however long-established.
### Implementation of Wave format `fmt` chunk
- [MSDN WAVEFORMATEX](https://docs.microsoft.com/en-us/windows/win32/api/mmeapi/ns-mmeapi-waveformatex)
- [MSDN WAVEFORMATEXTENSIBLE](https://docs.microsoft.com/en-us/windows/win32/api/mmreg/ns-mmreg-waveformatextensible)
### Other resources
- [RFC 3261][rfc3261] (June 1998) "WAVE and AVI Codec Registries"
- [Peter Kabal, McGill University](http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html)
- [Multimedia Programming Interface and Data Specifications 1.0](http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/Docs/riffmci.pdf)
(August 1991), IBM Corporation and Microsoft Corporation
### Formatting of Specific Metadatums
- [iXML Metadata Specification](http://www.gallery.co.uk/ixml/) (April 2019)
- EBU 3285 Supplements:
- [Supplement 2](https://tech.ebu.ch/docs/tech/tech3285s2.pdf) (July 2001): Quality chunk and cuesheet
- [Supplement 3](https://tech.ebu.ch/docs/tech/tech3285s3.pdf) (July 2001): Peak Metadata
- [Supplement 4](https://tech.ebu.ch/docs/tech/tech3285s4.pdf) (April 2003): Link Metadata
- [Supplement 5](https://tech.ebu.ch/docs/tech/tech3285s5.pdf) (May 2018): ADM Metadata
- [Supplement 6](https://tech.ebu.ch/docs/tech/tech3285s6.pdf) (October 2009): Dolby Metadata
- [EBU Tech R099](https://tech.ebu.ch/docs/r/r099.pdf) (October 2011) "Unique Source Identifier (USID) for use in the
<OriginatorReference> field of the Broadcast Wave Format"
- [EBU Tech R098](https://tech.ebu.ch/docs/r/r098.pdf) (1999) "Format for the <CodingHistory> field in Broadcast Wave Format files, BWF"
[ebu3285]: https://tech.ebu.ch/docs/tech/tech3285.pdf
[ebu3306v1]: https://tech.ebu.ch/docs/tech/tech3306v1_1.pdf
[ebu3306v2]: https://tech.ebu.ch/docs/tech/tech3306.pdf
[itu2088]: https://www.itu.int/dms_pubrec/itu-r/rec/bs/R-REC-BS.2088-1-201910-I!!PDF-E.pdf
[rfc3261]: https://tools.ietf.org/html/rfc2361
[github]: https://github.com/iluvcapra/bwavfile [github]: https://github.com/iluvcapra/bwavfile
*/ */
// #![feature(external_doc)]
// #[doc(include="../README.md")]
// #[cfg(doctest)]
// pub struct ReadmeDoctests;
extern crate encoding;
extern crate byteorder; extern crate byteorder;
extern crate encoding;
extern crate uuid; extern crate uuid;
mod fourcc;
mod errors;
mod common_format; mod common_format;
mod errors;
mod fourcc;
mod list_form;
mod parser; mod parser;
mod raw_chunk_reader;
mod audio_frame_reader;
mod chunks;
mod bext; mod bext;
mod chunks;
mod cue;
mod fmt; mod fmt;
mod sample;
mod wavereader; mod wavereader;
mod wavewriter; mod wavewriter;
pub use errors::Error;
pub use wavereader::{WaveReader};
pub use bext::Bext; pub use bext::Bext;
pub use fmt::{WaveFmt, WaveFmtExtended, ChannelDescriptor}; pub use common_format::{
pub use common_format::CommonFormat; CommonFormat, WAVE_TAG_EXTENDED, WAVE_TAG_FLOAT, WAVE_TAG_MPEG, WAVE_TAG_PCM,
pub use audio_frame_reader::AudioFrameReader; WAVE_UUID_BFORMAT_FLOAT, WAVE_UUID_BFORMAT_PCM, WAVE_UUID_FLOAT, WAVE_UUID_MPEG, WAVE_UUID_PCM,
};
pub use cue::Cue;
pub use errors::Error;
pub use fmt::{
ADMAudioID, ChannelDescriptor, ChannelMask, ReadWavAudioData, WaveFmt, WaveFmtExtended,
};
pub use sample::{Sample, I24};
pub use wavereader::{AudioFrameReader, WaveReader};
pub use wavewriter::{AudioFrameWriter, WaveWriter};

42
src/list_form.rs Normal file
View File

@@ -0,0 +1,42 @@
use super::fourcc::{FourCC, ReadFourCC};
use byteorder::{LittleEndian, ReadBytesExt};
use std::io::{Cursor, Error, Read};
pub struct ListFormItem {
pub signature: FourCC,
pub contents: Vec<u8>,
}
/// A helper that will accept a LIST chunk as a [u8]
/// and give you back each segment
///
pub fn collect_list_form(list_contents: &[u8]) -> Result<Vec<ListFormItem>, Error> {
let mut cursor = Cursor::new(list_contents);
let mut remain = list_contents.len();
let _ = cursor.read_fourcc()?; // skip signature
remain -= 4;
let mut retval: Vec<ListFormItem> = vec![];
while remain > 0 {
let this_sig = cursor.read_fourcc()?;
let this_size = cursor.read_u32::<LittleEndian>()? as usize;
remain -= 8;
let mut content_buf = vec![0u8; this_size];
cursor.read_exact(&mut content_buf)?;
remain -= this_size;
retval.push(ListFormItem {
signature: this_sig,
contents: content_buf,
});
if this_size % 2 == 1 {
cursor.read_u8()?;
//panic!("Got this far!");
remain -= 1;
}
}
Ok(retval)
}

View File

@@ -1,15 +1,14 @@
use std::collections::HashMap;
use std::io; use std::io;
use std::io::SeekFrom::{Current, Start}; use std::io::SeekFrom::{Current, Start};
use std::io::{Seek, Read}; use std::io::{Read, Seek};
use std::collections::HashMap;
use byteorder::LittleEndian; use byteorder::LittleEndian;
use byteorder::ReadBytesExt; use byteorder::ReadBytesExt;
use super::errors::Error; use super::errors::Error;
use super::fourcc::{FourCC, ReadFourCC}; use super::fourcc::{FourCC, ReadFourCC};
use super::fourcc::{RIFF_SIG, RF64_SIG, BW64_SIG, WAVE_SIG, DS64_SIG, DATA_SIG}; use super::fourcc::{BW64_SIG, DATA_SIG, DS64_SIG, RF64_SIG, RIFF_SIG, WAVE_SIG};
// just for your reference... // just for your reference...
// RF64 documentation https://www.itu.int/dms_pubrec/itu-r/rec/bs/R-REC-BS.2088-1-201910-I!!PDF-E.pdf // RF64 documentation https://www.itu.int/dms_pubrec/itu-r/rec/bs/R-REC-BS.2088-1-201910-I!!PDF-E.pdf
@@ -21,12 +20,26 @@ const RF64_SIZE_MARKER: u32 = 0xFF_FF_FF_FF;
#[derive(Debug)] #[derive(Debug)]
pub enum Event { pub enum Event {
StartParse, StartParse,
ReadHeader { signature: FourCC, length_field: u32 }, ReadHeader {
ReadRF64Header { signature: FourCC }, signature: FourCC,
ReadDS64 {file_size: u64, long_sizes: HashMap<FourCC,u64> }, length_field: u32,
BeginChunk { signature: FourCC, content_start: u64, content_length: u64 }, },
Failed { error: Error }, ReadRF64Header {
FinishParse signature: FourCC,
},
ReadDS64 {
file_size: u64,
long_sizes: HashMap<FourCC, u64>,
},
BeginChunk {
signature: FourCC,
content_start: u64,
content_length: u64,
},
Failed {
error: Error,
},
FinishParse,
} }
#[derive(Debug)] #[derive(Debug)]
@@ -36,29 +49,29 @@ enum State {
ReadyForDS64, ReadyForDS64,
ReadyForChunk { at: u64, remaining: u64 }, ReadyForChunk { at: u64, remaining: u64 },
Error, Error,
Complete Complete,
} }
pub struct Parser<R: Read + Seek> { pub struct Parser<R: Read + Seek> {
stream: R, stream: R,
state: State, state: State,
ds64state: HashMap<FourCC,u64> ds64state: HashMap<FourCC, u64>,
} }
#[derive(Debug, PartialEq, Eq)]
pub struct ChunkIteratorItem { pub struct ChunkIteratorItem {
pub signature: FourCC, pub signature: FourCC,
pub start: u64, pub start: u64,
pub length: u64 pub length: u64,
} }
impl<R: Read + Seek> Parser<R> { impl<R: Read + Seek> Parser<R> {
// wraps a stream // wraps a stream
pub fn make(stream: R) -> Result<Self, Error> { pub fn make(stream: R) -> Result<Self, Error> {
let newmap: HashMap<FourCC, u64> = HashMap::new(); let newmap: HashMap<FourCC, u64> = HashMap::new();
let mut the_stream = stream; let mut the_stream = stream;
the_stream.seek(Start(0))?; the_stream.seek(Start(0))?;
return Ok(Parser { Ok(Parser {
stream: the_stream, stream: the_stream,
state: State::New, state: State::New,
ds64state: newmap, ds64state: newmap,
@@ -70,24 +83,39 @@ impl<R: Read + Seek> Parser<R> {
// } // }
pub fn into_chunk_iterator(self) -> impl Iterator<Item = Result<ChunkIteratorItem, Error>> { pub fn into_chunk_iterator(self) -> impl Iterator<Item = Result<ChunkIteratorItem, Error>> {
self.filter_map({|event| self.filter_map({
if let Event::BeginChunk {signature , content_start, content_length } = event { |event| {
Some(Ok(ChunkIteratorItem {signature, start: content_start, length: content_length })) if let Event::BeginChunk {
signature,
content_start,
content_length,
} = event
{
Some(Ok(ChunkIteratorItem {
signature,
start: content_start,
length: content_length,
}))
} else if let Event::Failed { error } = event { } else if let Event::Failed { error } = event {
Some(Err(error)) Some(Err(error))
} else { } else {
None None
} }
}
}) })
} }
pub fn into_chunk_list(self) -> Result<Vec<ChunkIteratorItem>, Error> { pub fn into_chunk_list(self) -> Result<Vec<ChunkIteratorItem>, Error> {
let mut error = Ok(()); let mut error = Ok(());
let chunks = self.into_chunk_iterator() let chunks = self
.into_chunk_iterator()
.scan(&mut error, |err, res| match res { .scan(&mut error, |err, res| match res {
Ok(ok) => Some(ok), Ok(ok) => Some(ok),
Err(e) => { **err = Err(e); None } Err(e) => {
**err = Err(e);
None
}
}) })
.collect(); .collect();
@@ -95,7 +123,6 @@ impl<R: Read + Seek> Parser<R> {
Ok(chunks) Ok(chunks)
} }
} }
impl<R: Read + Seek> Iterator for Parser<R> { impl<R: Read + Seek> Iterator for Parser<R> {
@@ -104,12 +131,11 @@ impl<R: Read + Seek> Iterator for Parser<R> {
fn next(&mut self) -> Option<Event> { fn next(&mut self) -> Option<Event> {
let (event, next_state) = self.advance(); let (event, next_state) = self.advance();
self.state = next_state; self.state = next_state;
return event; event
} }
} }
impl<R: Read + Seek> Parser<R> { impl<R: Read + Seek> Parser<R> {
fn parse_header(&mut self) -> Result<(Event, State), io::Error> { fn parse_header(&mut self) -> Result<(Event, State), io::Error> {
let file_sig = self.stream.read_fourcc()?; let file_sig = self.stream.read_fourcc()?;
let length = self.stream.read_u32::<LittleEndian>()?; let length = self.stream.read_u32::<LittleEndian>()?;
@@ -122,30 +148,30 @@ impl<R: Read + Seek> Parser<R> {
(RIFF_SIG, size, WAVE_SIG) => { (RIFF_SIG, size, WAVE_SIG) => {
event = Event::ReadHeader { event = Event::ReadHeader {
signature: file_sig, signature: file_sig,
length_field: size length_field: size,
}; };
next_state = State::ReadyForChunk { next_state = State::ReadyForChunk {
at: 12, at: 12,
remaining: (length - 4) as u64, remaining: (length - 4) as u64,
}; };
}, }
(RF64_SIG, RF64_SIZE_MARKER, WAVE_SIG) | (BW64_SIG, RF64_SIZE_MARKER, WAVE_SIG) => { (RF64_SIG, RF64_SIZE_MARKER, WAVE_SIG) | (BW64_SIG, RF64_SIZE_MARKER, WAVE_SIG) => {
event = Event::ReadRF64Header { event = Event::ReadRF64Header {
signature: file_sig signature: file_sig,
}; };
next_state = State::ReadyForDS64; next_state = State::ReadyForDS64;
}, }
_ => { _ => {
event = Event::Failed { event = Event::Failed {
error: Error::HeaderNotRecognized error: Error::HeaderNotRecognized,
}; };
next_state = State::Error; next_state = State::Error;
} }
} }
return Ok( (event, next_state) ); Ok((event, next_state))
} }
fn parse_ds64(&mut self) -> Result<(Event, State), Error> { fn parse_ds64(&mut self) -> Result<(Event, State), Error> {
@@ -156,8 +182,7 @@ impl<R: Read + Seek> Parser<R> {
let mut read: u64 = 0; let mut read: u64 = 0;
if ds64_sig != DS64_SIG { if ds64_sig != DS64_SIG {
return Err(Error::MissingRequiredDS64); Err(Error::MissingRequiredDS64)
} else { } else {
let long_file_size = self.stream.read_u64::<LittleEndian>()?; let long_file_size = self.stream.read_u64::<LittleEndian>()?;
let long_data_size = self.stream.read_u64::<LittleEndian>()?; let long_data_size = self.stream.read_u64::<LittleEndian>()?;
@@ -196,19 +221,17 @@ impl<R: Read + Seek> Parser<R> {
remaining: long_file_size - (4 + 8 + ds64_size), remaining: long_file_size - (4 + 8 + ds64_size),
}; };
return Ok( (event, state) ); Ok((event, state))
} }
} }
fn enter_chunk(&mut self, at: u64, remaining: u64) -> Result<(Event, State), io::Error> { fn enter_chunk(&mut self, at: u64, remaining: u64) -> Result<(Event, State), io::Error> {
let event; let event;
let state; let state;
if remaining == 0 { if remaining == 0 {
event = Event::FinishParse; event = Event::FinishParse;
state = State::Complete; state = State::Complete;
} else { } else {
let this_fourcc = self.stream.read_fourcc()?; let this_fourcc = self.stream.read_fourcc()?;
let this_size: u64; let this_size: u64;
@@ -220,59 +243,52 @@ impl<R: Read + Seek> Parser<R> {
this_size = self.stream.read_u32::<LittleEndian>()? as u64; this_size = self.stream.read_u32::<LittleEndian>()? as u64;
} }
let this_displacement :u64 = if this_size % 2 == 1 { this_size + 1 } else { this_size }; let this_displacement: u64 = if this_size % 2 == 1 {
this_size + 1
} else {
this_size
};
self.stream.seek(Current(this_displacement as i64))?; self.stream.seek(Current(this_displacement as i64))?;
event = Event::BeginChunk { event = Event::BeginChunk {
signature: this_fourcc, signature: this_fourcc,
content_start: at + 8, content_start: at + 8,
content_length: this_size content_length: this_size,
}; };
state = State::ReadyForChunk { state = State::ReadyForChunk {
at: at + 8 + this_displacement, at: at + 8 + this_displacement,
remaining: remaining - 8 - this_displacement remaining: remaining - 8 - this_displacement,
} }
} }
return Ok( (event, state) ); Ok((event, state))
} }
fn handle_state(&mut self) -> Result<(Option<Event>, State), Error> { fn handle_state(&mut self) -> Result<(Option<Event>, State), Error> {
match self.state { match self.state {
State::New => { State::New => Ok((Some(Event::StartParse), State::ReadyForHeader)),
return Ok( ( Some(Event::StartParse) , State::ReadyForHeader) );
},
State::ReadyForHeader => { State::ReadyForHeader => {
let (event, state) = self.parse_header()?; let (event, state) = self.parse_header()?;
return Ok( ( Some(event), state ) ); Ok((Some(event), state))
}, }
State::ReadyForDS64 => { State::ReadyForDS64 => {
let (event, state) = self.parse_ds64()?; let (event, state) = self.parse_ds64()?;
return Ok( ( Some(event), state ) ); Ok((Some(event), state))
}, }
State::ReadyForChunk { at, remaining } => { State::ReadyForChunk { at, remaining } => {
let (event, state) = self.enter_chunk(at, remaining)?; let (event, state) = self.enter_chunk(at, remaining)?;
return Ok( ( Some(event), state ) ); Ok((Some(event), state))
},
State::Error => {
return Ok( ( Some(Event::FinishParse) , State::Complete ) );
},
State::Complete => {
return Ok( ( None, State::Complete ) );
} }
State::Error => Ok((Some(Event::FinishParse), State::Complete)),
State::Complete => Ok((None, State::Complete)),
} }
} }
fn advance(&mut self) -> (Option<Event>, State) { fn advance(&mut self) -> (Option<Event>, State) {
match self.handle_state() { match self.handle_state() {
Ok(( event , state) ) => { Ok((event, state)) => (event, state),
return (event, state); Err(error) => (Some(Event::Failed { error }), State::Error),
},
Err(error) => {
return (Some(Event::Failed { error: error.into() } ), State::Error );
} }
} }
} }
}

View File

@@ -1,73 +0,0 @@
use std::cmp::min;
use std::io::SeekFrom;
use std::io::SeekFrom::{Start, Current, End};
use std::io::{Seek,Read,Error,ErrorKind};
// I'm not sure this hasn't already been written somewhere in
// std but I'm just doing this here as an exercise.
#[derive(Debug)]
pub struct RawChunkReader<'a, R: Read + Seek> {
reader: &'a mut R,
start: u64,
length: u64,
position: u64
}
impl<'a,R: Read + Seek> RawChunkReader<'a, R> {
pub fn new(reader: &'a mut R, start: u64, length: u64) -> Self {
return Self {
reader: reader,
start: start,
length: length,
position: 0
}
}
pub fn length(&self) -> u64 {
self.length
}
}
impl<'a, R:Read + Seek> Read for RawChunkReader<'_, R> {
fn read(&mut self, buf: &mut [u8]) -> Result<usize, Error> {
if self.position >= self.length {
Err(Error::new(ErrorKind::UnexpectedEof, "RawChunkReader encountered end-of-file"))
} else {
self.reader.seek(Start(self.start + self.position))?;
let to_read = min(self.length - self.position, buf.len() as u64);
self.reader.take(to_read).read(buf)?;
self.position += to_read;
Ok(to_read as usize)
}
}
}
impl<'a, R:Read + Seek> Seek for RawChunkReader<'_, R> {
fn seek(&mut self, seek: SeekFrom) -> Result<u64, std::io::Error> {
match seek {
Start(s) => {
self.position = s;
Ok(self.position)
},
Current(s) => {
let new_position = s + self.position as i64;
if new_position < 0 {
Err( Error::new(ErrorKind::Other, "Attempted seek before beginning of chunk") )
} else {
self.position = new_position as u64;
Ok(self.position)
}
},
End(s) => {
let new_position = s + self.length as i64;
if new_position < 0 {
Err( Error::new(ErrorKind::Other, "Attempted seek before beginning of chunk") )
} else {
self.position = new_position as u64;
Ok(self.position)
}
}
}
}
}

14
src/sample.rs Normal file
View File

@@ -0,0 +1,14 @@
pub use dasp_sample::I24;
use dasp_sample::Duplex;
pub trait Sample:
dasp_sample::Sample + Duplex<u8> + Duplex<i16> + Duplex<I24> + Duplex<i32> + Duplex<f32>
{
}
impl Sample for u8 {}
impl Sample for i16 {}
impl Sample for I24 {}
impl Sample for i32 {}
impl Sample for f32 {}

2
src/sampler.rs Normal file
View File

@@ -0,0 +1,2 @@
/// ## Resources
/// - [Sampler Metadata](http://www.piclist.com/techref/io/serial/midi/wave.html)

View File

@@ -1,135 +1,416 @@
use std::fs::File; use std::fs::File;
use super::parser::Parser; use std::path::Path;
use super::fourcc::{FourCC, FMT__SIG,DATA_SIG, BEXT_SIG, JUNK_SIG, FLLR_SIG};
use super::errors::Error as ParserError; use std::io::Cursor;
use super::raw_chunk_reader::RawChunkReader; use std::io::SeekFrom;
use super::fmt::WaveFmt; use std::io::SeekFrom::Start;
use std::io::{BufReader, Read, Seek};
use super::bext::Bext; use super::bext::Bext;
use super::audio_frame_reader::AudioFrameReader;
use super::chunks::ReadBWaveChunks; use super::chunks::ReadBWaveChunks;
use super::cue::Cue;
use super::errors::Error as ParserError;
use super::errors::Error;
use super::fmt::{ChannelDescriptor, ChannelMask, WaveFmt};
use super::fourcc::{
FourCC, ReadFourCC, ADTL_SIG, AXML_SIG, BEXT_SIG, CUE__SIG, DATA_SIG, FLLR_SIG, FMT__SIG,
IXML_SIG, JUNK_SIG, LIST_SIG,
};
use super::parser::Parser;
use super::{CommonFormat, Sample, I24};
use byteorder::LittleEndian;
use byteorder::ReadBytesExt;
use std::io::{Read, Seek}; use dasp_sample::Sample as _; // Expose to_sample()
/// Read audio frames
///
/// The inner reader is interpreted as a raw audio data
/// bitstream having a format specified by `format`.
///
#[derive(Debug)]
pub struct AudioFrameReader<R: Read + Seek> {
inner: R,
format: WaveFmt,
start: u64,
length: u64,
}
impl<R: Read + Seek> AudioFrameReader<R> {
/// Create a new `AudioFrameReader`
///
/// ### Panics
///
/// This method does a few sanity checks on the provided format
/// parameter to confirm the `block_alignment` law is fulfilled
/// and the format tag is readable by this implementation (only
/// format 0x01 is supported at this time.)
pub fn new(mut inner: R, format: WaveFmt, start: u64, length: u64) -> Result<Self, Error> {
assert!(
format.block_alignment * 8 == format.bits_per_sample * format.channel_count,
"Unable to read audio frames from packed formats: block alignment is {}, should be {}",
format.block_alignment,
(format.bits_per_sample / 8) * format.channel_count
);
assert!(
format.common_format() == CommonFormat::IntegerPCM
|| format.common_format() == CommonFormat::IeeeFloatPCM,
"Unsupported format tag {:?}",
format.tag
);
inner.seek(Start(start))?;
Ok(AudioFrameReader {
inner,
format,
start,
length,
})
}
/// Unwrap the inner reader.
pub fn into_inner(self) -> R {
self.inner
}
/// Locate the read position to a different frame
///
/// Seeks within the audio stream.
///
/// Returns the new location of the read position.
///
/// locate() behaves similarly to Read methods in that
/// seeking after the end of the audio data is not an error.
pub fn locate(&mut self, to: u64) -> Result<u64, Error> {
let position = to * self.format.block_alignment as u64;
let seek_result = self.inner.seek(Start(self.start + position))?;
Ok((seek_result - self.start) / self.format.block_alignment as u64)
}
/// Reads frames from the file into the provided buffer
///
/// The function will attempt to fill the buffer, but will stop without error when the end of
/// the file is reached.
///
/// The reader will convert from the file's sample type into the buffer's sample type.
/// Note that no dithering will be applied during sample type conversion,
/// if dithering is required then it will need to be applied manually.
///
/// The return value is the number of frames read into the buffer.
pub fn read_frames<S>(&mut self, buffer: &mut [S]) -> Result<u64, Error>
where
S: Sample,
{
use CommonFormat::*;
let channel_count = self.format.channel_count as usize;
let common_format = self.format.common_format();
let bits_per_sample = self.format.bits_per_sample;
if buffer.len() % channel_count != 0 {
return Err(Error::InvalidBufferSize {
buffer_size: buffer.len(),
channel_count: self.format.channel_count,
});
}
let position = self.inner.stream_position()? - self.start;
let frames_requested = (buffer.len() / channel_count) as u64;
let bytes_per_frame = self.format.block_alignment as u64;
let frames_remaining = (self.length - position) / bytes_per_frame;
let frames_to_read = frames_requested.min(frames_remaining);
let samples_to_read = frames_to_read as usize * channel_count;
match (common_format, bits_per_sample) {
(IntegerPCM, 8) => read_into_buffer(samples_to_read, buffer, || {
Ok(self.inner.read_u8()?.to_sample())
}),
(IntegerPCM, 16) => read_into_buffer(samples_to_read, buffer, || {
Ok(self.inner.read_i16::<LittleEndian>()?.to_sample())
}),
(IntegerPCM, 24) => read_into_buffer(samples_to_read, buffer, || {
Ok(I24::from(self.inner.read_i24::<LittleEndian>()?).to_sample())
}),
(IntegerPCM, 32) => read_into_buffer(samples_to_read, buffer, || {
Ok(self.inner.read_i32::<LittleEndian>()?.to_sample())
}),
(IeeeFloatPCM, 32) => read_into_buffer(samples_to_read, buffer, || {
Ok(self.inner.read_f32::<LittleEndian>()?.to_sample())
}),
(_, _) => panic!(
"Unsupported format, bits per sample {}, channels {}, sample format: {:?}",
bits_per_sample, channel_count, common_format
),
}?;
Ok(frames_to_read)
}
}
fn read_into_buffer<S, F>(
sample_count: usize,
buffer: &mut [S],
mut read_fn: F,
) -> Result<(), Error>
where
F: FnMut() -> Result<S, Error>,
{
for output in buffer.iter_mut().take(sample_count) {
*output = read_fn()?;
}
Ok(())
}
/// Wave, Broadcast-WAV and RF64/BW64 parser/reader.
///
/// ```
/// use bwavfile::WaveReader;
/// let mut r = WaveReader::open("tests/media/ff_silence.wav").unwrap();
///
/// let format = r.format().unwrap();
/// assert_eq!(format.sample_rate, 44100);
/// assert_eq!(format.channel_count, 1);
///
/// let mut frame_reader = r.audio_frame_reader().unwrap();
/// let mut buffer = format.create_frame_buffer::<i32>(1);
///
/// let read = frame_reader.read_frames(&mut buffer).unwrap();
///
/// assert_eq!(buffer, [0i32]);
/// assert_eq!(read, 1);
///
/// ```
///
/// ## Resources
///
/// ### Implementation of Wave Files
/// - [Peter Kabal, McGill University](http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html)
/// - [Multimedia Programming Interface and Data Specifications 1.0](http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/Docs/riffmci.pdf)
/// (August 1991), IBM Corporation and Microsoft Corporation
///
/// ### Implementation of Broadcast Wave Files
/// - [EBU Tech 3285][ebu3285] (May 2011), "Specification of the Broadcast Wave Format (BWF)"
/// - [Supplement 1](https://tech.ebu.ch/docs/tech/tech3285s1.pdf) (July 1997): MPEG Audio
/// - [EBU Rec 68](https://tech.ebu.ch/docs/r/r068.pdf): Signal modulation and format constraints
///
/// ### Implementation of 64-bit Wave Files
/// - [ITU-R 2088][itu2088] (October 2019), "Long-form file format for the international exchange of audio programme materials with metadata"
/// - Presently in force, adopted by the EBU in [EBU Tech 3306v2][ebu3306v2] (June 2018).
/// - [EBU Tech 3306v1][ebu3306v1] (July 2009), "MBWF / RF64: An extended File Format for Audio"
/// - No longer in force, however long-established.
///
///
/// [ebu3285]: https://tech.ebu.ch/docs/tech/tech3285.pdf
/// [ebu3306v1]: https://tech.ebu.ch/docs/tech/tech3306v1_1.pdf
/// [ebu3306v2]: https://tech.ebu.ch/docs/tech/tech3306.pdf
/// [itu2088]: https://www.itu.int/dms_pubrec/itu-r/rec/bs/R-REC-BS.2088-1-201910-I!!PDF-E.pdf
/// [rfc3261]: https://tools.ietf.org/html/rfc2361
/**
* Wave, Broadcast-WAV and RF64/BW64 parser/reader.
*
* ```
* use bwavfile::WaveReader;
* let mut r = WaveReader::open("tests/media/ff_silence.wav").unwrap();
*
* let format = r.format().unwrap();
* assert_eq!(format.sample_rate, 44100);
* assert_eq!(format.channel_count, 1);
*
* let mut frame_reader = r.audio_frame_reader().unwrap();
* let mut buffer = frame_reader.create_frame_buffer();
*
* let read = frame_reader.read_integer_frame(&mut buffer).unwrap();
*
* assert_eq!(buffer, [0i32]);
* assert_eq!(read, 1);
*
* ```
*/
#[derive(Debug)] #[derive(Debug)]
pub struct WaveReader<R: Read + Seek> { pub struct WaveReader<R: Read + Seek> {
pub inner: R, pub inner: R,
} }
impl WaveReader<BufReader<File>> {
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, ParserError> {
let f = File::open(path)?;
let inner = BufReader::new(f);
Self::new(inner)
}
}
impl WaveReader<File> { impl WaveReader<File> {
/** /// Open a file for reading with unbuffered IO.
* Open a file for reading. ///
* /// A convenience that opens `path` and calls `Self::new()`
* A convenience that opens `path` and calls `Self::new()` pub fn open_unbuffered<P: AsRef<Path>>(path: P) -> Result<Self, ParserError> {
*
*/
pub fn open(path: &str) -> Result<Self, ParserError> {
let inner = File::open(path)?; let inner = File::open(path)?;
return Ok( Self::new(inner)? ) Self::new(inner)
} }
} }
impl<R: Read + Seek> WaveReader<R> { impl<R: Read + Seek> WaveReader<R> {
/** /// Wrap a `Read` struct in a new `WaveReader`.
* Wrap a `Read` struct in a new `WaveReader`. ///
* /// This is the primary entry point into the `WaveReader` interface. The
* This is the primary entry point into the `WaveReader` interface. The /// stream passed as `inner` must be at the beginning of the header of the
* stream passed as `inner` must be at the beginning of the header of the /// WAVE data. For a .wav file, this means it must be at the start of the
* WAVE data. For a .wav file, this means it must be at the start of the /// file.
* file. ///
* /// This function does a minimal validation on the provided stream and
* This function does a minimal validation on the provided stream and /// will return an `Err(errors::Error)` immediately if there is a structural
* will return an `Err(errors::Error)` immediately if there is a structural /// inconsistency that makes the stream unreadable or if it's missing
* inconsistency that makes the stream unreadable or if it's missing /// essential components that make interpreting the audio data impossible.
* essential components that make interpreting the audio data impoossible.
* /// ```rust
* ```rust /// use std::fs::File;
* use std::fs::File; /// use std::io::{Error,ErrorKind};
* use std::io::{Error,ErrorKind}; /// use bwavfile::{WaveReader, Error as WavError};
* use bwavfile::{WaveReader, Error as WavError}; ///
* /// let f = File::open("tests/media/error.wav").unwrap();
* let f = File::open("tests/media/error.wav").unwrap(); ///
* /// let reader = WaveReader::new(f);
* let reader = WaveReader::new(f); ///
* /// match reader {
* match reader { /// Ok(_) => panic!("error.wav should not be openable"),
* Ok(_) => panic!("error.wav should not be openable"), /// Err( WavError::IOError( e ) ) => {
* Err( WavError::IOError( e ) ) => { /// assert_eq!(e.kind(), ErrorKind::UnexpectedEof)
* assert_eq!(e.kind(), ErrorKind::UnexpectedEof) /// }
* } /// Err(e) => panic!("Unexpected error was returned {:?}", e)
* Err(e) => panic!("Unexpected error was returned {:?}", e) /// }
* } ///
* /// ```
* ```
*
*/
pub fn new(inner: R) -> Result<Self, ParserError> { pub fn new(inner: R) -> Result<Self, ParserError> {
let mut retval = Self { inner }; let mut retval = Self { inner };
retval.validate_readable()?; retval.validate_readable()?;
Ok(retval) Ok(retval)
} }
/** /// Unwrap the inner reader.
* Unwrap the inner reader.
*/
pub fn into_inner(self) -> R { pub fn into_inner(self) -> R {
return self.inner; self.inner
} }
/** ///
* Create an `AudioFrameReader` for reading each audio frame. /// Create an `AudioFrameReader` for reading each audio frame and consume the `WaveReader`.
*/ ///
pub fn audio_frame_reader(&mut self) -> Result<AudioFrameReader<RawChunkReader<R>>, ParserError> { pub fn audio_frame_reader(mut self) -> Result<AudioFrameReader<R>, ParserError> {
let format = self.format()?; let format = self.format()?;
let audio_chunk_reader = self.chunk_reader(DATA_SIG, 0)?; let audio_chunk_reader = self.get_chunk_extent_at_index(DATA_SIG, 0)?;
Ok(AudioFrameReader::new(audio_chunk_reader, format)) AudioFrameReader::new(
self.inner,
format,
audio_chunk_reader.0,
audio_chunk_reader.1,
)
} }
/** /// The count of audio frames in the file.
* The count of audio frames in the file.
*/
pub fn frame_length(&mut self) -> Result<u64, ParserError> { pub fn frame_length(&mut self) -> Result<u64, ParserError> {
let (_, data_length) = self.get_chunk_extent_at_index(DATA_SIG, 0)?; let (_, data_length) = self.get_chunk_extent_at_index(DATA_SIG, 0)?;
let format = self.format()?; let format = self.format()?;
Ok(data_length / (format.block_alignment as u64)) Ok(data_length / (format.block_alignment as u64))
} }
/** /// Sample and frame format of this wave file.
* Sample and frame format of this wave file. ///
*/
pub fn format(&mut self) -> Result<WaveFmt, ParserError> { pub fn format(&mut self) -> Result<WaveFmt, ParserError> {
self.chunk_reader(FMT__SIG, 0)?.read_wave_fmt() let (start, _) = self.get_chunk_extent_at_index(FMT__SIG, 0)?;
self.inner.seek(SeekFrom::Start(start))?;
self.inner.read_wave_fmt()
} }
/** /// The Broadcast-WAV metadata record for this file, if present.
* The Broadcast-WAV metadata record for this file. ///
*/ pub fn broadcast_extension(&mut self) -> Result<Option<Bext>, ParserError> {
pub fn broadcast_extension(&mut self) -> Result<Bext, ParserError> { let mut bext_buff: Vec<u8> = vec![];
self.chunk_reader(BEXT_SIG, 0)?.read_bext() let result = self.read_chunk(BEXT_SIG, 0, &mut bext_buff)?;
if result > 0 {
let mut bext_cursor = Cursor::new(bext_buff);
Ok(Some(bext_cursor.read_bext()?))
} else {
Ok(None)
}
}
/// Describe the channels in this file
///
/// Returns a vector of channel descriptors, one for each channel
///
/// ```rust
/// use bwavfile::WaveReader;
/// use bwavfile::ChannelMask;
///
/// let mut f = WaveReader::open("tests/media/pt_24bit_51.wav").unwrap();
///
/// let chans = f.channels().unwrap();
/// assert_eq!(chans[0].index, 0);
/// assert_eq!(chans[0].speaker, ChannelMask::FrontLeft);
/// assert_eq!(chans[3].index, 3);
/// assert_eq!(chans[3].speaker, ChannelMask::LowFrequency);
/// assert_eq!(chans[4].speaker, ChannelMask::BackLeft);
/// ```
pub fn channels(&mut self) -> Result<Vec<ChannelDescriptor>, ParserError> {
let format = self.format()?;
let channel_masks: Vec<ChannelMask> = match (format.channel_count, format.extended_format) {
(1, _) => vec![ChannelMask::FrontCenter],
(2, _) => vec![ChannelMask::FrontLeft, ChannelMask::FrontRight],
(n, Some(x)) => ChannelMask::channels(x.channel_mask, n),
(n, _) => vec![ChannelMask::DirectOut; n as usize],
};
Ok((0..format.channel_count)
.zip(channel_masks)
.map(|(i, m)| ChannelDescriptor {
index: i,
speaker: m,
adm_track_audio_ids: vec![],
})
.collect())
}
/// Read cue points.
///
/// ```rust
/// use bwavfile::WaveReader;
/// use bwavfile::Cue;
///
/// let mut f = WaveReader::open("tests/media/izotope_test.wav").unwrap();
/// let cue_points = f.cue_points().unwrap();
///
/// assert_eq!(cue_points.len(), 3);
/// assert_eq!(cue_points[0].frame, 12532);
/// assert_eq!(cue_points[0].length, None);
/// assert_eq!(cue_points[0].label, Some(String::from("Marker 1")));
/// assert_eq!(cue_points[0].note, Some(String::from("Marker 1 Comment")));
///
/// assert_eq!(cue_points[1].frame, 20997);
/// assert_eq!(cue_points[1].length, None);
/// assert_eq!(cue_points[1].label, Some(String::from("Marker 2")));
/// assert_eq!(cue_points[1].note, Some(String::from("Marker 2 Comment")));
///
/// assert_eq!(cue_points[2].frame, 26711);
/// assert_eq!(cue_points[2].length, Some(6465));
/// assert_eq!(cue_points[2].label, Some(String::from("Timed Region")));
/// assert_eq!(cue_points[2].note, Some(String::from("Region Comment")));
///
/// ```
pub fn cue_points(&mut self) -> Result<Vec<Cue>, ParserError> {
let mut cue_buffer: Vec<u8> = vec![];
let mut adtl_buffer: Vec<u8> = vec![];
let cue_read = self.read_chunk(CUE__SIG, 0, &mut cue_buffer)?;
let adtl_read = self.read_list(ADTL_SIG, &mut adtl_buffer)?;
match (cue_read, adtl_read) {
(0, _) => Ok(vec![]),
(_, 0) => Ok(Cue::collect_from(&cue_buffer, None)?),
(_, _) => Ok(Cue::collect_from(&cue_buffer, Some(&adtl_buffer))?),
}
}
/// Read iXML data.
///
/// The iXML data will be appended to `buffer`.
/// If there are no iXML metadata present in the file,
/// Ok(0) will be returned.
pub fn read_ixml(&mut self, buffer: &mut Vec<u8>) -> Result<usize, ParserError> {
self.read_chunk(IXML_SIG, 0, buffer)
}
/// Read AXML data.
///
/// The axml data will be appended to `buffer`. By convention this will
/// generally be ADM metadata.
///
/// If there are no axml metadata present in the file,
/// Ok(0) will be returned
pub fn read_axml(&mut self, buffer: &mut Vec<u8>) -> Result<usize, ParserError> {
self.read_chunk(AXML_SIG, 0, buffer)
} }
/** /**
@@ -151,79 +432,80 @@ impl<R: Read + Seek> WaveReader<R> {
} }
} }
/** /// Validate minimal WAVE file.
* Validate minimal WAVE file. ///
* /// `Ok(())` if the source is `validate_readable()` AND
* `Ok(())` if the source is `validate_readable()` AND ///
* /// - Contains _only_ a `fmt` chunk and `data` chunk, with no other chunks present
* - Contains _only_ a `fmt` chunk and `data` chunk, with no other chunks present /// - `fmt` chunk is exactly 16 bytes long and begins _exactly_ at file offset 12
* - is not an RF64/BW64 /// - `data` content begins _exactly_ at file offset 36
* /// - is not an RF64/BW64
* Some clients require a WAVE file to only contain format and data without any other ///
* metadata and this function is provided to validate this condition. /// Some clients require a WAVE file to only contain format and data without any other
* /// metadata and this function is provided to validate this condition.
* ### Examples ///
* /// ### Examples
* ``` ///
* # use bwavfile::WaveReader; /// ```
* /// # use bwavfile::WaveReader;
* let mut w = WaveReader::open("tests/media/ff_minimal.wav").unwrap(); ///
* w.validate_minimal().expect("Minimal wav did not validate not minimal!"); /// let mut w = WaveReader::open("tests/media/ff_minimal.wav").unwrap();
* ``` /// w.validate_minimal().expect("Minimal wav did not validate not minimal!");
* /// ```
* ``` ///
* # use bwavfile::WaveReader; /// ```
* /// # use bwavfile::WaveReader;
* let mut x = WaveReader::open("tests/media/pt_24bit_51.wav").unwrap(); ///
* x.validate_minimal().expect_err("Complex WAV validated minimal!"); /// let mut x = WaveReader::open("tests/media/pt_24bit_51.wav").unwrap();
* ``` /// x.validate_minimal().expect_err("Complex WAV validated minimal!");
*/ /// ```
pub fn validate_minimal(&mut self) -> Result<(), ParserError> { pub fn validate_minimal(&mut self) -> Result<(), ParserError> {
self.validate_readable()?; self.validate_readable()?;
let chunk_fourccs: Vec<FourCC> = Parser::make(&mut self.inner)? let chunk_fourccs: Vec<FourCC> = Parser::make(&mut self.inner)?
.into_chunk_list()?.iter().map(|c| c.signature ).collect(); .into_chunk_list()?
.iter()
.map(|c| c.signature)
.collect();
if chunk_fourccs == vec![FMT__SIG, DATA_SIG] { if chunk_fourccs == vec![FMT__SIG, DATA_SIG] {
Ok(()) Ok(()) /* FIXME: finish implementation */
} else { } else {
Err(ParserError::NotMinimalWaveFile) Err(ParserError::NotMinimalWaveFile)
} }
} }
/** /// Validate Broadcast-WAVE file format
* Validate Broadcast-WAVE file format ///
* /// Returns `Ok(())` if `validate_readable()` and file contains a
* Returns `Ok(())` if `validate_readable()` and file contains a /// Broadcast-WAV metadata record (a `bext` chunk).
* Broadcast-WAV metadata record (a `bext` chunk). ///
* /// ### Examples
* ### Examples ///
* /// ```
* ``` /// # use bwavfile::WaveReader;
* # use bwavfile::WaveReader; ///
* /// let mut w = WaveReader::open("tests/media/ff_bwav_stereo.wav").unwrap();
* let mut w = WaveReader::open("tests/media/ff_bwav_stereo.wav").unwrap(); /// w.validate_broadcast_wave().expect("BWAVE file did not validate BWAVE");
* w.validate_broadcast_wave().expect("BWAVE file did not validate BWAVE"); ///
* /// let mut x = WaveReader::open("tests/media/pt_24bit.wav").unwrap();
* let mut x = WaveReader::open("tests/media/pt_24bit.wav").unwrap(); /// x.validate_broadcast_wave().expect("BWAVE file did not validate BWAVE");
* x.validate_broadcast_wave().expect("BWAVE file did not validate BWAVE"); ///
* /// let mut y = WaveReader::open("tests/media/audacity_16bit.wav").unwrap();
* let mut y = WaveReader::open("tests/media/audacity_16bit.wav").unwrap(); /// y.validate_broadcast_wave().expect_err("Plain WAV file DID validate BWAVE");
* y.validate_broadcast_wave().expect_err("Plain WAV file DID validate BWAVE"); /// ```
* ``` ///
*/
pub fn validate_broadcast_wave(&mut self) -> Result<(), ParserError> { pub fn validate_broadcast_wave(&mut self) -> Result<(), ParserError> {
self.validate_readable()?; self.validate_readable()?;
let (_, _) = self.get_chunk_extent_at_index(BEXT_SIG, 0)?; let (_, _) = self.get_chunk_extent_at_index(BEXT_SIG, 0)?;
Ok(()) Ok(())
} }
/** ///
* Verify data is aligned to a block boundary. /// Verify data is aligned to a block boundary.
* ///
* Returns `Ok(())` if `validate_readable()` and the start of the /// Returns `Ok(())` if `validate_readable()` and the start of the
* `data` chunk's content begins at 0x4000. /// `data` chunk's content begins at 0x4000.
*/
pub fn validate_data_chunk_alignment(&mut self) -> Result<(), ParserError> { pub fn validate_data_chunk_alignment(&mut self) -> Result<(), ParserError> {
self.validate_readable()?; self.validate_readable()?;
let (start, _) = self.get_chunk_extent_at_index(DATA_SIG, 0)?; let (start, _) = self.get_chunk_extent_at_index(DATA_SIG, 0)?;
@@ -234,55 +516,127 @@ impl<R: Read + Seek> WaveReader<R> {
} }
} }
/** /// Verify audio data can be appended immediately to this file.
* Verify audio data can be appended immediately to this file. ///
* /// Returns `Ok(())` if:
* Returns `Ok(())` if: /// - `validate_readable()`
* - `validate_readable()` /// - there is a `JUNK` or `FLLR` immediately at the beginning of the chunk
* - there is a `JUNK` or `FLLR` immediately at the beginning of the chunk /// list adequately large enough to be overwritten by a `ds64` (92 bytes)
* list adequately large enough to be overwritten by a `ds64` (92 bytes) /// - `data` is the final chunk
* - `data` is the final chunk
*/
pub fn validate_prepared_for_append(&mut self) -> Result<(), ParserError> { pub fn validate_prepared_for_append(&mut self) -> Result<(), ParserError> {
self.validate_readable()?; self.validate_readable()?;
let chunks = Parser::make(&mut self.inner)?.into_chunk_list()?; let chunks = Parser::make(&mut self.inner)?.into_chunk_list()?;
let ds64_space_required = 92; let ds64_space_required = 92;
let eligible_filler_chunks = chunks.iter() let eligible_filler_chunks = chunks
.iter()
.take_while(|c| c.signature == JUNK_SIG || c.signature == FLLR_SIG); .take_while(|c| c.signature == JUNK_SIG || c.signature == FLLR_SIG);
let filler = eligible_filler_chunks let filler = eligible_filler_chunks
.enumerate() .enumerate()
.fold(0, |accum, (n, item)| if n == 0 { accum + item.length } else {accum + item.length + 8}); .fold(0, |accum, (n, item)| {
if n == 0 {
accum + item.length
} else {
accum + item.length + 8
}
});
if filler < ds64_space_required { if filler < ds64_space_required {
Err(ParserError::InsufficientDS64Reservation {expected: ds64_space_required, actual: filler}) Err(ParserError::InsufficientDS64Reservation {
expected: ds64_space_required,
actual: filler,
})
} else { } else {
let data_pos = chunks.iter().position(|c| c.signature == DATA_SIG); let data_pos = chunks.iter().position(|c| c.signature == DATA_SIG);
match data_pos { match data_pos {
Some(p) if p == chunks.len() - 1 => Ok(()), Some(p) if p == chunks.len() - 1 => Ok(()),
_ => Err(ParserError::DataChunkNotPreparedForAppend) _ => Err(ParserError::DataChunkNotPreparedForAppend),
} }
} }
} }
} }
impl<R:Read+Seek> WaveReader<R> { /* Private Implementation */ impl<R: Read + Seek> WaveReader<R> {
// Private implementation
//
// As time passes this get smore obnoxious because I haven't implemented recursive chunk
// parsing in the raw parser and I'm working around it
fn chunk_reader(&mut self, signature: FourCC, at_index: u32) -> Result<RawChunkReader<R>, ParserError> { // fn chunk_reader(&mut self, signature: FourCC, at_index: u32) -> Result<RawChunkReader<R>, ParserError> {
let (start, length) = self.get_chunk_extent_at_index(signature, at_index)?; // let (start, length) = self.get_chunk_extent_at_index(signature, at_index)?;
Ok( RawChunkReader::new(&mut self.inner, start, length) ) // Ok( RawChunkReader::new(&mut self.inner, start, length) )
// }
fn read_list(&mut self, ident: FourCC, buffer: &mut Vec<u8>) -> Result<usize, ParserError> {
if let Some(index) = self.get_list_form(ident)? {
self.read_chunk(LIST_SIG, index, buffer)
} else {
Ok(0)
}
} }
fn get_chunk_extent_at_index(&mut self, fourcc: FourCC, index: u32) -> Result<(u64,u64), ParserError> { fn read_chunk(
&mut self,
ident: FourCC,
at: u32,
buffer: &mut Vec<u8>,
) -> Result<usize, ParserError> {
match self.get_chunk_extent_at_index(ident, at) {
Ok((start, length)) => {
buffer.resize(length as usize, 0x0);
self.inner.seek(SeekFrom::Start(start))?;
self.inner.read(buffer).map_err(ParserError::IOError)
}
Err(ParserError::ChunkMissing { signature: _ }) => Ok(0),
Err(any) => Err(any),
}
}
/// Extent of every chunk with the given fourcc
fn get_chunks_extents(&mut self, fourcc: FourCC) -> Result<Vec<(u64, u64)>, ParserError> {
let p = Parser::make(&mut self.inner)?.into_chunk_list()?; let p = Parser::make(&mut self.inner)?.into_chunk_list()?;
if let Some(chunk) = p.iter().filter(|item| item.signature == fourcc).nth(index as usize) { Ok(p.iter()
Ok ((chunk.start, chunk.length)) .filter(|item| item.signature == fourcc)
.map(|item| (item.start, item.length))
.collect())
}
/// Index of first LIST for with the given FORM fourcc
fn get_list_form(&mut self, fourcc: FourCC) -> Result<Option<u32>, ParserError> {
for (n, (start, _)) in self.get_chunks_extents(LIST_SIG)?.iter().enumerate() {
self.inner.seek(SeekFrom::Start(*start))?;
let this_fourcc = self.inner.read_fourcc()?;
if this_fourcc == fourcc {
return Ok(Some(n as u32));
}
}
Ok(None)
}
fn get_chunk_extent_at_index(
&mut self,
fourcc: FourCC,
index: u32,
) -> Result<(u64, u64), ParserError> {
if let Some((start, length)) = self.get_chunks_extents(fourcc)?.get(index as usize) {
Ok((*start, *length))
} else { } else {
Err(ParserError::ChunkMissing { signature: fourcc }) Err(ParserError::ChunkMissing { signature: fourcc })
} }
} }
} }
#[test]
fn test_list_form() {
let mut f = WaveReader::open("tests/media/izotope_test.wav").unwrap();
let mut buf: Vec<u8> = vec![];
f.read_list(ADTL_SIG, &mut buf).unwrap();
assert_ne!(buf.len(), 0);
}

View File

@@ -1,104 +1,621 @@
use std::io::{Write, Seek, SeekFrom};
use std::fs::File; use std::fs::File;
use std::io::Cursor; use std::io::{BufWriter, Cursor, Seek, SeekFrom, Write};
use std::path::Path;
use super::errors::Error; use crate::CommonFormat;
use super::chunks::{WriteBWaveChunks};
use super::fmt::WaveFmt;
use super::fourcc::{
FourCC, WriteFourCC, AXML_SIG, BEXT_SIG, DATA_SIG, DS64_SIG, ELM1_SIG, FMT__SIG, IXML_SIG,
JUNK_SIG, RF64_SIG, RIFF_SIG, WAVE_SIG,
};
use super::{Error, Sample, I24};
//use super::common_format::CommonFormat;
use super::bext::Bext; use super::bext::Bext;
use super::fmt::{WaveFmt}; use super::chunks::WriteBWaveChunks;
use super::fourcc::{FourCC, RIFF_SIG, WAVE_SIG, FMT__SIG, JUNK_SIG, BEXT_SIG, DATA_SIG, WriteFourCC};
use byteorder::LittleEndian; use byteorder::LittleEndian;
use byteorder::WriteBytesExt; use byteorder::WriteBytesExt;
struct WaveWriter<W> where W: Write + Seek { /// Write audio frames to a `WaveWriter`.
inner : W ///
///
pub struct AudioFrameWriter<W>
where
W: Write + Seek,
{
inner: WaveChunkWriter<W>,
write_buffer: Vec<u8>,
} }
impl WaveWriter<File> { impl<W> AudioFrameWriter<W>
pub fn create(path : &str, format:WaveFmt, broadcast_extension: Option<Bext>) -> Result<Self,Error> { where
let inner = File::create(path)?; W: Write + Seek,
Self::make(inner, format, broadcast_extension) {
fn new(inner: WaveChunkWriter<W>) -> Self {
AudioFrameWriter {
inner,
write_buffer: Vec::new(),
} }
} }
impl<W:Write + Seek> WaveWriter<W> { /// Write interleaved samples in `buffer`
///
/// The writer will convert from the buffer's sample type into the file's sample type.
/// Note that no dithering will be applied during sample type conversion,
/// if dithering is required then it will need to be applied manually.
pub fn write_frames<S>(&mut self, buffer: &[S]) -> Result<(), Error>
where
S: Sample,
{
let format = &self.inner.inner.format;
let channel_count = format.channel_count as usize;
pub fn make(inner : W, format: WaveFmt, broadcast_extension: Option<Bext>) -> Result<Self, Error> { if buffer.len() % channel_count != 0 {
let mut retval = Self { inner }; return Err(Error::InvalidBufferSize {
retval.prepare_created(format, broadcast_extension)?; buffer_size: buffer.len(),
Ok(retval) channel_count: format.channel_count,
});
} }
fn prepare_created(&mut self, format : WaveFmt, broadcast_extension: Option<Bext>) -> Result<(),Error> { let frame_count = buffer.len() / channel_count;
self.inner.write_fourcc(RIFF_SIG)?; let write_buffer_size = format.block_alignment as usize * frame_count;
self.inner.write_u32::<LittleEndian>(4)?; self.write_buffer.resize(write_buffer_size, 0);
self.inner.write_fourcc(WAVE_SIG)?;
let mut written : u64 = 4;
let ds64_reservation = [0u8; 92]; let mut write_cursor = Cursor::new(&mut self.write_buffer);
written += self.primitive_append_chunk(JUNK_SIG, &ds64_reservation)?; let common_format = format.common_format();
let bits_per_sample = format.bits_per_sample;
let fmt_data : Vec<u8> = { match (common_format, bits_per_sample) {
let mut c = Cursor::new(vec![]); (_, 8) => {
c.write_wave_fmt(&format)?; for sample in buffer {
c.into_inner() write_cursor.write_u8(sample.to_sample())?
}; }
}
written += self.primitive_append_chunk(FMT__SIG, &fmt_data)?; (_, 16) => {
for sample in buffer {
if let Some(bext) = broadcast_extension { write_cursor.write_i16::<LittleEndian>(sample.to_sample())?
let mut b = Cursor::new(vec![]); }
b.write_bext(&bext)?; }
let data = b.into_inner(); (_, 24) => {
written += self.primitive_append_chunk(BEXT_SIG, &data)?; for sample in buffer {
write_cursor.write_i24::<LittleEndian>(sample.to_sample::<I24>().inner())?
}
}
(CommonFormat::IntegerPCM, 32) => {
for sample in buffer {
write_cursor.write_i32::<LittleEndian>(sample.to_sample())?
}
}
(CommonFormat::IeeeFloatPCM, 32) => {
for sample in buffer {
write_cursor.write_f32::<LittleEndian>(sample.to_sample())?
}
}
(_, _) => panic!(
"Unrecognized format, bits per sample {}, channels {}, sample format {:?}",
bits_per_sample, channel_count, common_format
),
} }
// show our work self.inner.write_all(&self.write_buffer)?;
let desired_data_alignment = 0x4000;
let data_fourcc_start = desired_data_alignment - 8;
let current_position_from_start = written + 8;
let data_pad_length = data_fourcc_start - current_position_from_start;
let data_padding = vec![0u8; data_pad_length as usize];
written += self.primitive_append_chunk(JUNK_SIG, &data_padding)?;
self.inner.write_fourcc(DATA_SIG)?;
self.inner.write_u32::<LittleEndian>(0)?;
written += 8;
self.inner.seek(SeekFrom::Start(4))?;
self.inner.write_u32::<LittleEndian>(written as u32)?;
Ok(()) Ok(())
} }
fn primitive_append_chunk(&mut self, signature: FourCC, data: &[u8]) -> Result<u64,Error> { /// Finish writing audio frames and unwrap the inner `WaveWriter`.
assert!((data.len() as u32) < u32::MAX, ///
"primitive_append_chunk called with a long data buffer"); /// This method must be called when the client has finished writing audio
/// data. This will finalize the audio data chunk.
self.inner.write_fourcc(signature)?; pub fn end(self) -> Result<WaveWriter<W>, Error> {
self.inner.write_u32::<LittleEndian>(data.len() as u32)?; self.inner.end()
self.inner.write_all(&data)?; }
let padding : u64 = data.len() as u64 % 2;
if padding == 1 {
self.inner.write_u8(0)?;
} }
Ok(8 + data.len() as u64 + padding) /// Write a wave data chunk.
///
/// `WaveChunkWriter` implements `Write` and as bytes are written to it,
///
/// ### Important!
///
/// When you are done writing to a chunk you must call `end()` in order to
/// finalize the chunk for storage.
pub struct WaveChunkWriter<W>
where
W: Write + Seek,
{
ident: FourCC,
inner: WaveWriter<W>,
content_start_pos: u64,
length: u64,
}
impl<W> WaveChunkWriter<W>
where
W: Write + Seek,
{
fn begin(mut inner: WaveWriter<W>, ident: FourCC) -> Result<Self, Error> {
let length: u64 = 0;
inner.inner.write_fourcc(ident)?;
inner.inner.write_u32::<LittleEndian>(length as u32)?;
inner.increment_form_length(8)?;
let content_start_pos = inner.inner.seek(SeekFrom::End(0))?;
Ok(WaveChunkWriter {
ident,
inner,
content_start_pos,
length,
})
}
fn end(mut self) -> Result<WaveWriter<W>, Error> {
if self.length % 2 == 1 {
self.inner.inner.seek(SeekFrom::End(0))?;
self.inner.inner.write_u8(0)?;
self.inner.increment_form_length(1)?;
}
Ok(self.inner)
}
fn increment_chunk_length(&mut self, amount: u64) -> Result<(), std::io::Error> {
self.length += amount;
if !self.inner.is_rf64 {
self.inner
.inner
.seek(SeekFrom::Start(self.content_start_pos - 4))?;
self.inner
.inner
.write_u32::<LittleEndian>(self.length as u32)?;
} else if self.ident == DATA_SIG {
let data_chunk_64bit_field_offset = 8 + 4 + 8 + 8;
self.inner
.inner
.seek(SeekFrom::Start(self.content_start_pos - 4))?;
self.inner.inner.write_u32::<LittleEndian>(0xFFFF_FFFF)?;
// this only need to happen once, not every time we increment
self.inner
.inner
.seek(SeekFrom::Start(data_chunk_64bit_field_offset))?;
self.inner.inner.write_u64::<LittleEndian>(self.length)?;
} else {
todo!("FIXME RF64 wave writing is not yet supported for chunks other than `data`")
}
Ok(())
}
}
impl<W> Write for WaveChunkWriter<W>
where
W: Write + Seek,
{
fn write(&mut self, buffer: &[u8]) -> Result<usize, std::io::Error> {
self.inner.inner.seek(SeekFrom::End(0))?;
let written = self.inner.inner.write(buffer)?;
self.inner.increment_form_length(written as u64)?;
self.increment_chunk_length(written as u64)?;
Ok(written)
}
fn flush(&mut self) -> Result<(), std::io::Error> {
self.inner.inner.flush()
}
}
/// Wave, Broadcast-WAV and RF64/BW64 writer.
///
/// A `WaveWriter` creates a new wave file at the given path (with `create()`)
/// or into the given `Write`- and `Seek`-able inner writer.
///
/// Audio is added to the wave file by starting the audio data chunk with
/// `WaveWriter::audio_frame_writer()`. All of the functions that add chunks
/// move the WaveWriter and return it to the host when complete.
///
/// # Structure of New Wave Files
///
/// `WaveWriter` will create a Wave file with two chunks automatically: a 96
/// byte `JUNK` chunk and a standard `fmt ` chunk, which has the extended
/// length if the format your provided requires it. The first `JUNK` chunk is
/// a reservation for a `ds64` record which will be written over it if
/// the file needs to be upgraded to RF64 format.
///
/// Chunks are added to the file in the order the client adds them.
/// `audio_file_writer()` will add a `data` chunk for the audio data, and will
/// also add an `elm1` filler chunk prior to the data chunk to ensure that the
/// first byte of the data chunk's content is aligned with 0x4000.
///
/// ```
/// use bwavfile::{WaveWriter,WaveFmt};
/// # use std::io::Cursor;
///
/// // Write a three-sample wave file to a cursor
/// let mut cursor = Cursor::new(vec![0u8;0]);
/// let format = WaveFmt::new_pcm_mono(48000, 24);
/// let w = WaveWriter::new(&mut cursor, format).unwrap();
///
/// let mut frame_writer = w.audio_frame_writer().unwrap();
///
/// frame_writer.write_frames(&[0i32]).unwrap();
/// frame_writer.write_frames(&[0i32]).unwrap();
/// frame_writer.write_frames(&[0i32]).unwrap();
/// frame_writer.end().unwrap();
/// ```
///
/// ## Resources
///
/// ### Implementation of Wave Files
/// - [Peter Kabal, McGill University](http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html)
/// - [Multimedia Programming Interface and Data Specifications 1.0](http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/Docs/riffmci.pdf)
/// (August 1991), IBM Corporation and Microsoft Corporation
///
/// ### Implementation of Broadcast Wave Files
/// - [EBU Tech 3285][ebu3285] (May 2011), "Specification of the Broadcast Wave Format (BWF)"
/// - [Supplement 1](https://tech.ebu.ch/docs/tech/tech3285s1.pdf) (July 1997): MPEG Audio
/// - [EBU Rec 68](https://tech.ebu.ch/docs/r/r068.pdf): Signal modulation and format constraints
///
/// ### Implementation of 64-bit Wave Files
/// - [ITU-R 2088][itu2088] (October 2019), "Long-form file format for the international exchange of audio programme materials with metadata"
/// - Presently in force, adopted by the EBU in [EBU Tech 3306v2][ebu3306v2] (June 2018).
/// - [EBU Tech 3306v1][ebu3306v1] (July 2009), "MBWF / RF64: An extended File Format for Audio"
/// - No longer in force, however long-established.
///
///
/// [ebu3285]: https://tech.ebu.ch/docs/tech/tech3285.pdf
/// [ebu3306v1]: https://tech.ebu.ch/docs/tech/tech3306v1_1.pdf
/// [ebu3306v2]: https://tech.ebu.ch/docs/tech/tech3306.pdf
/// [itu2088]: https://www.itu.int/dms_pubrec/itu-r/rec/bs/R-REC-BS.2088-1-201910-I!!PDF-E.pdf
/// [rfc3261]: https://tools.ietf.org/html/rfc2361
pub struct WaveWriter<W>
where
W: Write + Seek,
{
inner: W,
form_length: u64,
/// True if file is RF64
pub is_rf64: bool,
/// Format of the wave file.
pub format: WaveFmt,
}
const DS64_RESERVATION_LENGTH: u32 = 96;
impl WaveWriter<BufWriter<File>> {
/// Create a new Wave file at `path`.
pub fn create<P: AsRef<Path>>(path: P, format: WaveFmt) -> Result<Self, Error> {
let f = File::create(path)?;
let b = BufWriter::new(f);
Self::new(b, format)
}
}
impl WaveWriter<File> {
/// Creare a new Wave file with unbuffered IO at `path`
pub fn create_unbuffered<P: AsRef<Path>>(path: P, format: WaveFmt) -> Result<Self, Error> {
let f = File::create(path)?;
Self::new(f, format)
}
}
impl<W> WaveWriter<W>
where
W: Write + Seek,
{
/// Wrap a writer in a Wave writer.
///
/// The inner writer will immediately have a RIFF WAVE file header
/// written to it along with the format descriptor (and possibly a `fact`
/// chunk if appropriate).
pub fn new(mut inner: W, format: WaveFmt) -> Result<Self, Error> {
inner.write_fourcc(RIFF_SIG)?;
inner.write_u32::<LittleEndian>(0)?;
inner.write_fourcc(WAVE_SIG)?;
let mut retval = WaveWriter {
inner,
form_length: 0,
is_rf64: false,
format,
};
retval.increment_form_length(4)?;
// write ds64_reservation
retval.write_junk(DS64_RESERVATION_LENGTH)?;
let mut chunk = retval.chunk(FMT__SIG)?;
chunk.write_wave_fmt(&format)?;
let retval = chunk.end()?;
Ok(retval)
}
fn write_chunk(&mut self, ident: FourCC, data: &[u8]) -> Result<(), Error> {
self.inner.seek(SeekFrom::End(0))?;
self.inner.write_fourcc(ident)?;
assert!(data.len() < u32::MAX as usize);
self.inner.write_u32::<LittleEndian>(data.len() as u32)?;
self.inner.write_all(data)?;
if data.len() % 2 == 0 {
self.increment_form_length(8 + data.len() as u64)?;
} else {
self.inner.write_u8(0)?;
self.increment_form_length(8 + data.len() as u64 + 1)?;
}
Ok(())
}
/// Write Broadcast-Wave metadata to the file.
///
/// This function will write the metadata chunk immediately to the end of
/// the file; if you have already written and closed the audio data the
/// bext chunk will be positioned after it.
pub fn write_broadcast_metadata(&mut self, bext: &Bext) -> Result<(), Error> {
//FIXME Implement re-writing
let mut c = Cursor::new(vec![0u8; 0]);
c.write_bext(bext)?;
let buf = c.into_inner();
self.write_chunk(BEXT_SIG, &buf)?;
Ok(())
}
/// Write iXML metadata
pub fn write_ixml(&mut self, ixml: &[u8]) -> Result<(), Error> {
//FIXME Implement re-writing
self.write_chunk(IXML_SIG, ixml)
}
/// Write axml/ADM metadata
pub fn write_axml(&mut self, axml: &[u8]) -> Result<(), Error> {
//FIXME Implement re-writing
self.write_chunk(AXML_SIG, axml)
}
/// Write a `JUNK` filler chunk
pub fn write_junk(&mut self, length: u32) -> Result<(), Error> {
let filler = vec![0u8; length as usize];
self.write_chunk(JUNK_SIG, &filler)
}
/// Create an audio frame writer, which takes possession of the callee
/// `WaveWriter`.
///
pub fn audio_frame_writer(mut self) -> Result<AudioFrameWriter<W>, Error> {
// append elm1 chunk
let framing = 0x4000;
let lip = self.inner.seek(SeekFrom::End(0))?;
let to_add = framing - (lip % framing) - 16;
let mut chunk = self.chunk(ELM1_SIG)?;
let buf = vec![0u8; to_add as usize];
chunk.write_all(&buf)?;
let closed = chunk.end()?;
let inner = closed.chunk(DATA_SIG)?;
Ok(AudioFrameWriter::new(inner))
}
/// Open a wave chunk writer here
fn chunk(mut self, ident: FourCC) -> Result<WaveChunkWriter<W>, Error> {
self.inner.seek(SeekFrom::End(0))?;
WaveChunkWriter::begin(self, ident)
}
/// Upgrade this file to RF64
fn promote_to_rf64(&mut self) -> Result<(), std::io::Error> {
if !self.is_rf64 {
self.inner.seek(SeekFrom::Start(0))?;
self.inner.write_fourcc(RF64_SIG)?;
self.inner.write_u32::<LittleEndian>(0xFFFF_FFFF)?;
self.inner.seek(SeekFrom::Start(12))?;
self.inner.write_fourcc(DS64_SIG)?;
self.inner.seek(SeekFrom::Current(4))?;
self.inner.write_u64::<LittleEndian>(self.form_length)?;
self.is_rf64 = true;
}
Ok(())
}
/// Add `amount` to the RIFF/RF64 form length
fn increment_form_length(&mut self, amount: u64) -> Result<(), std::io::Error> {
self.form_length += amount;
if self.is_rf64 {
self.inner.seek(SeekFrom::Start(8 + 4 + 8))?;
self.inner.write_u64::<LittleEndian>(self.form_length)?;
} else if self.form_length < u32::MAX as u64 {
self.inner.seek(SeekFrom::Start(4))?;
self.inner
.write_u32::<LittleEndian>(self.form_length as u32)?;
} else {
self.promote_to_rf64()?;
}
Ok(())
} }
} }
#[test] #[test]
fn test_chunk_append() -> Result<(), Error> { fn test_new() {
let mut test :Vec<u8> = vec![]; use super::fourcc::ReadFourCC;
let mut cursor = Cursor::new(test); use byteorder::ReadBytesExt;
let f = WaveFmt::new_pcm(48000, 16, 1); use std::io::Cursor;
let mut w = WaveWriter::make(cursor, f, None)?;
let mut cursor = Cursor::new(vec![0u8; 0]);
let format = WaveFmt::new_pcm_mono(4800, 24);
WaveWriter::new(&mut cursor, format).unwrap();
cursor.seek(SeekFrom::Start(0)).unwrap();
Ok(()) assert_eq!(cursor.read_fourcc().unwrap(), RIFF_SIG);
let form_size = cursor.read_u32::<LittleEndian>().unwrap();
assert_eq!(cursor.read_fourcc().unwrap(), WAVE_SIG);
assert_eq!(cursor.read_fourcc().unwrap(), JUNK_SIG);
let junk_size = cursor.read_u32::<LittleEndian>().unwrap();
assert_eq!(junk_size, 96);
cursor.seek(SeekFrom::Current(junk_size as i64)).unwrap();
assert_eq!(cursor.read_fourcc().unwrap(), FMT__SIG);
let fmt_size = cursor.read_u32::<LittleEndian>().unwrap();
assert_eq!(form_size, 4 + 8 + junk_size + 8 + fmt_size);
}
#[test]
fn test_write_audio() {
use super::fourcc::ReadFourCC;
use byteorder::ReadBytesExt;
use std::io::Cursor;
let mut cursor = Cursor::new(vec![0u8; 0]);
let format = WaveFmt::new_pcm_mono(48000, 24);
let w = WaveWriter::new(&mut cursor, format).unwrap();
let mut frame_writer = w.audio_frame_writer().unwrap();
frame_writer.write_frames(&[0i32]).unwrap();
frame_writer.write_frames(&[0i32]).unwrap();
frame_writer.write_frames(&[0i32]).unwrap();
frame_writer.end().unwrap();
cursor.seek(SeekFrom::Start(0)).unwrap();
cursor.seek(SeekFrom::Start(0)).unwrap();
assert_eq!(cursor.read_fourcc().unwrap(), RIFF_SIG);
let form_size = cursor.read_u32::<LittleEndian>().unwrap();
assert_eq!(cursor.read_fourcc().unwrap(), WAVE_SIG); //4
assert_eq!(cursor.read_fourcc().unwrap(), JUNK_SIG); //4
let junk_size = cursor.read_u32::<LittleEndian>().unwrap(); //4
cursor.seek(SeekFrom::Current(junk_size as i64)).unwrap();
assert_eq!(cursor.read_fourcc().unwrap(), FMT__SIG); //4
let fmt_size = cursor.read_u32::<LittleEndian>().unwrap(); //4
cursor.seek(SeekFrom::Current(fmt_size as i64)).unwrap();
assert_eq!(cursor.read_fourcc().unwrap(), ELM1_SIG); //4
let elm1_size = cursor.read_u32::<LittleEndian>().unwrap(); //4
cursor.seek(SeekFrom::Current(elm1_size as i64)).unwrap();
assert_eq!(cursor.read_fourcc().unwrap(), DATA_SIG); //4
let data_size = cursor.read_u32::<LittleEndian>().unwrap(); //4
assert_eq!(data_size, 9);
let tell = cursor.seek(SeekFrom::Current(0)).unwrap();
assert!(tell % 0x4000 == 0);
assert_eq!(
form_size,
4 + 8 + junk_size + 8 + fmt_size + 8 + elm1_size + 8 + data_size + data_size % 2
)
}
#[test]
fn test_write_bext() {
use std::io::Cursor;
let mut cursor = Cursor::new(vec![0u8; 0]);
let format = WaveFmt::new_pcm_mono(48000, 24);
let mut w = WaveWriter::new(&mut cursor, format).unwrap();
let bext = Bext {
description: String::from("Test description"),
originator: String::from(""),
originator_reference: String::from(""),
origination_date: String::from("2020-01-01"),
origination_time: String::from("12:34:56"),
time_reference: 0,
version: 0,
umid: None,
loudness_value: None,
loudness_range: None,
max_true_peak_level: None,
max_momentary_loudness: None,
max_short_term_loudness: None,
coding_history: String::from(""),
};
w.write_broadcast_metadata(&bext).unwrap();
let mut frame_writer = w.audio_frame_writer().unwrap();
frame_writer.write_frames(&[0i32]).unwrap();
frame_writer.write_frames(&[0i32]).unwrap();
frame_writer.write_frames(&[0i32]).unwrap();
frame_writer.end().unwrap();
}
// NOTE! This test of RF64 writing takes several minutes to complete in debug builds
#[test]
fn test_create_rf64() {
use super::fourcc::ReadFourCC;
use byteorder::ReadBytesExt;
let mut cursor = Cursor::new(vec![0u8; 0]);
let format = WaveFmt::new_pcm_stereo(48000, 24);
let w = WaveWriter::new(&mut cursor, format).unwrap();
let buflen = 16000 as u64;
let buf = vec![0i32; buflen as usize];
let four_and_a_half_hours_of_frames = 48000 * 16_200;
let mut af = w.audio_frame_writer().unwrap();
for _ in 0..(four_and_a_half_hours_of_frames * format.channel_count as u64 / buflen) {
af.write_frames(&buf).unwrap();
}
af.end().unwrap();
assert!(
cursor.seek(SeekFrom::End(0)).unwrap() > 0xFFFF_FFFFu64,
"internal test error, Created file is not long enough to be RF64"
);
let expected_data_length = four_and_a_half_hours_of_frames * format.block_alignment as u64;
cursor.seek(SeekFrom::Start(0)).unwrap();
assert_eq!(cursor.read_fourcc().unwrap(), RF64_SIG);
assert_eq!(cursor.read_u32::<LittleEndian>().unwrap(), 0xFFFF_FFFF);
assert_eq!(cursor.read_fourcc().unwrap(), WAVE_SIG);
assert_eq!(cursor.read_fourcc().unwrap(), DS64_SIG);
let ds64_size = cursor.read_u32::<LittleEndian>().unwrap();
let form_size = cursor.read_u64::<LittleEndian>().unwrap();
let data_size = cursor.read_u64::<LittleEndian>().unwrap();
assert_eq!(data_size, expected_data_length);
cursor
.seek(SeekFrom::Current(ds64_size as i64 - 16))
.unwrap();
assert_eq!(cursor.read_fourcc().unwrap(), FMT__SIG);
let fmt_size = cursor.read_u32::<LittleEndian>().unwrap();
cursor
.seek(SeekFrom::Current((fmt_size + fmt_size % 2) as i64))
.unwrap();
assert_eq!(cursor.read_fourcc().unwrap(), ELM1_SIG);
let elm1_size = cursor.read_u32::<LittleEndian>().unwrap();
let data_start = cursor
.seek(SeekFrom::Current((elm1_size + elm1_size % 2) as i64))
.unwrap();
assert!(
(data_start + 8) % 0x4000 == 0,
"data content start is not aligned, starts at {}",
data_start + 8
);
assert_eq!(cursor.read_fourcc().unwrap(), DATA_SIG);
assert_eq!(cursor.read_u32::<LittleEndian>().unwrap(), 0xFFFF_FFFF);
cursor.seek(SeekFrom::Current(data_size as i64)).unwrap();
assert_eq!(
4 + 8 + ds64_size as u64 + 8 + data_size + 8 + fmt_size as u64 + 8 + elm1_size as u64,
form_size
)
} }

View File

@@ -1,4 +1,57 @@
[ [
{
"streams": [
{
"index": 0,
"codec_name": "pcm_s24le",
"codec_long_name": "PCM signed 24-bit little-endian",
"codec_type": "audio",
"codec_time_base": "1/48000",
"codec_tag_string": "[1][0][0][0]",
"codec_tag": "0x0001",
"sample_fmt": "s32",
"sample_rate": "48000",
"channels": 2,
"channel_layout": "stereo",
"bits_per_sample": 24,
"r_frame_rate": "0/0",
"avg_frame_rate": "0/0",
"time_base": "1/48000",
"duration_ts": 4800,
"duration": "0.100000",
"bit_rate": "2304000",
"bits_per_raw_sample": "24",
"disposition": {
"default": 0,
"dub": 0,
"original": 0,
"comment": 0,
"lyrics": 0,
"karaoke": 0,
"forced": 0,
"hearing_impaired": 0,
"visual_impaired": 0,
"clean_effects": 0,
"attached_pic": 0,
"timed_thumbnails": 0
}
}
],
"format": {
"filename": "tests/media/ff_pink.wav",
"nb_streams": 1,
"nb_programs": 0,
"format_name": "wav",
"format_long_name": "WAV / WAVE (Waveform Audio)",
"duration": "0.100000",
"size": "28902",
"bit_rate": "2312160",
"probe_score": 99,
"tags": {
"encoder": "Lavf58.45.100"
}
}
},
{ {
"streams": [ "streams": [
{ {

View File

@@ -1,7 +1,6 @@
extern crate serde_json; extern crate serde_json;
use core::fmt::Debug; use core::fmt::Debug;
use serde_json::{Value, from_str}; use serde_json::{from_str, Value};
use std::fs::File; use std::fs::File;
use std::io::Read; use std::io::Read;
@@ -13,28 +12,25 @@ use bwavfile::WaveReader;
// as read by `WaveReader`. // as read by `WaveReader`.
// This is rickety but we're going with it // This is rickety but we're going with it
fn assert_match_stream<T>(stream_key: &str, fn assert_match_stream<T>(stream_key: &str, other: impl Fn(&mut WaveReader<File>) -> T)
other: impl Fn(&mut WaveReader<File>) -> T) where
where T: PartialEq + Debug, T: PartialEq + Debug,
T: Into<Value> T: Into<Value>,
{ {
let mut json_file = File::open("tests/ffprobe_media_tests.json").unwrap(); let mut json_file = File::open("tests/ffprobe_media_tests.json").unwrap();
let mut s = String::new(); let mut s = String::new();
json_file.read_to_string(&mut s).unwrap(); json_file.read_to_string(&mut s).unwrap();
if let Value::Array(v) = from_str(&mut s).unwrap() { /* */ if let Value::Array(v) = from_str(&mut s).unwrap() {
/* */
v.iter() v.iter()
.filter(|value| { .filter(|value| !value["format"]["filename"].is_null())
!value["format"]["filename"].is_null()
})
.for_each(|value| { .for_each(|value| {
let filen: &str = value["format"]["filename"].as_str().unwrap(); let filen: &str = value["format"]["filename"].as_str().unwrap();
let json_value: &Value = &value["streams"][0][stream_key]; let json_value: &Value = &value["streams"][0][stream_key];
let mut wavfile = WaveReader::open(filen).unwrap(); let mut wavfile = WaveReader::open_unbuffered(filen).unwrap();
let wavfile_value: T = other(&mut wavfile); let wavfile_value: T = other(&mut wavfile);
println!("asserting {} for {}", stream_key, filen); println!("asserting {} for {}", stream_key, filen);
assert_eq!(Into::<Value>::into(wavfile_value), *json_value); assert_eq!(Into::<Value>::into(wavfile_value), *json_value);
}) })
} }
} }
@@ -46,7 +42,9 @@ fn test_frame_count() {
#[test] #[test]
fn test_sample_rate() { fn test_sample_rate() {
assert_match_stream("sample_rate", |w| format!("{}", w.format().unwrap().sample_rate) ); assert_match_stream("sample_rate", |w| {
format!("{}", w.format().unwrap().sample_rate)
});
} }
#[test] #[test]

View File

@@ -1,16 +1,16 @@
extern crate bwavfile; extern crate bwavfile;
use bwavfile::WaveReader; use bwavfile::ChannelMask;
use bwavfile::Error; use bwavfile::Error;
use bwavfile::WaveReader;
use bwavfile::I24;
#[test] #[test]
fn test_open() { fn test_open() {
let path = "tests/media/ff_silence.wav"; let path = "tests/media/ff_silence.wav";
match WaveReader::open(path) { match WaveReader::open(path) {
Ok(_) => { Ok(_) => (),
()
},
Err(x) => { Err(x) => {
assert!(false, "Opened error.wav with unexpected error {:?}", x) assert!(false, "Opened error.wav with unexpected error {:?}", x)
} }
@@ -75,3 +75,147 @@ fn test_minimal_wave() {
assert!(true); assert!(true);
} }
} }
#[test]
fn test_read() {
let path = "tests/media/audacity_16bit.wav";
let mut w = WaveReader::open(path).expect("Failure opening test file");
let mut buffer = w.format().unwrap().create_frame_buffer::<i16>(1);
let mut reader = w.audio_frame_reader().unwrap();
assert_eq!(reader.read_frames(&mut buffer).unwrap(), 1);
assert_eq!(buffer[0], -2823_i16);
assert_eq!(reader.read_frames(&mut buffer).unwrap(), 1);
assert_eq!(buffer[0], 2012_i16);
assert_eq!(reader.read_frames(&mut buffer).unwrap(), 1);
assert_eq!(buffer[0], 4524_i16);
}
#[test]
fn test_locate_multichannel_read() {
let path = "tests/media/ff_pink.wav";
let mut w = WaveReader::open(path).expect("Failure opening test file");
let mut buffer = w.format().unwrap().create_frame_buffer::<I24>(1);
let mut reader = w.audio_frame_reader().unwrap();
assert_eq!(reader.read_frames(&mut buffer).unwrap(), 1);
assert_eq!(buffer[0], I24::from(332702));
assert_eq!(buffer[1], I24::from(3258791));
assert_eq!(reader.read_frames(&mut buffer).unwrap(), 1);
assert_eq!(buffer[0], I24::from(-258742)); // 0x800000 = 8388608 // 8129866 - 8388608
assert_eq!(buffer[1], I24::from(0x0D7EF9));
assert_eq!(reader.locate(100).unwrap(), 100);
assert_eq!(reader.read_frames(&mut buffer).unwrap(), 1);
assert_eq!(buffer[0], I24::from(0x109422));
assert_eq!(buffer[1], I24::from(-698901)); // 7689707 - 8388608
}
#[test]
fn test_channels_stereo() {
let path = "tests/media/ff_pink.wav";
let mut w = WaveReader::open(path).expect("Failure opening test file");
let channels = w.channels().unwrap();
assert_eq!(channels.len(), 2);
assert_eq!(channels[0].index, 0);
assert_eq!(channels[1].index, 1);
assert_eq!(channels[0].speaker, ChannelMask::FrontLeft);
assert_eq!(channels[1].speaker, ChannelMask::FrontRight);
}
#[test]
fn test_channels_mono_no_extended() {
let path = "tests/media/audacity_16bit.wav";
let mut w = WaveReader::open(path).expect("Failure opening test file");
let channels = w.channels().unwrap();
assert_eq!(channels.len(), 1);
assert_eq!(channels[0].index, 0);
assert_eq!(channels[0].speaker, ChannelMask::FrontCenter);
}
#[test]
fn test_channels_stereo_no_fmt_extended() {
let path = "tests/media/pt_24bit_stereo.wav";
let mut w = WaveReader::open(path).expect("Failure opening test file");
let channels = w.channels().unwrap();
assert_eq!(channels.len(), 2);
assert_eq!(channels[0].index, 0);
assert_eq!(channels[1].index, 1);
assert_eq!(channels[0].speaker, ChannelMask::FrontLeft);
assert_eq!(channels[1].speaker, ChannelMask::FrontRight);
}
///See issue 6 and 7
#[test]
fn test_frame_reader_consumes_reader() {
// Issue #6
use bwavfile::{AudioFrameReader, WaveFmt};
use std::fs::File;
fn from_wav_filename(
wav_filename: &str,
) -> Result<(WaveFmt, AudioFrameReader<std::io::BufReader<File>>), ()> {
if let Ok(mut r) = WaveReader::open(&wav_filename) {
let format = r.format().unwrap();
let frame_reader = r.audio_frame_reader().unwrap();
Ok((format, frame_reader))
} else {
Err(())
}
}
let _result = from_wav_filename("tests/media/pt_24bit_stereo.wav").unwrap();
}
///See to PR#10
#[test]
fn test_cue_read_sounddevices() {
let mut f = WaveReader::open("tests/media/sounddevices_6_cue_points.wav").unwrap();
let cue_points = f.cue_points().unwrap();
assert_eq!(cue_points.len(), 6);
assert_eq!(cue_points[0].frame, 0);
assert_eq!(cue_points[0].length, None);
assert_eq!(cue_points[0].label, None);
assert_eq!(cue_points[0].note, None);
assert_eq!(cue_points[0].offset, 90112);
assert_eq!(cue_points[1].frame, 0);
assert_eq!(cue_points[1].length, None);
assert_eq!(cue_points[1].label, None);
assert_eq!(cue_points[1].note, None);
assert_eq!(cue_points[1].offset, 176128);
assert_eq!(cue_points[2].frame, 0);
assert_eq!(cue_points[2].length, None);
assert_eq!(cue_points[2].label, None);
assert_eq!(cue_points[2].note, None);
assert_eq!(cue_points[2].offset, 237568);
assert_eq!(cue_points[3].frame, 0);
assert_eq!(cue_points[3].length, None);
assert_eq!(cue_points[3].label, None);
assert_eq!(cue_points[3].note, None);
assert_eq!(cue_points[3].offset, 294912);
assert_eq!(cue_points[4].frame, 0);
assert_eq!(cue_points[4].length, None);
assert_eq!(cue_points[4].label, None);
assert_eq!(cue_points[4].note, None);
assert_eq!(cue_points[4].offset, 380928);
assert_eq!(cue_points[5].frame, 0);
assert_eq!(cue_points[5].length, None);
assert_eq!(cue_points[5].label, None);
assert_eq!(cue_points[5].note, None);
assert_eq!(cue_points[5].offset, 385024);
}

Binary file not shown.