wavefile 0.8.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (117) hide show
  1. checksums.yaml +5 -5
  2. data/LICENSE +1 -1
  3. data/README.markdown +33 -28
  4. data/Rakefile +2 -2
  5. data/lib/wavefile.rb +5 -5
  6. data/lib/wavefile/chunk_readers/format_chunk_reader.rb +1 -1
  7. data/lib/wavefile/duration.rb +1 -1
  8. data/lib/wavefile/format.rb +120 -29
  9. data/lib/wavefile/reader.rb +18 -16
  10. data/lib/wavefile/unvalidated_format.rb +51 -3
  11. data/lib/wavefile/writer.rb +74 -21
  12. data/test/buffer_test.rb +13 -10
  13. data/test/chunk_readers/format_chunk_reader_test.rb +40 -40
  14. data/test/fixtures/{invalid → wave/invalid}/bad_riff_header.wav +0 -0
  15. data/test/fixtures/{invalid → wave/invalid}/bad_wavefile_format.wav +0 -0
  16. data/test/fixtures/{invalid → wave/invalid}/empty.wav +0 -0
  17. data/test/fixtures/{invalid → wave/invalid}/empty_format_chunk.wav +0 -0
  18. data/test/fixtures/{invalid → wave/invalid}/incomplete_riff_header.wav +0 -0
  19. data/test/fixtures/{invalid → wave/invalid}/insufficient_format_chunk.wav +0 -0
  20. data/test/fixtures/{invalid → wave/invalid}/no_data_chunk.wav +0 -0
  21. data/test/fixtures/{invalid → wave/invalid}/no_format_chunk.wav +0 -0
  22. data/test/fixtures/{unsupported → wave/unsupported}/bad_audio_format.wav +0 -0
  23. data/test/fixtures/{unsupported → wave/unsupported}/bad_channel_count.wav +0 -0
  24. data/test/fixtures/{unsupported → wave/unsupported}/bad_sample_rate.wav +0 -0
  25. data/test/fixtures/{unsupported → wave/unsupported}/extensible_container_size_bigger_than_sample_size.wav +0 -0
  26. data/test/fixtures/wave/unsupported/extensible_unsupported_subformat_guid.wav +0 -0
  27. data/test/fixtures/{unsupported → wave/unsupported}/unsupported_audio_format.wav +0 -0
  28. data/test/fixtures/{unsupported → wave/unsupported}/unsupported_bits_per_sample.wav +0 -0
  29. data/test/fixtures/{valid → wave/valid}/no_samples.wav +0 -0
  30. data/test/fixtures/wave/valid/valid_extensible_20_pcm_16_44100_speaker_mapping_overflow.wav +0 -0
  31. data/test/fixtures/{valid → wave/valid}/valid_extensible_mono_float_32_44100.wav +0 -0
  32. data/test/fixtures/{valid → wave/valid}/valid_extensible_mono_float_64_44100.wav +0 -0
  33. data/test/fixtures/wave/valid/valid_extensible_mono_pcm_16_44100.wav +0 -0
  34. data/test/fixtures/wave/valid/valid_extensible_mono_pcm_16_44100_non_default_speaker_mapping.wav +0 -0
  35. data/test/fixtures/wave/valid/valid_extensible_mono_pcm_24_44100.wav +0 -0
  36. data/test/fixtures/wave/valid/valid_extensible_mono_pcm_32_44100.wav +0 -0
  37. data/test/fixtures/wave/valid/valid_extensible_mono_pcm_8_44100.wav +0 -0
  38. data/test/fixtures/{valid → wave/valid}/valid_extensible_stereo_float_32_44100.wav +0 -0
  39. data/test/fixtures/{valid → wave/valid}/valid_extensible_stereo_float_64_44100.wav +0 -0
  40. data/test/fixtures/wave/valid/valid_extensible_stereo_pcm_16_44100.wav +0 -0
  41. data/test/fixtures/wave/valid/valid_extensible_stereo_pcm_16_44100_center_right_speakers.wav +0 -0
  42. data/test/fixtures/wave/valid/valid_extensible_stereo_pcm_16_44100_more_speakers_than_channels.wav +0 -0
  43. data/test/fixtures/wave/valid/valid_extensible_stereo_pcm_16_44100_more_speakers_than_defined_by_spec.wav +0 -0
  44. data/test/fixtures/wave/valid/valid_extensible_stereo_pcm_16_44100_only_undefined_high_bit_speakers.wav +0 -0
  45. data/test/fixtures/wave/valid/valid_extensible_stereo_pcm_24_44100.wav +0 -0
  46. data/test/fixtures/wave/valid/valid_extensible_stereo_pcm_24_44100_incomplete_speaker_mapping.wav +0 -0
  47. data/test/fixtures/wave/valid/valid_extensible_stereo_pcm_24_44100_no_speaker_mapping.wav +0 -0
  48. data/test/fixtures/wave/valid/valid_extensible_stereo_pcm_32_44100.wav +0 -0
  49. data/test/fixtures/wave/valid/valid_extensible_stereo_pcm_8_44100.wav +0 -0
  50. data/test/fixtures/{valid → wave/valid}/valid_extensible_tri_float_32_44100.wav +0 -0
  51. data/test/fixtures/{valid → wave/valid}/valid_extensible_tri_float_64_44100.wav +0 -0
  52. data/test/fixtures/wave/valid/valid_extensible_tri_pcm_16_44100.wav +0 -0
  53. data/test/fixtures/wave/valid/valid_extensible_tri_pcm_16_44100_custom_speaker_mapping.wav +0 -0
  54. data/test/fixtures/wave/valid/valid_extensible_tri_pcm_24_44100.wav +0 -0
  55. data/test/fixtures/wave/valid/valid_extensible_tri_pcm_32_44100.wav +0 -0
  56. data/test/fixtures/wave/valid/valid_extensible_tri_pcm_8_44100.wav +0 -0
  57. data/test/fixtures/{valid → wave/valid}/valid_mono_float_32_44100.wav +0 -0
  58. data/test/fixtures/{valid → wave/valid}/valid_mono_float_64_44100.wav +0 -0
  59. data/test/fixtures/{valid → wave/valid}/valid_mono_pcm_16_44100.wav +0 -0
  60. data/test/fixtures/{valid → wave/valid}/valid_mono_pcm_16_44100_junk_chunk_with_padding_byte.wav +0 -0
  61. data/test/fixtures/wave/valid/valid_mono_pcm_16_44100_with_extension.wav +0 -0
  62. data/test/fixtures/{valid → wave/valid}/valid_mono_pcm_24_44100.wav +0 -0
  63. data/test/fixtures/{valid → wave/valid}/valid_mono_pcm_32_44100.wav +0 -0
  64. data/test/fixtures/{valid → wave/valid}/valid_mono_pcm_8_44100.wav +0 -0
  65. data/test/fixtures/{valid → wave/valid}/valid_mono_pcm_8_44100_with_padding_byte.wav +0 -0
  66. data/test/fixtures/{valid → wave/valid}/valid_stereo_float_32_44100.wav +0 -0
  67. data/test/fixtures/{valid → wave/valid}/valid_stereo_float_64_44100.wav +0 -0
  68. data/test/fixtures/{valid → wave/valid}/valid_stereo_pcm_16_44100.wav +0 -0
  69. data/test/fixtures/{valid → wave/valid}/valid_stereo_pcm_24_44100.wav +0 -0
  70. data/test/fixtures/{valid → wave/valid}/valid_stereo_pcm_32_44100.wav +0 -0
  71. data/test/fixtures/{valid → wave/valid}/valid_stereo_pcm_8_44100.wav +0 -0
  72. data/test/fixtures/{valid → wave/valid}/valid_tri_float_32_44100.wav +0 -0
  73. data/test/fixtures/{valid → wave/valid}/valid_tri_float_64_44100.wav +0 -0
  74. data/test/fixtures/{valid → wave/valid}/valid_tri_pcm_16_44100.wav +0 -0
  75. data/test/fixtures/{valid → wave/valid}/valid_tri_pcm_24_44100.wav +0 -0
  76. data/test/fixtures/{valid → wave/valid}/valid_tri_pcm_32_44100.wav +0 -0
  77. data/test/fixtures/{valid → wave/valid}/valid_tri_pcm_8_44100.wav +0 -0
  78. data/test/format_test.rb +189 -3
  79. data/test/reader_test.rb +179 -4
  80. data/test/unvalidated_format_test.rb +181 -6
  81. data/test/wavefile_io_test_helper.rb +11 -1
  82. data/test/writer_test.rb +246 -25
  83. metadata +70 -80
  84. data/test/fixtures/actual_output/total_duration_mono_float_32_44100.wav +0 -0
  85. data/test/fixtures/actual_output/total_duration_mono_float_64_44100.wav +0 -0
  86. data/test/fixtures/actual_output/total_duration_mono_pcm_16_44100.wav +0 -0
  87. data/test/fixtures/actual_output/total_duration_mono_pcm_24_44100.wav +0 -0
  88. data/test/fixtures/actual_output/total_duration_mono_pcm_32_44100.wav +0 -0
  89. data/test/fixtures/actual_output/total_duration_mono_pcm_8_44100.wav +0 -0
  90. data/test/fixtures/actual_output/total_duration_stereo_float_32_44100.wav +0 -0
  91. data/test/fixtures/actual_output/total_duration_stereo_float_64_44100.wav +0 -0
  92. data/test/fixtures/actual_output/total_duration_stereo_pcm_16_44100.wav +0 -0
  93. data/test/fixtures/actual_output/total_duration_stereo_pcm_24_44100.wav +0 -0
  94. data/test/fixtures/actual_output/total_duration_stereo_pcm_32_44100.wav +0 -0
  95. data/test/fixtures/actual_output/total_duration_stereo_pcm_8_44100.wav +0 -0
  96. data/test/fixtures/actual_output/total_duration_tri_float_32_44100.wav +0 -0
  97. data/test/fixtures/actual_output/total_duration_tri_float_64_44100.wav +0 -0
  98. data/test/fixtures/actual_output/total_duration_tri_pcm_16_44100.wav +0 -0
  99. data/test/fixtures/actual_output/total_duration_tri_pcm_24_44100.wav +0 -0
  100. data/test/fixtures/actual_output/total_duration_tri_pcm_32_44100.wav +0 -0
  101. data/test/fixtures/actual_output/total_duration_tri_pcm_8_44100.wav +0 -0
  102. data/test/fixtures/invalid/README.markdown +0 -10
  103. data/test/fixtures/unsupported/README.markdown +0 -6
  104. data/test/fixtures/unsupported/extensible_unsupported_subformat_guid.wav +0 -0
  105. data/test/fixtures/valid/README.markdown +0 -3
  106. data/test/fixtures/valid/valid_extensible_mono_pcm_16_44100.wav +0 -0
  107. data/test/fixtures/valid/valid_extensible_mono_pcm_24_44100.wav +0 -0
  108. data/test/fixtures/valid/valid_extensible_mono_pcm_32_44100.wav +0 -0
  109. data/test/fixtures/valid/valid_extensible_mono_pcm_8_44100.wav +0 -0
  110. data/test/fixtures/valid/valid_extensible_stereo_pcm_16_44100.wav +0 -0
  111. data/test/fixtures/valid/valid_extensible_stereo_pcm_24_44100.wav +0 -0
  112. data/test/fixtures/valid/valid_extensible_stereo_pcm_32_44100.wav +0 -0
  113. data/test/fixtures/valid/valid_extensible_stereo_pcm_8_44100.wav +0 -0
  114. data/test/fixtures/valid/valid_extensible_tri_pcm_16_44100.wav +0 -0
  115. data/test/fixtures/valid/valid_extensible_tri_pcm_24_44100.wav +0 -0
  116. data/test/fixtures/valid/valid_extensible_tri_pcm_32_44100.wav +0 -0
  117. data/test/fixtures/valid/valid_extensible_tri_pcm_8_44100.wav +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 21ddb98d87753898bf2893d58097b8fb53050e8e
4
- data.tar.gz: b45ed2b6c1aa0c34ac760c49d0a285bf59606856
2
+ SHA256:
3
+ metadata.gz: 345be2bed4e76c59bfb0163cc844399ff54bc7ca2643b82ba555c4bf167d5b5e
4
+ data.tar.gz: 1923c71c3af5438319425709de665acd6bf6be53f67058f7bf4b99c43d7a2493
5
5
  SHA512:
6
- metadata.gz: 899880af7dc77b33d23035e7b2a1708c2e679f69f7b66fd1a302e736dccf8e1abf65bb7b2719c606e3e93977caba9882376dbd3f45708fb0d97a2c72be28c952
7
- data.tar.gz: e44cc7f1066a21762ae8fb627043d1c26eb2bd7ab1254d81e4d7fcd9a66dc7c3937591b37b96903b1b6f6c7837e77a17360137351bf1610ab3e5feb3ca7cd691
6
+ metadata.gz: 3134a277c3c239e41e0dc60dda461223664a316e2ea16b299b61d137deee8cb545ccde99bac35566a8834571da2538aa973551d6f27b0521be9f78c0c8d88765
7
+ data.tar.gz: '069c9654d04eabb25c5125983b658ff468697de5eb551b7673be42fe6775a470899d20ba19e7e44c9fcd280377866ade72b1bf1aa672523065155e75c17c0604'
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  == WaveFile Ruby Gem
2
2
 
3
- # Copyright (c) 2009-17 Joel Strait
3
+ # Copyright (c) 2009-18 Joel Strait
4
4
  #
5
5
  # Permission is hereby granted, free of charge, to any person
6
6
  # obtaining a copy of this software and associated documentation
@@ -1,12 +1,12 @@
1
- A pure Ruby gem for reading and writing sound files in Wave format (*.wav).
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](http://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.
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 is a short example that shows how to append three separate Wave files into a single file:
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 IEEE Floating Point (32/64 bits per sample) format.
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: v0.8.1
40
+ # Current Release: v1.0.0
41
41
 
42
- Released on January 31, 2017, this version fixes an error when frozen string literals are enabled in Ruby 2.3 or higher. (At the time of this release, that means Ruby 2.3 or 2.4). The gem should now work properly when the `--enable-frozen-string-literal` Ruby option is enabled. Thanks to [@samaaron](https://github.com/samaaron) for finding and fixing this!
42
+ Released on June 10, 2018, this version has these changes:
43
43
 
44
- # Release v0.8.0
45
-
46
- Released on January 29, 2017, this version includes these changes:
47
-
48
- * Wave files using WAVEFORMATEXTENSIBLE format (format code 65534) can now be read.
49
- * Notes/Limitations
50
- * The same formats supported in "vanilla" Wave files are supported when reading WAVEFORMATEXTENSIBLE files. That is, PCM (8/16/24/32 bits per sample) or IEEE_FLOAT (32/64 bits per sample).
51
- * The channel speaker mapping field is not exposed.
52
- * The number of valid bits per sample must match the sample container size. For example, if a file has a sample container size of 24 bits and each sample is 24 bits, then it can be read. If the container size is 32 bits and each sample is 24 bits, it _can't_ be read.
53
- * Writing files using WAVEFORMATEXTENSIBLE format is not supported - all files will be written as a "vanilla" file regardless of the number of channels or sample format.
54
- * `Reader` and `Writer` can now be constructed using an open `IO` instance, to allow reading/writing using an arbitrary `IO`-like object (`File`, `StringIO`, etc). Previously, they could only be constructed from a file name (given by a String). Thanks to [@taf2](https://github.com/taf2) for suggesting this feature and providing an example pull request.
55
- * The buffer size in `Reader.each_buffer()` is now optional. If not given, a default buffer size will be used.
56
- * Two `Duration` objects will now evaluate to equal if they represent the same amount of time, due to an overridden definition of `==`. Thanks to [Christopher Smith](https://github.com/chrylis) for suggesting this improvement.
57
- * A `ReaderClosedError` is now raised (instead of `IOError`) when attempting to read from a closed `Reader` instance. However, `ReaderClosedError` extends `IOError`.
58
- * A `WriterClosedError` is now raised (instead of `IOError`) when attempting to read from a closed `Writer` instance. However, `ReaderClosedError` extends `IOError`.
59
- * **Backwards Incompatible Changes**
60
- * `Reader.file_name` and `Writer.file_name` have been removed. When a `Reader` or `Writer` instance is constructed from an `IO` instance, this field wouldn't necessarily have a sensible value. Since I don't know of an obvious use-case for these fields, going ahead and removing them altogether.
61
- * The long deprecated ability to provide the sample format for a `Format` instance as an integer (implying PCM format) has been removed. For example, this is no longer valid: `Format.new(:mono, 16, 44100)`. Instead, use `Format.new(:mono, :pcm_16, 44100)`.
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.0, 2.3.3, 2.2.6, 2.1.10, 2.0, 1.9.3
68
+ * MRI 2.5.1, 2.4.4, 2.3.7, 2.2.10, 2.1.10, 2.0
69
69
 
70
- 1.9.3 is the minimum supported Ruby version.
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. It is written in pure Ruby, and is entirely self-contained.
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
- If you want to change one of the fixture `*.wav` files under `/test/fixtures`, edit the appropriate `*.yml` file defined in `/tools`, and then run this:
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("tools/#{subfolder}/*.yml")
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
@@ -7,10 +7,10 @@ require 'wavefile/unvalidated_format'
7
7
  require 'wavefile/writer'
8
8
 
9
9
  module WaveFile
10
- VERSION = "0.8.1"
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[:extension_size] == 22
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
@@ -78,7 +78,7 @@ module WaveFile
78
78
  @seconds == other_duration.seconds &&
79
79
  @milliseconds == other_duration.milliseconds
80
80
  end
81
-
81
+
82
82
  # Public
83
83
  attr_reader :sample_frame_count
84
84
 
@@ -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 trying to read from a file that is either not a wave file,
8
- # or that is not valid according to the wave file spec.
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 :mono (equivalent to 1) or
27
- # :stereo (equivalent to 2).
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 :pcm_8, :pcm_16, :pcm_24, :pcm_32, :float_32,
31
- # :float_64, and :float (equivalent to :float_32)
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
- def initialize(channels, format_code, sample_rate)
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
- sample_format, bits_per_sample = normalize_format_code(format_code)
74
+
44
75
  validate_channels(channels)
45
- validate_sample_format(sample_format)
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
- SUPPORTED_SAMPLE_FORMATS = [:pcm, :float] # :nodoc:
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 validate_sample_format(candidate_sample_format)
127
- unless SUPPORTED_SAMPLE_FORMATS.include? candidate_sample_format
128
- raise InvalidFormatError,
129
- "Sample format of #{candidate_sample_format} is unsupported. " +
130
- "Only #{SUPPORTED_SAMPLE_FORMATS.inspect} are supported."
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. Must be between #{VALID_CHANNEL_RANGE.min} and #{VALID_CHANNEL_RANGE.max}."
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 validate_bits_per_sample(candidate_sample_format, candidate_bits_per_sample)
144
- unless SUPPORTED_BITS_PER_SAMPLE[candidate_sample_format].include? candidate_bits_per_sample
218
+ def validate_format_code(candidate_format_code)
219
+ unless SUPPORTED_FORMAT_CODES.include? candidate_format_code
145
220
  raise InvalidFormatError,
146
- "Bits per sample of #{candidate_bits_per_sample} is unsupported for " +
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. Must be between #{VALID_SAMPLE_RATE_RANGE.min} and #{VALID_SAMPLE_RATE_RANGE.max}"
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
@@ -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. After a Reader is closed, no more sample data can be read from it.
137
- # Note: If the Reader is constructed from an open IO instance (as opposed to a file name),
138
- # the IO instance will _not_ be closed. You'll have to manually close it yourself.
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 a Format object describing the sample format of the Wave file being read.
160
- # This is not necessarily the format that the sample data will be read as - to determine
161
- # that, use #format.
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 supported by this class.
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 a Format object describing how sample data is being read from the Wave file (number of
174
- # channels, sample format and bits per sample, etc). Note that this might be different from the
175
- # underlying format of the Wave file on disk.
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