wavefile 0.8.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/LICENSE +1 -1
- data/README.markdown +33 -28
- data/Rakefile +2 -2
- data/lib/wavefile.rb +5 -5
- data/lib/wavefile/chunk_readers/format_chunk_reader.rb +1 -1
- data/lib/wavefile/duration.rb +1 -1
- data/lib/wavefile/format.rb +120 -29
- data/lib/wavefile/reader.rb +18 -16
- data/lib/wavefile/unvalidated_format.rb +51 -3
- data/lib/wavefile/writer.rb +74 -21
- data/test/buffer_test.rb +13 -10
- data/test/chunk_readers/format_chunk_reader_test.rb +40 -40
- data/test/fixtures/{invalid → wave/invalid}/bad_riff_header.wav +0 -0
- data/test/fixtures/{invalid → wave/invalid}/bad_wavefile_format.wav +0 -0
- data/test/fixtures/{invalid → wave/invalid}/empty.wav +0 -0
- data/test/fixtures/{invalid → wave/invalid}/empty_format_chunk.wav +0 -0
- data/test/fixtures/{invalid → wave/invalid}/incomplete_riff_header.wav +0 -0
- data/test/fixtures/{invalid → wave/invalid}/insufficient_format_chunk.wav +0 -0
- data/test/fixtures/{invalid → wave/invalid}/no_data_chunk.wav +0 -0
- data/test/fixtures/{invalid → wave/invalid}/no_format_chunk.wav +0 -0
- data/test/fixtures/{unsupported → wave/unsupported}/bad_audio_format.wav +0 -0
- data/test/fixtures/{unsupported → wave/unsupported}/bad_channel_count.wav +0 -0
- data/test/fixtures/{unsupported → wave/unsupported}/bad_sample_rate.wav +0 -0
- data/test/fixtures/{unsupported → wave/unsupported}/extensible_container_size_bigger_than_sample_size.wav +0 -0
- data/test/fixtures/wave/unsupported/extensible_unsupported_subformat_guid.wav +0 -0
- data/test/fixtures/{unsupported → wave/unsupported}/unsupported_audio_format.wav +0 -0
- data/test/fixtures/{unsupported → wave/unsupported}/unsupported_bits_per_sample.wav +0 -0
- data/test/fixtures/{valid → wave/valid}/no_samples.wav +0 -0
- data/test/fixtures/wave/valid/valid_extensible_20_pcm_16_44100_speaker_mapping_overflow.wav +0 -0
- data/test/fixtures/{valid → wave/valid}/valid_extensible_mono_float_32_44100.wav +0 -0
- data/test/fixtures/{valid → wave/valid}/valid_extensible_mono_float_64_44100.wav +0 -0
- data/test/fixtures/wave/valid/valid_extensible_mono_pcm_16_44100.wav +0 -0
- data/test/fixtures/wave/valid/valid_extensible_mono_pcm_16_44100_non_default_speaker_mapping.wav +0 -0
- data/test/fixtures/wave/valid/valid_extensible_mono_pcm_24_44100.wav +0 -0
- data/test/fixtures/wave/valid/valid_extensible_mono_pcm_32_44100.wav +0 -0
- data/test/fixtures/wave/valid/valid_extensible_mono_pcm_8_44100.wav +0 -0
- data/test/fixtures/{valid → wave/valid}/valid_extensible_stereo_float_32_44100.wav +0 -0
- data/test/fixtures/{valid → wave/valid}/valid_extensible_stereo_float_64_44100.wav +0 -0
- data/test/fixtures/wave/valid/valid_extensible_stereo_pcm_16_44100.wav +0 -0
- data/test/fixtures/wave/valid/valid_extensible_stereo_pcm_16_44100_center_right_speakers.wav +0 -0
- data/test/fixtures/wave/valid/valid_extensible_stereo_pcm_16_44100_more_speakers_than_channels.wav +0 -0
- data/test/fixtures/wave/valid/valid_extensible_stereo_pcm_16_44100_more_speakers_than_defined_by_spec.wav +0 -0
- data/test/fixtures/wave/valid/valid_extensible_stereo_pcm_16_44100_only_undefined_high_bit_speakers.wav +0 -0
- data/test/fixtures/wave/valid/valid_extensible_stereo_pcm_24_44100.wav +0 -0
- data/test/fixtures/wave/valid/valid_extensible_stereo_pcm_24_44100_incomplete_speaker_mapping.wav +0 -0
- data/test/fixtures/wave/valid/valid_extensible_stereo_pcm_24_44100_no_speaker_mapping.wav +0 -0
- data/test/fixtures/wave/valid/valid_extensible_stereo_pcm_32_44100.wav +0 -0
- data/test/fixtures/wave/valid/valid_extensible_stereo_pcm_8_44100.wav +0 -0
- data/test/fixtures/{valid → wave/valid}/valid_extensible_tri_float_32_44100.wav +0 -0
- data/test/fixtures/{valid → wave/valid}/valid_extensible_tri_float_64_44100.wav +0 -0
- data/test/fixtures/wave/valid/valid_extensible_tri_pcm_16_44100.wav +0 -0
- data/test/fixtures/wave/valid/valid_extensible_tri_pcm_16_44100_custom_speaker_mapping.wav +0 -0
- data/test/fixtures/wave/valid/valid_extensible_tri_pcm_24_44100.wav +0 -0
- data/test/fixtures/wave/valid/valid_extensible_tri_pcm_32_44100.wav +0 -0
- data/test/fixtures/wave/valid/valid_extensible_tri_pcm_8_44100.wav +0 -0
- data/test/fixtures/{valid → wave/valid}/valid_mono_float_32_44100.wav +0 -0
- data/test/fixtures/{valid → wave/valid}/valid_mono_float_64_44100.wav +0 -0
- data/test/fixtures/{valid → wave/valid}/valid_mono_pcm_16_44100.wav +0 -0
- data/test/fixtures/{valid → wave/valid}/valid_mono_pcm_16_44100_junk_chunk_with_padding_byte.wav +0 -0
- data/test/fixtures/wave/valid/valid_mono_pcm_16_44100_with_extension.wav +0 -0
- data/test/fixtures/{valid → wave/valid}/valid_mono_pcm_24_44100.wav +0 -0
- data/test/fixtures/{valid → wave/valid}/valid_mono_pcm_32_44100.wav +0 -0
- data/test/fixtures/{valid → wave/valid}/valid_mono_pcm_8_44100.wav +0 -0
- data/test/fixtures/{valid → wave/valid}/valid_mono_pcm_8_44100_with_padding_byte.wav +0 -0
- data/test/fixtures/{valid → wave/valid}/valid_stereo_float_32_44100.wav +0 -0
- data/test/fixtures/{valid → wave/valid}/valid_stereo_float_64_44100.wav +0 -0
- data/test/fixtures/{valid → wave/valid}/valid_stereo_pcm_16_44100.wav +0 -0
- data/test/fixtures/{valid → wave/valid}/valid_stereo_pcm_24_44100.wav +0 -0
- data/test/fixtures/{valid → wave/valid}/valid_stereo_pcm_32_44100.wav +0 -0
- data/test/fixtures/{valid → wave/valid}/valid_stereo_pcm_8_44100.wav +0 -0
- data/test/fixtures/{valid → wave/valid}/valid_tri_float_32_44100.wav +0 -0
- data/test/fixtures/{valid → wave/valid}/valid_tri_float_64_44100.wav +0 -0
- data/test/fixtures/{valid → wave/valid}/valid_tri_pcm_16_44100.wav +0 -0
- data/test/fixtures/{valid → wave/valid}/valid_tri_pcm_24_44100.wav +0 -0
- data/test/fixtures/{valid → wave/valid}/valid_tri_pcm_32_44100.wav +0 -0
- data/test/fixtures/{valid → wave/valid}/valid_tri_pcm_8_44100.wav +0 -0
- data/test/format_test.rb +189 -3
- data/test/reader_test.rb +179 -4
- data/test/unvalidated_format_test.rb +181 -6
- data/test/wavefile_io_test_helper.rb +11 -1
- data/test/writer_test.rb +246 -25
- metadata +70 -80
- data/test/fixtures/actual_output/total_duration_mono_float_32_44100.wav +0 -0
- data/test/fixtures/actual_output/total_duration_mono_float_64_44100.wav +0 -0
- data/test/fixtures/actual_output/total_duration_mono_pcm_16_44100.wav +0 -0
- data/test/fixtures/actual_output/total_duration_mono_pcm_24_44100.wav +0 -0
- data/test/fixtures/actual_output/total_duration_mono_pcm_32_44100.wav +0 -0
- data/test/fixtures/actual_output/total_duration_mono_pcm_8_44100.wav +0 -0
- data/test/fixtures/actual_output/total_duration_stereo_float_32_44100.wav +0 -0
- data/test/fixtures/actual_output/total_duration_stereo_float_64_44100.wav +0 -0
- data/test/fixtures/actual_output/total_duration_stereo_pcm_16_44100.wav +0 -0
- data/test/fixtures/actual_output/total_duration_stereo_pcm_24_44100.wav +0 -0
- data/test/fixtures/actual_output/total_duration_stereo_pcm_32_44100.wav +0 -0
- data/test/fixtures/actual_output/total_duration_stereo_pcm_8_44100.wav +0 -0
- data/test/fixtures/actual_output/total_duration_tri_float_32_44100.wav +0 -0
- data/test/fixtures/actual_output/total_duration_tri_float_64_44100.wav +0 -0
- data/test/fixtures/actual_output/total_duration_tri_pcm_16_44100.wav +0 -0
- data/test/fixtures/actual_output/total_duration_tri_pcm_24_44100.wav +0 -0
- data/test/fixtures/actual_output/total_duration_tri_pcm_32_44100.wav +0 -0
- data/test/fixtures/actual_output/total_duration_tri_pcm_8_44100.wav +0 -0
- data/test/fixtures/invalid/README.markdown +0 -10
- data/test/fixtures/unsupported/README.markdown +0 -6
- data/test/fixtures/unsupported/extensible_unsupported_subformat_guid.wav +0 -0
- data/test/fixtures/valid/README.markdown +0 -3
- data/test/fixtures/valid/valid_extensible_mono_pcm_16_44100.wav +0 -0
- data/test/fixtures/valid/valid_extensible_mono_pcm_24_44100.wav +0 -0
- data/test/fixtures/valid/valid_extensible_mono_pcm_32_44100.wav +0 -0
- data/test/fixtures/valid/valid_extensible_mono_pcm_8_44100.wav +0 -0
- data/test/fixtures/valid/valid_extensible_stereo_pcm_16_44100.wav +0 -0
- data/test/fixtures/valid/valid_extensible_stereo_pcm_24_44100.wav +0 -0
- data/test/fixtures/valid/valid_extensible_stereo_pcm_32_44100.wav +0 -0
- data/test/fixtures/valid/valid_extensible_stereo_pcm_8_44100.wav +0 -0
- data/test/fixtures/valid/valid_extensible_tri_pcm_16_44100.wav +0 -0
- data/test/fixtures/valid/valid_extensible_tri_pcm_24_44100.wav +0 -0
- data/test/fixtures/valid/valid_extensible_tri_pcm_32_44100.wav +0 -0
- data/test/fixtures/valid/valid_extensible_tri_pcm_8_44100.wav +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 345be2bed4e76c59bfb0163cc844399ff54bc7ca2643b82ba555c4bf167d5b5e
|
4
|
+
data.tar.gz: 1923c71c3af5438319425709de665acd6bf6be53f67058f7bf4b99c43d7a2493
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3134a277c3c239e41e0dc60dda461223664a316e2ea16b299b61d137deee8cb545ccde99bac35566a8834571da2538aa973551d6f27b0521be9f78c0c8d88765
|
7
|
+
data.tar.gz: '069c9654d04eabb25c5125983b658ff468697de5eb551b7673be42fe6775a470899d20ba19e7e44c9fcd280377866ade72b1bf1aa672523065155e75c17c0604'
|
data/LICENSE
CHANGED
data/README.markdown
CHANGED
@@ -1,12 +1,12 @@
|
|
1
|
-
A
|
1
|
+
A Ruby gem for reading and writing sound files in Wave format (*.wav).
|
2
2
|
|
3
|
-
You can use this gem to create Ruby programs that work with audio, such as a [command-line drum machine](
|
3
|
+
You can use this gem to create Ruby programs that work with audio, such as a [command-line drum machine](https://beatsdrummachine.com). Since it is written in pure Ruby (as opposed to wrapping an existing C library), you can use it without having to compile a separate extension.
|
4
4
|
|
5
5
|
For more info, check out the website: <http://wavefilegem.com/>
|
6
6
|
|
7
7
|
# Example Usage
|
8
8
|
|
9
|
-
This
|
9
|
+
This short example shows how to append three separate Wave files into a single file:
|
10
10
|
|
11
11
|
```ruby
|
12
12
|
require 'wavefile'
|
@@ -30,44 +30,44 @@ More examples can be found at <http://wavefilegem.com/examples>.
|
|
30
30
|
|
31
31
|
This gem lets you read and write audio data! You can use it to create Ruby programs that work with sound.
|
32
32
|
|
33
|
-
* Read and write Wave files with any number of channels, in PCM (8/16/24/32 bits per sample) or
|
33
|
+
* Read and write Wave files with any number of channels, in integer PCM (8/16/24/32 bits per sample) or floating point PCM (32/64 bits per sample) format.
|
34
34
|
* Seamlessly convert between sample formats. Read sample data from a file into any format supported by this gem, regardless of how the sample data is stored in the actual file. Or, create sample data in one format (such as floats between -1.0 and 1.0), but write it to a file in a different format (such as 16-bit PCM).
|
35
35
|
* Automatic file management, similar to how `IO.open` works. That is, you can open a file for reading or writing, and if a block is given, the file will automatically be closed when the block exits.
|
36
36
|
* Query metadata about Wave files (sample rate, number of channels, number of sample frames, etc.), including files that are in a format this gem can't read or write.
|
37
37
|
* Written in pure Ruby, so it's easy to include in your program. There's no need to compile a separate extension in order to use it.
|
38
38
|
|
39
39
|
|
40
|
-
# Current Release:
|
40
|
+
# Current Release: v1.0.0
|
41
41
|
|
42
|
-
Released on
|
42
|
+
Released on June 10, 2018, this version has these changes:
|
43
43
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
*
|
49
|
-
|
50
|
-
*
|
51
|
-
|
52
|
-
|
53
|
-
*
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
*
|
58
|
-
* A
|
59
|
-
*
|
60
|
-
|
61
|
-
|
44
|
+
* **Ruby 2.0 or greater is now required** - the gem no longer works in Ruby 1.9.3.
|
45
|
+
* **Backwards incompatible change:** Calling `Reader.close` on a `Reader` instance that is already closed no longer raises `ReaderClosedError`. Instead, it does nothing. Similarly, calling `Writer.close` on a `Writer` instance that is already closed no longer raises `WriterClosedError`. Thanks to [@kylekyle](https://github.com/kylekyle) for raising this as an issue.
|
46
|
+
* **Better compatibility when writing Wave files.** `Writer` will now write files using a format called WAVE_FORMAT_EXTENSIBLE where appropriate. This is a behind-the-scenes improvement - for most use cases it won't affect how you use the gem, but can result in better compatibility with other programs.
|
47
|
+
* A file will automatically be written using WAVE_FORMAT_EXTENSIBLE format if any of the following are true:
|
48
|
+
* It has more than 2 channels
|
49
|
+
* It uses integer PCM sample format and the bits per sample is not 8 or 16 (in other words, if the sample format is `:pcm_24` or `:pcm_32`).
|
50
|
+
* A specific channel->speaker mapping is given (see below).
|
51
|
+
* **The channel->speaker mapping field can now be read from files that have it defined.** For example, if a file indicates that the first sound channel should be mapped to the back right speaker, the second channel to the top center speaker, etc., this can be read using the `Reader.format.speaker_mapping` field.
|
52
|
+
* Example:
|
53
|
+
* ~~~
|
54
|
+
reader = Reader.new("4_channel_file.wav")
|
55
|
+
puts reader.format.speaker_mapping.inspect # [:front_left, :front_right, :front_center, :back_center]
|
56
|
+
~~~
|
57
|
+
* The channel->speaker mapping field isn't present in all Wave files. (Specifically, it's only present if the file uses WAVE_FORMAT_EXTENSIBLE format). For a non-WAVE_FORMAT_EXTENSIBLE file, `Reader.native_format.speaker_mapping` will be `nil`, to reflect that the channel->speaker mapping is undefined. `Reader.format.speaker_mapping` will use a "sensible" default value for the given number of channels.
|
58
|
+
* **A channel->speaker mapping array can optionally be given when constructing a `Format` instance.** If not given, a default value will be set for the given number of channels.
|
59
|
+
* Example:
|
60
|
+
* `Format.new(4, :pcm_16, 44100, speaker_mapping: [:front_left, :front_right, :front_center, :low_frequency])`
|
61
|
+
* **Errors raised by `Format.new` are improved to provide more detail.**
|
62
62
|
|
63
63
|
|
64
64
|
# Compatibility
|
65
65
|
|
66
66
|
WaveFile has been tested with these Ruby versions, and appears to be compatible with them:
|
67
67
|
|
68
|
-
* MRI 2.4.
|
68
|
+
* MRI 2.5.1, 2.4.4, 2.3.7, 2.2.10, 2.1.10, 2.0
|
69
69
|
|
70
|
-
|
70
|
+
2.0 is the minimum supported Ruby version.
|
71
71
|
|
72
72
|
If you find any compatibility issues, please let me know by opening a GitHub issue.
|
73
73
|
|
@@ -87,7 +87,7 @@ Note that if you're installing the gem into the default Ruby that comes pre-inst
|
|
87
87
|
|
88
88
|
# Dependencies
|
89
89
|
|
90
|
-
WaveFile has no external dependencies when used as a gem.
|
90
|
+
WaveFile has no external dependencies when used as a gem.
|
91
91
|
|
92
92
|
However, it does have dependencies for local development, in order to run the tests. See below in section "Local Development".
|
93
93
|
|
@@ -106,10 +106,15 @@ Then, to run the tests:
|
|
106
106
|
|
107
107
|
## Generating test fixtures
|
108
108
|
|
109
|
-
|
109
|
+
The `*.wav` fixtures in `test/fixtures/wave` are generated from `*.yml` files defined in `/test/fixtures/yaml`. To change one of the `*.wav` fixtures, edit the corresponding `*.yml` file, and then run:
|
110
110
|
|
111
111
|
rake test:create_fixtures
|
112
112
|
|
113
|
+
Similarly, if you want to add a new `*.wav` fixture, add a new `*.yml` file that describes it in `/test/fixtures/yaml`, and then run the rake command above.
|
114
|
+
|
115
|
+
Behind the scenes, `rake test:create_fixtures` runs `tools/fixture_writer.rb`, which is what actually generates each `*.wav` file.
|
116
|
+
|
117
|
+
|
113
118
|
## Generating RDoc Documentation
|
114
119
|
|
115
120
|
rake rdoc
|
data/Rakefile
CHANGED
@@ -18,11 +18,11 @@ end
|
|
18
18
|
namespace :test do
|
19
19
|
task :create_fixtures do
|
20
20
|
["valid", "invalid", "unsupported"].each do |subfolder|
|
21
|
-
fixtures = Dir.glob("
|
21
|
+
fixtures = Dir.glob("test/fixtures/yaml/#{subfolder}/*.yml")
|
22
22
|
|
23
23
|
fixtures.each do |fixture|
|
24
24
|
basename = File.basename(fixture, ".yml")
|
25
|
-
`ruby tools/fixture_writer.rb #{fixture} test/fixtures/#{subfolder}/#{basename}.wav`
|
25
|
+
`ruby tools/fixture_writer.rb #{fixture} test/fixtures/wave/#{subfolder}/#{basename}.wav`
|
26
26
|
end
|
27
27
|
end
|
28
28
|
end
|
data/lib/wavefile.rb
CHANGED
@@ -7,10 +7,10 @@ require 'wavefile/unvalidated_format'
|
|
7
7
|
require 'wavefile/writer'
|
8
8
|
|
9
9
|
module WaveFile
|
10
|
-
VERSION = "0.
|
10
|
+
VERSION = "1.0.0"
|
11
11
|
|
12
12
|
WAVEFILE_FORMAT_CODE = "WAVE" # :nodoc:
|
13
|
-
FORMAT_CODES = {:pcm => 1, :float => 3, :extensible => 65534} # :nodoc:
|
13
|
+
FORMAT_CODES = {:pcm => 1, :float => 3, :extensible => 65534}.freeze # :nodoc:
|
14
14
|
SUB_FORMAT_GUID_PCM = String.new("\x01\x00\x00\x00\x00\x00\x10\x00\x80\x00\x00\xAA\x00\x38\x9B\x71").force_encoding("ASCII-8BIT").freeze # :nodoc:
|
15
15
|
SUB_FORMAT_GUID_FLOAT = String.new("\x03\x00\x00\x00\x00\x00\x10\x00\x80\x00\x00\xAA\x00\x38\x9B\x71").force_encoding("ASCII-8BIT").freeze # :nodoc:
|
16
16
|
CHUNK_IDS = {:riff => "RIFF",
|
@@ -25,10 +25,10 @@ module WaveFile
|
|
25
25
|
:labeled_text => "ltxt",
|
26
26
|
:note => "note",
|
27
27
|
:sample => "smpl",
|
28
|
-
:instrument => "inst" } # :nodoc:
|
28
|
+
:instrument => "inst" }.freeze # :nodoc:
|
29
29
|
|
30
|
-
PACK_CODES = {:pcm => { 8 => "C*", 16 => "s<*", 24 => "C*", 32 => "l<*"},
|
31
|
-
:float => { 32 => "e*", 64 => "E*"}} # :nodoc:
|
30
|
+
PACK_CODES = {:pcm => { 8 => "C*", 16 => "s<*", 24 => "C*", 32 => "l<*"}.freeze,
|
31
|
+
:float => { 32 => "e*", 64 => "E*"}.freeze}.freeze # :nodoc:
|
32
32
|
|
33
33
|
UNSIGNED_INT_16 = "v" # :nodoc:
|
34
34
|
UNSIGNED_INT_32 = "V" # :nodoc:
|
@@ -33,7 +33,7 @@ module WaveFile
|
|
33
33
|
raise_error InvalidFormatError, "The format chunk extension is shorter than expected."
|
34
34
|
end
|
35
35
|
|
36
|
-
if format_chunk[:
|
36
|
+
if format_chunk[:audio_format] == FORMAT_CODES[:extensible]
|
37
37
|
format_chunk[:valid_bits_per_sample] = raw_bytes.slice!(0...2).unpack(UNSIGNED_INT_16).first
|
38
38
|
format_chunk[:speaker_mapping] = raw_bytes.slice!(0...4).unpack(UNSIGNED_INT_32).first
|
39
39
|
format_chunk[:sub_audio_format_guid] = raw_bytes
|
data/lib/wavefile/duration.rb
CHANGED
data/lib/wavefile/format.rb
CHANGED
@@ -4,8 +4,9 @@ module WaveFile
|
|
4
4
|
# this Gem. Or, because it's a not a valid Wave file period.
|
5
5
|
class FormatError < StandardError; end
|
6
6
|
|
7
|
-
# Public: Error that is raised when
|
8
|
-
#
|
7
|
+
# Public: Error that is raised when constructing a Format instance that is not valid,
|
8
|
+
# trying to read from a file that is not a wave file, or trying to read from a file
|
9
|
+
# that is not valid according to the wave file spec.
|
9
10
|
class InvalidFormatError < FormatError; end
|
10
11
|
|
11
12
|
# Public: Error that is raised when trying to read from a valid wave file that has its sample data
|
@@ -23,13 +24,26 @@ module WaveFile
|
|
23
24
|
# Public: Constructs a new immutable Format.
|
24
25
|
#
|
25
26
|
# channels - The number of channels in the format. Can either be an Integer
|
26
|
-
# (e.g. 1, 2, 3) or the symbols
|
27
|
-
#
|
27
|
+
# (e.g. 1, 2, 3) or the symbols +:mono+ (equivalent to 1) or
|
28
|
+
# +:stereo+ (equivalent to 2).
|
28
29
|
# format_code - A symbol indicating the format of each sample. Consists of
|
29
30
|
# two parts: a format code, and the bits per sample. The valid
|
30
|
-
# values are
|
31
|
-
#
|
31
|
+
# values are +:pcm_8+, +:pcm_16+, +:pcm_24+, +:pcm_32+, +:float_32+,
|
32
|
+
# +:float_64+, and +:float+ (equivalent to +:float_32+)
|
32
33
|
# sample_rate - The number of samples per second, such as 44100
|
34
|
+
# speaker_mapping - An optional array which indicates which speaker each channel should be
|
35
|
+
# mapped to. Each value in the array should be one of these values:
|
36
|
+
# +:front_left+, +:front_right+, +:front_center+, +:low_frequency+, +:back_left+,
|
37
|
+
# +:back_right+, +:front_left_of_center+, +:front_right_of_center+,
|
38
|
+
# +:back_center+, +:side_left+, +:side_right+, +:top_center+, +:top_front_left+,
|
39
|
+
# +:top_front_center+, +:top_front_right+, +:top_back_left+, +:top_back_center+,
|
40
|
+
# +:top_back_right+. Each value should only appear once, and the channels
|
41
|
+
# must follow the ordering above. For example, [:front_center, :back_left]
|
42
|
+
# is a valid speaker mapping, but [:back_left, :front_center] is not.
|
43
|
+
# If a given channel should not be mapped to a specific speaker, the
|
44
|
+
# value :undefined can be used. If this field is omitted, a default
|
45
|
+
# value for the given number of channels. For example, if there are 2
|
46
|
+
# channels, this will be set to [:front_left, :front_right].
|
33
47
|
#
|
34
48
|
# Examples
|
35
49
|
#
|
@@ -38,20 +52,42 @@ module WaveFile
|
|
38
52
|
#
|
39
53
|
# format = Format.new(:stereo, :float_32, 44100)
|
40
54
|
# format = Format.new(:stereo, :float, 44100) # Equivalent to above
|
41
|
-
|
55
|
+
#
|
56
|
+
# format = Format.new(2, :pcm_16, 44100, speaker_mapping: [:front_right, :front_center])
|
57
|
+
#
|
58
|
+
# # Channels should explicitly not be mapped to particular speakers
|
59
|
+
# # (otherwise, if no speaker_mapping set, it will be set to a default
|
60
|
+
# # value for the number of channels).
|
61
|
+
# format = Format.new(2, :pcm_16, 44100, speaker_mapping: [:undefined, :undefined])
|
62
|
+
#
|
63
|
+
# # Will result in InvalidFormatError, because speakers are defined in
|
64
|
+
# # invalid order
|
65
|
+
# format = Format.new(2, :pcm_16, 44100, speaker_mapping: [:front_right, :front_left])
|
66
|
+
#
|
67
|
+
# # speaker_mapping will be set to [:front_left, :undefined, :undefined],
|
68
|
+
# # because channels without a speaker mapping will be mapped to :undefined
|
69
|
+
# format = Format.new(3, :pcm_16, 44100, speaker_mapping: [:front_left])
|
70
|
+
#
|
71
|
+
# Raises InvalidFormatError if the given arguments are invalid.
|
72
|
+
def initialize(channels, format_code, sample_rate, speaker_mapping: nil)
|
42
73
|
channels = normalize_channels(channels)
|
43
|
-
|
74
|
+
|
44
75
|
validate_channels(channels)
|
45
|
-
|
46
|
-
validate_bits_per_sample(sample_format, bits_per_sample)
|
76
|
+
validate_format_code(format_code)
|
47
77
|
validate_sample_rate(sample_rate)
|
48
78
|
|
79
|
+
sample_format, bits_per_sample = normalize_format_code(format_code)
|
80
|
+
|
81
|
+
speaker_mapping = normalize_speaker_mapping(channels, speaker_mapping)
|
82
|
+
validate_speaker_mapping(channels, speaker_mapping)
|
83
|
+
|
49
84
|
@channels = channels
|
50
85
|
@sample_format = sample_format
|
51
86
|
@bits_per_sample = bits_per_sample
|
52
87
|
@sample_rate = sample_rate
|
53
88
|
@block_align = (@bits_per_sample / 8) * @channels
|
54
89
|
@byte_rate = @block_align * @sample_rate
|
90
|
+
@speaker_mapping = speaker_mapping
|
55
91
|
end
|
56
92
|
|
57
93
|
# Public: Returns true if the format has 1 channel, false otherwise.
|
@@ -86,6 +122,9 @@ module WaveFile
|
|
86
122
|
# Is equivalent to block_align * sample_rate.
|
87
123
|
attr_reader :byte_rate
|
88
124
|
|
125
|
+
# Public: Returns the mapping of each channel to a speaker.
|
126
|
+
attr_reader :speaker_mapping
|
127
|
+
|
89
128
|
private
|
90
129
|
|
91
130
|
# Internal
|
@@ -94,12 +133,7 @@ module WaveFile
|
|
94
133
|
VALID_SAMPLE_RATE_RANGE = 1..4_294_967_296 # :nodoc:
|
95
134
|
|
96
135
|
# Internal
|
97
|
-
|
98
|
-
# Internal
|
99
|
-
SUPPORTED_BITS_PER_SAMPLE = {
|
100
|
-
:pcm => [8, 16, 24, 32],
|
101
|
-
:float => [32, 64],
|
102
|
-
} # :nodoc:
|
136
|
+
SUPPORTED_FORMAT_CODES = [:pcm_8, :pcm_16, :pcm_24, :pcm_32, :float, :float_32, :float_64].freeze # :nodoc:
|
103
137
|
|
104
138
|
# Internal
|
105
139
|
def normalize_channels(channels)
|
@@ -123,37 +157,94 @@ module WaveFile
|
|
123
157
|
end
|
124
158
|
|
125
159
|
# Internal
|
126
|
-
def
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
160
|
+
def normalize_speaker_mapping(channels, speaker_mapping)
|
161
|
+
if speaker_mapping.nil?
|
162
|
+
speaker_mapping = default_speaker_mapping(channels)
|
163
|
+
elsif !speaker_mapping.is_a?(Array)
|
164
|
+
return speaker_mapping
|
165
|
+
else
|
166
|
+
speaker_mapping = speaker_mapping.dup
|
167
|
+
end
|
168
|
+
|
169
|
+
if speaker_mapping.length < channels
|
170
|
+
speaker_mapping += [:undefined] * (channels - speaker_mapping.length)
|
171
|
+
end
|
172
|
+
|
173
|
+
speaker_mapping.freeze
|
174
|
+
end
|
175
|
+
|
176
|
+
# Internal
|
177
|
+
def default_speaker_mapping(channels)
|
178
|
+
# These default mappings determined from these sources:
|
179
|
+
#
|
180
|
+
# See https://docs.microsoft.com/en-us/windows-hardware/drivers/audio/extensible-wave-format-descriptors
|
181
|
+
# This article says to use the `front_center` speaker for mono files when using WAVE_FORMAT_EXTENSIBLE
|
182
|
+
#
|
183
|
+
# https://msdn.microsoft.com/en-us/library/windows/desktop/dd390971(v=vs.85).aspx
|
184
|
+
#
|
185
|
+
# https://xiph.org/flac/format.html#frame_header
|
186
|
+
if channels == 1 # Mono
|
187
|
+
[:front_center]
|
188
|
+
elsif channels == 2 # Stereo
|
189
|
+
[:front_left, :front_right]
|
190
|
+
elsif channels == 3
|
191
|
+
[:front_left, :front_right, :front_center]
|
192
|
+
elsif channels == 4 # Quad
|
193
|
+
[:front_left, :front_right, :back_left, :back_right]
|
194
|
+
elsif channels == 5
|
195
|
+
[:front_left, :front_right, :front_center, :back_left, :back_right]
|
196
|
+
elsif channels == 6 # 5.1
|
197
|
+
[:front_left, :front_right, :front_center, :low_frequency, :back_left, :back_right]
|
198
|
+
elsif channels == 7
|
199
|
+
[:front_left, :front_right, :front_center, :low_frequency, :back_center, :side_left, :side_right]
|
200
|
+
elsif channels == 8 # 7.1
|
201
|
+
[:front_left, :front_right, :front_center, :low_frequency, :back_left, :back_right, :front_left_of_center, :front_right_of_center]
|
202
|
+
elsif channels <= UnvalidatedFormat::SPEAKER_POSITIONS.length
|
203
|
+
UnvalidatedFormat::SPEAKER_POSITIONS[0...channels]
|
204
|
+
else
|
205
|
+
UnvalidatedFormat::SPEAKER_POSITIONS
|
131
206
|
end
|
132
207
|
end
|
133
208
|
|
134
209
|
# Internal
|
135
210
|
def validate_channels(candidate_channels)
|
136
|
-
unless VALID_CHANNEL_RANGE === candidate_channels
|
211
|
+
unless candidate_channels.is_a?(Integer) && VALID_CHANNEL_RANGE === candidate_channels
|
137
212
|
raise InvalidFormatError,
|
138
|
-
"Invalid number of channels
|
213
|
+
"Invalid number of channels: `#{candidate_channels}`. Must be an Integer between #{VALID_CHANNEL_RANGE.min} and #{VALID_CHANNEL_RANGE.max}."
|
139
214
|
end
|
140
215
|
end
|
141
216
|
|
142
217
|
# Internal
|
143
|
-
def
|
144
|
-
unless
|
218
|
+
def validate_format_code(candidate_format_code)
|
219
|
+
unless SUPPORTED_FORMAT_CODES.include? candidate_format_code
|
145
220
|
raise InvalidFormatError,
|
146
|
-
"
|
147
|
-
"sample format #{candidate_sample_format}."
|
221
|
+
"Invalid sample format: `#{candidate_format_code}`. Must be one of: #{SUPPORTED_FORMAT_CODES.inspect}"
|
148
222
|
end
|
149
223
|
end
|
150
224
|
|
151
225
|
# Internal
|
152
226
|
def validate_sample_rate(candidate_sample_rate)
|
153
|
-
unless VALID_SAMPLE_RATE_RANGE === candidate_sample_rate
|
227
|
+
unless candidate_sample_rate.is_a?(Integer) && VALID_SAMPLE_RATE_RANGE === candidate_sample_rate
|
154
228
|
raise InvalidFormatError,
|
155
|
-
"Invalid sample rate
|
229
|
+
"Invalid sample rate: `#{candidate_sample_rate}`. Must be an Integer between #{VALID_SAMPLE_RATE_RANGE.min} and #{VALID_SAMPLE_RATE_RANGE.max}"
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
# Internal
|
234
|
+
def validate_speaker_mapping(channels, candidate_speaker_mapping)
|
235
|
+
if candidate_speaker_mapping.is_a?(Array) && candidate_speaker_mapping.length == channels
|
236
|
+
speaker_mapping_without_invalid_speakers = UnvalidatedFormat::SPEAKER_POSITIONS & candidate_speaker_mapping
|
237
|
+
if speaker_mapping_without_invalid_speakers.length < channels
|
238
|
+
speaker_mapping_without_invalid_speakers += [:undefined] * (channels - speaker_mapping_without_invalid_speakers.length)
|
239
|
+
end
|
240
|
+
|
241
|
+
if speaker_mapping_without_invalid_speakers == candidate_speaker_mapping
|
242
|
+
return
|
243
|
+
end
|
156
244
|
end
|
245
|
+
|
246
|
+
raise InvalidFormatError,
|
247
|
+
"Invalid speaker_mapping: `#{candidate_speaker_mapping.inspect}`. Should be an array the same size as the number of channels, containing either :undefined or these known speakers: #{UnvalidatedFormat::SPEAKER_POSITIONS.inspect}. Each defined speaker must come before any of the ones after it in the master list, and all :undefined speakers must come after the last defined speaker."
|
157
248
|
end
|
158
249
|
end
|
159
250
|
end
|
data/lib/wavefile/reader.rb
CHANGED
@@ -27,7 +27,7 @@ module WaveFile
|
|
27
27
|
# (default: the wave file's internal format).
|
28
28
|
#
|
29
29
|
# Returns a Reader object that is ready to start reading the specified file's sample data.
|
30
|
-
# Raises Errno::ENOENT if the specified file can't be found
|
30
|
+
# Raises +Errno::ENOENT+ if the specified file can't be found
|
31
31
|
# Raises InvalidFormatError if the specified file isn't a valid wave file
|
32
32
|
def initialize(io_or_file_name, format=nil)
|
33
33
|
if io_or_file_name.is_a?(String)
|
@@ -45,7 +45,7 @@ module WaveFile
|
|
45
45
|
rescue InvalidFormatError
|
46
46
|
raise InvalidFormatError, "Does not appear to be a valid Wave file"
|
47
47
|
end
|
48
|
-
|
48
|
+
|
49
49
|
@data_chunk_reader = riff_reader.data_chunk_reader
|
50
50
|
|
51
51
|
if block_given?
|
@@ -133,16 +133,15 @@ module WaveFile
|
|
133
133
|
end
|
134
134
|
|
135
135
|
|
136
|
-
# Public: Closes the Reader.
|
137
|
-
#
|
138
|
-
#
|
136
|
+
# Public: Closes the Reader. If the Reader is already closed, does nothing. After a Reader
|
137
|
+
# is closed, no more sample data can be read from it. Note: If the Reader is constructed
|
138
|
+
# from an open IO instance (as opposed to a file name), the IO instance will _not_ be closed.
|
139
|
+
# You'll have to manually close it yourself. This is intentional, because Reader can't know
|
140
|
+
# what you may/may not want to do with the IO instance in the future.
|
139
141
|
#
|
140
142
|
# Returns nothing.
|
141
|
-
# Raises ReaderClosedError if the Reader is already closed.
|
142
143
|
def close
|
143
|
-
if @closed
|
144
|
-
raise ReaderClosedError
|
145
|
-
end
|
144
|
+
return if @closed
|
146
145
|
|
147
146
|
if @io_source == :file_name
|
148
147
|
@io.close
|
@@ -156,23 +155,26 @@ module WaveFile
|
|
156
155
|
Duration.new(total_sample_frames, @data_chunk_reader.format.sample_rate)
|
157
156
|
end
|
158
157
|
|
159
|
-
# Public: Returns
|
160
|
-
# This
|
161
|
-
# that,
|
158
|
+
# Public: Returns an object describing the sample format of the Wave file being read.
|
159
|
+
# This returns the data contained in the "fmt " chunk of the Wave file. It will not
|
160
|
+
# necessarily match the format that the samples are read out as (for that, see #format).
|
162
161
|
def native_format
|
163
162
|
@data_chunk_reader.raw_native_format
|
164
163
|
end
|
165
164
|
|
166
165
|
# Public: Returns true if this is a valid Wave file and contains sample data that is in a format
|
167
166
|
# that this class can read, and returns false if this is a valid Wave file but does not
|
168
|
-
# contain a sample format
|
167
|
+
# contain a sample format that this gem knows how to read.
|
169
168
|
def readable_format?
|
170
169
|
@data_chunk_reader.readable_format
|
171
170
|
end
|
172
171
|
|
173
|
-
# Public: Returns
|
174
|
-
#
|
175
|
-
#
|
172
|
+
# Public: Returns an object describing how sample data is being read from the Wave file.
|
173
|
+
# I.e., number of channels, bits per sample, sample format, etc. If #readable_format? is
|
174
|
+
# true, then this will be a Format object. The format the samples are read out as might
|
175
|
+
# be different from how the samples are actually stored in the file. Therefore, #format
|
176
|
+
# might not match #native_format. If #readable_format? is false, then this will return the
|
177
|
+
# same value as #native_format.
|
176
178
|
def format
|
177
179
|
@data_chunk_reader.format
|
178
180
|
end
|