wavefile 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +2 -2
  3. data/README.markdown +67 -47
  4. data/Rakefile +23 -0
  5. data/lib/wavefile.rb +4 -2
  6. data/lib/wavefile/buffer.rb +40 -25
  7. data/lib/wavefile/chunk_readers.rb +7 -1
  8. data/lib/wavefile/chunk_readers/base_chunk_reader.rb +10 -0
  9. data/lib/wavefile/chunk_readers/data_chunk_reader.rb +77 -0
  10. data/lib/wavefile/chunk_readers/format_chunk_reader.rb +59 -0
  11. data/lib/wavefile/chunk_readers/generic_chunk_reader.rb +15 -0
  12. data/lib/wavefile/chunk_readers/riff_chunk_reader.rb +19 -0
  13. data/lib/wavefile/chunk_readers/riff_reader.rb +67 -0
  14. data/lib/wavefile/duration.rb +47 -12
  15. data/lib/wavefile/format.rb +44 -23
  16. data/lib/wavefile/reader.rb +101 -111
  17. data/lib/wavefile/unvalidated_format.rb +36 -6
  18. data/lib/wavefile/writer.rb +138 -40
  19. data/test/buffer_test.rb +21 -17
  20. data/test/chunk_readers/format_chunk_reader_test.rb +130 -0
  21. data/test/duration_test.rb +42 -1
  22. data/test/fixtures/actual_output/total_duration_mono_float_32_44100.wav +0 -0
  23. data/test/fixtures/actual_output/total_duration_mono_float_64_44100.wav +0 -0
  24. data/test/fixtures/actual_output/total_duration_mono_pcm_16_44100.wav +0 -0
  25. data/test/fixtures/actual_output/total_duration_mono_pcm_24_44100.wav +0 -0
  26. data/test/fixtures/actual_output/total_duration_mono_pcm_32_44100.wav +0 -0
  27. data/test/fixtures/actual_output/total_duration_mono_pcm_8_44100.wav +0 -0
  28. data/test/fixtures/actual_output/total_duration_stereo_float_32_44100.wav +0 -0
  29. data/test/fixtures/actual_output/total_duration_stereo_float_64_44100.wav +0 -0
  30. data/test/fixtures/actual_output/total_duration_stereo_pcm_16_44100.wav +0 -0
  31. data/test/fixtures/actual_output/total_duration_stereo_pcm_24_44100.wav +0 -0
  32. data/test/fixtures/actual_output/total_duration_stereo_pcm_32_44100.wav +0 -0
  33. data/test/fixtures/actual_output/total_duration_stereo_pcm_8_44100.wav +0 -0
  34. data/test/fixtures/actual_output/total_duration_tri_float_32_44100.wav +0 -0
  35. data/test/fixtures/actual_output/total_duration_tri_float_64_44100.wav +0 -0
  36. data/test/fixtures/actual_output/total_duration_tri_pcm_16_44100.wav +0 -0
  37. data/test/fixtures/actual_output/total_duration_tri_pcm_24_44100.wav +0 -0
  38. data/test/fixtures/actual_output/total_duration_tri_pcm_32_44100.wav +0 -0
  39. data/test/fixtures/actual_output/total_duration_tri_pcm_8_44100.wav +0 -0
  40. data/test/fixtures/unsupported/README.markdown +1 -1
  41. data/test/fixtures/unsupported/bad_channel_count.wav +0 -0
  42. data/test/fixtures/unsupported/extensible_container_size_bigger_than_sample_size.wav +0 -0
  43. data/test/fixtures/unsupported/extensible_unsupported_subformat_guid.wav +0 -0
  44. data/test/fixtures/valid/valid_extensible_mono_float_32_44100.wav +0 -0
  45. data/test/fixtures/valid/valid_extensible_mono_float_64_44100.wav +0 -0
  46. data/test/fixtures/valid/valid_extensible_mono_pcm_16_44100.wav +0 -0
  47. data/test/fixtures/valid/valid_extensible_mono_pcm_24_44100.wav +0 -0
  48. data/test/fixtures/valid/valid_extensible_mono_pcm_32_44100.wav +0 -0
  49. data/test/fixtures/valid/valid_extensible_mono_pcm_8_44100.wav +0 -0
  50. data/test/fixtures/valid/valid_extensible_stereo_float_32_44100.wav +0 -0
  51. data/test/fixtures/valid/valid_extensible_stereo_float_64_44100.wav +0 -0
  52. data/test/fixtures/valid/valid_extensible_stereo_pcm_16_44100.wav +0 -0
  53. data/test/fixtures/valid/valid_extensible_stereo_pcm_24_44100.wav +0 -0
  54. data/test/fixtures/valid/valid_extensible_stereo_pcm_32_44100.wav +0 -0
  55. data/test/fixtures/valid/valid_extensible_stereo_pcm_8_44100.wav +0 -0
  56. data/test/fixtures/valid/valid_extensible_tri_float_32_44100.wav +0 -0
  57. data/test/fixtures/valid/valid_extensible_tri_float_64_44100.wav +0 -0
  58. data/test/fixtures/valid/valid_extensible_tri_pcm_16_44100.wav +0 -0
  59. data/test/fixtures/valid/valid_extensible_tri_pcm_24_44100.wav +0 -0
  60. data/test/fixtures/valid/valid_extensible_tri_pcm_32_44100.wav +0 -0
  61. data/test/fixtures/valid/valid_extensible_tri_pcm_8_44100.wav +0 -0
  62. data/test/format_test.rb +22 -48
  63. data/test/reader_test.rb +188 -93
  64. data/test/unvalidated_format_test.rb +130 -4
  65. data/test/wavefile_io_test_helper.rb +6 -4
  66. data/test/writer_test.rb +118 -95
  67. metadata +47 -4
  68. data/lib/wavefile/chunk_readers/header_reader.rb +0 -163
  69. data/test/fixtures/actual_output/no_samples.wav +0 -0
@@ -0,0 +1,59 @@
1
+ module WaveFile
2
+ module ChunkReaders
3
+ # Internal
4
+ class FormatChunkReader < BaseChunkReader # :nodoc:
5
+ def initialize(io, chunk_size)
6
+ @io = io
7
+ @chunk_size = chunk_size
8
+ end
9
+
10
+ def read
11
+ if @chunk_size < MINIMUM_CHUNK_SIZE
12
+ raise_error InvalidFormatError, "The format chunk is incomplete."
13
+ end
14
+
15
+ raw_bytes = read_chunk_body(CHUNK_IDS[:format], @chunk_size)
16
+
17
+ format_chunk = {}
18
+ format_chunk[:audio_format],
19
+ format_chunk[:channels],
20
+ format_chunk[:sample_rate],
21
+ format_chunk[:byte_rate],
22
+ format_chunk[:block_align],
23
+ format_chunk[:bits_per_sample] = raw_bytes.slice!(0...MINIMUM_CHUNK_SIZE).unpack("vvVVvv")
24
+
25
+ if @chunk_size > MINIMUM_CHUNK_SIZE
26
+ format_chunk[:extension_size] = raw_bytes.slice!(0...2).unpack(UNSIGNED_INT_16).first
27
+
28
+ if format_chunk[:extension_size] == nil
29
+ raise_error InvalidFormatError, "The format chunk is missing an expected extension."
30
+ end
31
+
32
+ if format_chunk[:extension_size] != raw_bytes.length
33
+ raise_error InvalidFormatError, "The format chunk extension is shorter than expected."
34
+ end
35
+
36
+ if format_chunk[:extension_size] == 22
37
+ format_chunk[:valid_bits_per_sample] = raw_bytes.slice!(0...2).unpack(UNSIGNED_INT_16).first
38
+ format_chunk[:speaker_mapping] = raw_bytes.slice!(0...4).unpack(UNSIGNED_INT_32).first
39
+ format_chunk[:sub_audio_format_guid] = raw_bytes
40
+ end
41
+ end
42
+
43
+ UnvalidatedFormat.new(format_chunk)
44
+ end
45
+
46
+ private
47
+
48
+ MINIMUM_CHUNK_SIZE = 16
49
+
50
+ def read_chunk_body(chunk_id, chunk_size)
51
+ begin
52
+ return @io.sysread(chunk_size)
53
+ rescue EOFError
54
+ raise_error InvalidFormatError, "The #{chunk_id} chunk has incomplete data."
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,15 @@
1
+ module WaveFile
2
+ module ChunkReaders
3
+ # Internal
4
+ class GenericChunkReader < BaseChunkReader # :nodoc:
5
+ def initialize(io, chunk_size)
6
+ @io = io
7
+ @chunk_size = chunk_size
8
+ end
9
+
10
+ def read
11
+ @io.sysread(@chunk_size)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,19 @@
1
+ module WaveFile
2
+ module ChunkReaders
3
+ # Internal
4
+ class RiffChunkReader < BaseChunkReader # :nodoc:
5
+ def initialize(io, chunk_size)
6
+ @io = io
7
+ @chunk_size = chunk_size
8
+ end
9
+
10
+ def read
11
+ riff_format = @io.sysread(4)
12
+
13
+ unless riff_format == WAVEFILE_FORMAT_CODE
14
+ raise_error InvalidFormatError, "Expected RIFF format of '#{WAVEFILE_FORMAT_CODE}', but was '#{riff_format}'"
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,67 @@
1
+ module WaveFile
2
+ module ChunkReaders
3
+ # Internal: Used to read the RIFF chunks in a wave file up until the data chunk. Thus it
4
+ # can be used to open a wave file and "queue it up" to the start of the actual sample data,
5
+ # as well as extract information out of pre-data chunks, such as the format chunk.
6
+ class RiffReader # :nodoc:
7
+ def initialize(io, format=nil)
8
+ @io = io
9
+
10
+ read_until_data_chunk(format)
11
+ end
12
+
13
+ attr_reader :native_format, :data_chunk_reader
14
+
15
+ private
16
+
17
+ def read_until_data_chunk(format)
18
+ begin
19
+ chunk_id, chunk_size = read_chunk_header
20
+ unless chunk_id == CHUNK_IDS[:riff]
21
+ raise_error InvalidFormatError, "Expected chunk ID '#{CHUNK_IDS[:riff]}', but was '#{chunk_id}'"
22
+ end
23
+ RiffChunkReader.new(@io, chunk_size).read
24
+
25
+ chunk_id, chunk_size = read_chunk_header
26
+ while chunk_id != CHUNK_IDS[:data]
27
+ if chunk_id == CHUNK_IDS[:format]
28
+ @native_format = FormatChunkReader.new(@io, chunk_size).read
29
+ else
30
+ # Other chunk types besides the format chunk are ignored. This may change in the future.
31
+ GenericChunkReader.new(@io, chunk_size).read
32
+ end
33
+
34
+ # The RIFF specification requires that each chunk be aligned to an even number of bytes,
35
+ # even if the byte count is an odd number.
36
+ #
37
+ # See http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/Docs/riffmci.pdf, page 11.
38
+ if chunk_size.odd?
39
+ @io.sysread(1)
40
+ end
41
+
42
+ chunk_id, chunk_size = read_chunk_header
43
+ end
44
+ rescue EOFError
45
+ raise_error InvalidFormatError, "It doesn't have a data chunk."
46
+ end
47
+
48
+ if @native_format == nil
49
+ raise_error InvalidFormatError, "The format chunk is either missing, or it comes after the data chunk."
50
+ end
51
+
52
+ @data_chunk_reader = DataChunkReader.new(@io, chunk_size, @native_format, format)
53
+ end
54
+
55
+ def read_chunk_header
56
+ chunk_id = @io.sysread(4)
57
+ chunk_size = @io.sysread(4).unpack(UNSIGNED_INT_32).first || 0
58
+
59
+ return chunk_id, chunk_size
60
+ end
61
+
62
+ def raise_error(exception_class, message)
63
+ raise exception_class, "Not a supported wave file. #{message}"
64
+ end
65
+ end
66
+ end
67
+ end
@@ -1,18 +1,18 @@
1
1
  module WaveFile
2
- # Calculates playback time given the number of sample frames and the sample rate. For
3
- # example, you can use this to calculate how long a given Wave file is.
2
+ # Public: Calculates playback time given the number of sample frames and the sample rate.
3
+ # For example, you can use this to calculate how long a given Wave file is.
4
4
  #
5
- # The hours, minutes, seconds, and milliseconds fields return values like you would
6
- # see on a stopwatch, and not the total amount of time in that unit. For example, a
7
- # stopwatch running for exactly 2 hours would show something like "2:00:00.000".
8
- # Accordingly, if the given sample frame count and sample rate add up to exactly
5
+ # The hours, minutes, seconds, and milliseconds fields return values like you would
6
+ # see on a stopwatch, and not the total amount of time in that unit. For example, a
7
+ # stopwatch running for exactly 2 hours would show something like "2:00:00.000".
8
+ # Accordingly, if the given sample frame count and sample rate add up to exactly
9
9
  # 2 hours, then hours will be 2, and minutes, seconds, and milliseconds will all be 0.
10
10
  #
11
11
  # This class is immutable - once a new Duration is constructed, it can't be modified.
12
12
  class Duration
13
- # Constructs a new immutable Duration.
13
+ # Public: Constructs a new immutable Duration.
14
14
  #
15
- # sample_frame_count - The number of sample frames, i.e. the number
15
+ # sample_frame_count - The number of sample frames, i.e. the number
16
16
  # samples in each channel.
17
17
  # sample_rate - The number of samples per second, such as 44100
18
18
  #
@@ -24,9 +24,11 @@ module WaveFile
24
24
  # duration.seconds # => 10
25
25
  # duration.milliseconds # => 294
26
26
  #
27
- # Note that the hours, minutes, seconds, and milliseconds fields do not return
28
- # the total of the respective unit in the entire duration. For example, if a
29
- # duration is exactly 2 hours, then minutes will be 0, not 120.
27
+ # The hours, minutes, seconds, and milliseconds fields return values like you would
28
+ # see on a stopwatch, and not the total amount of time in that unit. For example, a
29
+ # stopwatch running for exactly 2 hours would show something like "2:00:00.000".
30
+ # Accordingly, if the given sample frame count and sample rate add up to exactly
31
+ # 2 hours, then hours will be 2, and minutes, seconds, and milliseconds will all be 0.
30
32
  def initialize(sample_frame_count, sample_rate)
31
33
  @sample_frame_count = sample_frame_count
32
34
  @sample_rate = sample_rate
@@ -54,12 +56,45 @@ module WaveFile
54
56
 
55
57
  @milliseconds = (sample_frame_count / sample_frames_per_millisecond).floor
56
58
  end
59
+
60
+ # Public: Returns true if this Duration represents that same amount of time as
61
+ # other_duration.
62
+ #
63
+ # Two Duration instances will evaluate as == if they correspond
64
+ # to the same "stopwatch time". This means that two Durations constructed
65
+ # from a different number of sample frames or different sample rates can be
66
+ # considered equal if they correspond to the same amount
67
+ # of time. For example, a Duration from 44,100 sample frames
68
+ # at 44,100 samples/sec will be considered equal to a Duration
69
+ # from 22,050 sample frames at 22,050 samples/sec, because
70
+ # both correspond to 1 second of audio.
71
+ #
72
+ # Since the finest resolution of a duration is 1 millisecond,
73
+ # two Durations that represent different amounts of time but
74
+ # differ by less than 1 millisecond will be considered equal.
75
+ def ==(other_duration)
76
+ @hours == other_duration.hours &&
77
+ @minutes == other_duration.minutes &&
78
+ @seconds == other_duration.seconds &&
79
+ @milliseconds == other_duration.milliseconds
80
+ end
57
81
 
82
+ # Public
58
83
  attr_reader :sample_frame_count
59
- attr_reader :sample_rate
84
+
85
+ # Public
86
+ attr_reader :sample_rate
87
+
88
+ # Public
60
89
  attr_reader :hours
90
+
91
+ # Public
61
92
  attr_reader :minutes
93
+
94
+ # Public
62
95
  attr_reader :seconds
96
+
97
+ # Public
63
98
  attr_reader :milliseconds
64
99
  end
65
100
  end
@@ -1,21 +1,34 @@
1
1
  module WaveFile
2
- # Represents information about the data format for a Wave file, such as number of
3
- # channels, bits per sample, sample rate, and so forth. A Format instance is used
4
- # by Reader to indicate what format to read samples out as, and by Writer to
2
+ # Public: Error that is raised when a file is not in a format supported by this Gem.
3
+ # For example, because it's a valid Wave file whose format is not supported by
4
+ # this Gem. Or, because it's a not a valid Wave file period.
5
+ class FormatError < StandardError; end
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.
9
+ class InvalidFormatError < FormatError; end
10
+
11
+ # Public: Error that is raised when trying to read from a valid wave file that has its sample data
12
+ # stored in a format that Reader doesn't understand.
13
+ class UnsupportedFormatError < FormatError; end
14
+
15
+ # Public: Represents information about the data format for a Wave file, such as number of
16
+ # channels, bits per sample, sample rate, and so forth. A Format instance is used
17
+ # by Reader to indicate what format to read samples out as, and by Writer to
5
18
  # indicate what format to write samples as.
6
19
  #
7
20
  # This class is immutable - once a new Format is constructed, it can't be modified.
8
21
  class Format
9
22
 
10
- # Constructs a new immutable Format.
23
+ # Public: Constructs a new immutable Format.
11
24
  #
12
- # channels - The number of channels in the format. Can either be a Fixnum
13
- # (e.g. 1, 2, 3) or the symbols :mono (equivalent to 1) or
25
+ # 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
14
27
  # :stereo (equivalent to 2).
15
- # format_code - A symbol indicating the format of each sample. Consists of
16
- # two parts: a format code, and the bits per sample. The valid
17
- # values are :pcm_8, :pcm_16, :pcm_32, :float_32, :float_64,
18
- # and :float (equivalent to :float_32)
28
+ # format_code - A symbol indicating the format of each sample. Consists of
29
+ # 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)
19
32
  # sample_rate - The number of samples per second, such as 44100
20
33
  #
21
34
  # Examples
@@ -24,7 +37,7 @@ module WaveFile
24
37
  # format = Format.new(:mono, :pcm_16, 44100) # Equivalent to above
25
38
  #
26
39
  # format = Format.new(:stereo, :float_32, 44100)
27
- # format = Format.new(:stereo, :float, 44100)
40
+ # format = Format.new(:stereo, :float, 44100) # Equivalent to above
28
41
  def initialize(channels, format_code, sample_rate)
29
42
  channels = normalize_channels(channels)
30
43
  sample_format, bits_per_sample = normalize_format_code(format_code)
@@ -41,49 +54,54 @@ module WaveFile
41
54
  @byte_rate = @block_align * @sample_rate
42
55
  end
43
56
 
44
- # Returns true if the format has 1 channel, false otherwise.
57
+ # Public: Returns true if the format has 1 channel, false otherwise.
45
58
  def mono?
46
59
  @channels == 1
47
60
  end
48
61
 
49
- # Returns true if the format has 2 channels, false otherwise.
62
+ # Public: Returns true if the format has 2 channels, false otherwise.
50
63
  def stereo?
51
64
  @channels == 2
52
65
  end
53
66
 
54
- # Returns the number of channels, such as 1 or 2. This will always return a
55
- # Fixnum, even if the number of channels is specified with a symbol (e.g. :mono)
67
+ # Public: Returns the number of channels, such as 1 or 2. This will always return a
68
+ # Integer, even if the number of channels is specified with a symbol (e.g. :mono)
56
69
  # in the constructor.
57
70
  attr_reader :channels
58
71
 
59
- # Returns a symbol indicating the sample format, such as :pcm or :float
72
+ # Public: Returns a symbol indicating the sample format, such as :pcm or :float
60
73
  attr_reader :sample_format
61
74
 
62
- # Returns the number of bits per sample, such as 8, 16, 24, 32, or 64.
75
+ # Public: Returns the number of bits per sample, such as 8, 16, 24, 32, or 64.
63
76
  attr_reader :bits_per_sample
64
77
 
65
- # Returns the number of samples per second, such as 44100.
78
+ # Public: Returns the number of samples per second, such as 44100.
66
79
  attr_reader :sample_rate
67
80
 
68
- # Returns the number of bytes in each sample frame. For example, in a 16-bit stereo file,
81
+ # Public: Returns the number of bytes in each sample frame. For example, in a 16-bit stereo file,
69
82
  # this will be 4 (2 bytes for each 16-bit sample, times 2 channels).
70
83
  attr_reader :block_align
71
84
 
72
- # Returns the number of bytes contained in 1 second of sample data.
85
+ # Public: Returns the number of bytes contained in 1 second of sample data.
73
86
  # Is equivalent to block_align * sample_rate.
74
87
  attr_reader :byte_rate
75
88
 
76
89
  private
77
90
 
91
+ # Internal
78
92
  VALID_CHANNEL_RANGE = 1..65535 # :nodoc:
93
+ # Internal
79
94
  VALID_SAMPLE_RATE_RANGE = 1..4_294_967_296 # :nodoc:
80
95
 
96
+ # Internal
81
97
  SUPPORTED_SAMPLE_FORMATS = [:pcm, :float] # :nodoc:
98
+ # Internal
82
99
  SUPPORTED_BITS_PER_SAMPLE = {
83
100
  :pcm => [8, 16, 24, 32],
84
101
  :float => [32, 64],
85
102
  } # :nodoc:
86
103
 
104
+ # Internal
87
105
  def normalize_channels(channels)
88
106
  if channels == :mono
89
107
  return 1
@@ -94,10 +112,9 @@ module WaveFile
94
112
  end
95
113
  end
96
114
 
115
+ # Internal
97
116
  def normalize_format_code(format_code)
98
- if SUPPORTED_BITS_PER_SAMPLE[:pcm].include? format_code
99
- [:pcm, format_code]
100
- elsif format_code == :float
117
+ if format_code == :float
101
118
  [:float, 32]
102
119
  else
103
120
  sample_format, bits_per_sample = format_code.to_s.split("_")
@@ -105,6 +122,7 @@ module WaveFile
105
122
  end
106
123
  end
107
124
 
125
+ # Internal
108
126
  def validate_sample_format(candidate_sample_format)
109
127
  unless SUPPORTED_SAMPLE_FORMATS.include? candidate_sample_format
110
128
  raise InvalidFormatError,
@@ -113,6 +131,7 @@ module WaveFile
113
131
  end
114
132
  end
115
133
 
134
+ # Internal
116
135
  def validate_channels(candidate_channels)
117
136
  unless VALID_CHANNEL_RANGE === candidate_channels
118
137
  raise InvalidFormatError,
@@ -120,6 +139,7 @@ module WaveFile
120
139
  end
121
140
  end
122
141
 
142
+ # Internal
123
143
  def validate_bits_per_sample(candidate_sample_format, candidate_bits_per_sample)
124
144
  unless SUPPORTED_BITS_PER_SAMPLE[candidate_sample_format].include? candidate_bits_per_sample
125
145
  raise InvalidFormatError,
@@ -128,6 +148,7 @@ module WaveFile
128
148
  end
129
149
  end
130
150
 
151
+ # Internal
131
152
  def validate_sample_rate(candidate_sample_rate)
132
153
  unless VALID_SAMPLE_RATE_RANGE === candidate_sample_rate
133
154
  raise InvalidFormatError,
@@ -1,22 +1,11 @@
1
1
  module WaveFile
2
- # Error that is raised when a file is not in a format supported by this Gem,
3
- # because it's a valid Wave file whose format is not supported by this Gem,
4
- # because it's a not a valid Wave file period, etc.
5
- class FormatError < StandardError; end
2
+ # Public: Error that is raised when trying to read from a Reader instance that has been closed.
3
+ class ReaderClosedError < IOError; end
6
4
 
7
- # 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.
9
- class InvalidFormatError < FormatError; end
10
-
11
- # Error that is raised when trying to read from a valid wave file that has its sample data
12
- # stored in a format that Reader doesn't understand.
13
- class UnsupportedFormatError < FormatError; end
14
-
15
-
16
- # Provides the ability to read sample data out of a wave file, as well as query a
17
- # wave file about its metadata (e.g. number of channels, sample rate, etc).
5
+ # Public: Provides the ability to read sample data out of a wave file, as well as query
6
+ # a wave file about its metadata (e.g. number of channels, sample rate, etc).
18
7
  #
19
- # When constructing a Reader a block can be given. All data should be read inside this
8
+ # When constructing a Reader a block can be given. All data should be read inside this
20
9
  # block, and when the block exits the Reader will automatically be closed.
21
10
  #
22
11
  # Reader.new("my_file.wav") do |reader|
@@ -29,43 +18,35 @@ module WaveFile
29
18
  # # Read sample data here
30
19
  # reader.close
31
20
  class Reader
32
- # Returns a Reader object that is ready to start reading the specified file's sample data.
21
+ # Public: Returns a Reader object that is ready to start reading the specified file's
22
+ # sample data.
33
23
  #
34
- # file_name - The name of the wave file to read from.
35
- # format - The format that read sample data should be returned in
24
+ # io_or_file_name - The name of the wave file to read from,
25
+ # or an open IO object to read from.
26
+ # format - The format that read sample data should be returned in
36
27
  # (default: the wave file's internal format).
37
28
  #
38
29
  # Returns a Reader object that is ready to start reading the specified file's sample data.
39
30
  # Raises Errno::ENOENT if the specified file can't be found
40
31
  # Raises InvalidFormatError if the specified file isn't a valid wave file
41
- def initialize(file_name, format=nil)
42
- @file_name = file_name
43
- @file = File.open(file_name, "rb")
44
-
45
- begin
46
- @riff_reader = ChunkReaders::RiffReader.new(@file, @file_name)
47
- rescue InvalidFormatError
48
- raise InvalidFormatError, "'#{@file_name}' does not appear to be a valid Wave file"
32
+ def initialize(io_or_file_name, format=nil)
33
+ if io_or_file_name.is_a?(String)
34
+ @io = File.open(io_or_file_name, "rb")
35
+ @io_source = :file_name
36
+ else
37
+ @io = io_or_file_name
38
+ @io_source = :io
49
39
  end
50
-
51
- @raw_native_format = @riff_reader.native_format
52
- @total_sample_frames = @riff_reader.data_chunk_reader.sample_frame_count
53
- @current_sample_frame = 0
54
40
 
55
- native_sample_format = "#{FORMAT_CODES.invert[native_format.audio_format]}_#{native_format.bits_per_sample}".to_sym
41
+ @closed = false
56
42
 
57
- @readable_format = true
58
43
  begin
59
- @native_format = Format.new(@raw_native_format.channels,
60
- native_sample_format,
61
- @raw_native_format.sample_rate)
62
- @pack_code = PACK_CODES[@native_format.sample_format][@native_format.bits_per_sample]
63
- rescue FormatError
64
- @readable_format = false
65
- @pack_code = nil
44
+ riff_reader = ChunkReaders::RiffReader.new(@io, format)
45
+ rescue InvalidFormatError
46
+ raise InvalidFormatError, "Does not appear to be a valid Wave file"
66
47
  end
67
-
68
- @format = (format == nil) ? (@native_format || @raw_native_format) : format
48
+
49
+ @data_chunk_reader = riff_reader.data_chunk_reader
69
50
 
70
51
  if block_given?
71
52
  begin
@@ -77,21 +58,45 @@ module WaveFile
77
58
  end
78
59
 
79
60
 
80
- # Reads sample data of the into successive Buffers of the specified size, until there is no more
81
- # sample data to be read. When all sample data has been read, the Reader is automatically closed.
82
- # Each Buffer is passed to the given block.
61
+ # Public: Reads sample data of the into successive Buffers of the specified size, until there is
62
+ # no more sample data to be read. When all sample data has been read, the Reader is automatically
63
+ # closed. Each Buffer is passed to the given block.
83
64
  #
84
- # Note that sample_frame_count indicates the number of sample frames to read, not number of samples.
85
- # A sample frame include one sample for each channel. For example, if sample_frame_count is 1024, then
86
- # for a stereo file 1024 samples will be read from the left channel, and 1024 samples will be read from
65
+ # If the Reader is constructed from an open IO, the IO is NOT closed after all sample data is
66
+ # read. However, the Reader will be closed and any attempt to continue to read from it will
67
+ # result in an error.
68
+ #
69
+ # Note that sample_frame_count indicates the number of sample frames to read, not number of samples.
70
+ # A sample frame include one sample for each channel. For example, if sample_frame_count is 1024, then
71
+ # for a stereo file 1024 samples will be read from the left channel, and 1024 samples will be read from
87
72
  # the right channel.
88
73
  #
89
- # sample_frame_count - The number of sample frames to read into each Buffer from each channel. The number
90
- # of sample frames read into the final Buffer could be less than this size, if there
74
+ # sample_frame_count - The number of sample frames to read into each Buffer from each channel. The number
75
+ # of sample frames read into the final Buffer could be less than this size, if there
91
76
  # are not enough remaining.
92
77
  #
93
- # Returns nothing.
94
- def each_buffer(sample_frame_count)
78
+ # Examples
79
+ #
80
+ # # sample_frame_count not given, so default buffer size
81
+ # Reader.new("my_file.wav").each_buffer do |buffer|
82
+ # puts "#{buffer.samples.length} sample frames read"
83
+ # end
84
+ #
85
+ # # Specific sample_frame_count given for each buffer
86
+ # Reader.new("my_file.wav").each_buffer(1024) do |buffer|
87
+ # puts "#{buffer.samples.length} sample frames read"
88
+ # end
89
+ #
90
+ # # Reading each buffer from an externally created IO
91
+ # file = File.open("my_file.wav", "rb")
92
+ # Reader.new(file).each_buffer do |buffer|
93
+ # puts "#{buffer.samples.length} sample frames read"
94
+ # end
95
+ # # Although Reader is closed, file still needs to be manually closed
96
+ # file.close
97
+ #
98
+ # Returns nothing. Has side effect of closing the Reader.
99
+ def each_buffer(sample_frame_count=4096)
95
100
  begin
96
101
  while true do
97
102
  yield(read(sample_frame_count))
@@ -102,104 +107,89 @@ module WaveFile
102
107
  end
103
108
 
104
109
 
105
- # Reads the specified number of sample frames from the wave file into a Buffer. Note that the Buffer will have
106
- # at most sample_frame_count sample frames, but could have less if the file doesn't have enough remaining.
110
+ # Public: Reads the specified number of sample frames from the wave file into a Buffer. Note that the Buffer
111
+ # will have at most sample_frame_count sample frames, but could have less if the file doesn't have enough
112
+ # remaining.
107
113
  #
108
- # sample_frame_count - The number of sample frames to read. Note that each sample frame includes a sample for
114
+ # sample_frame_count - The number of sample frames to read. Note that each sample frame includes a sample for
109
115
  # each channel.
110
116
  #
111
117
  # Returns a Buffer containing sample_frame_count sample frames
112
118
  # Raises UnsupportedFormatError if file is in a format that can't be read by this gem
119
+ # Raises ReaderClosedError if the Writer has been closed.
113
120
  # Raises EOFError if no samples could be read due to reaching the end of the file
114
121
  def read(sample_frame_count)
115
- raise UnsupportedFormatError unless @readable_format
116
-
117
- if @current_sample_frame >= @total_sample_frames
118
- #FIXME: Do something different here, because the end of the file has not actually necessarily been reached
119
- raise EOFError
120
- elsif sample_frame_count > sample_frames_remaining
121
- sample_frame_count = sample_frames_remaining
122
- end
123
-
124
- samples = @file.sysread(sample_frame_count * @native_format.block_align).unpack(@pack_code)
125
- @current_sample_frame += sample_frame_count
126
-
127
- if @native_format.bits_per_sample == 24
128
- # Since the sample data is little endian, the 3 bytes will go from least->most significant
129
- samples = samples.each_slice(3).map {|least_significant_byte, middle_byte, most_significant_byte|
130
- # Convert the byte read as "C" to one read as "c"
131
- most_significant_byte = [most_significant_byte].pack("c").unpack("c").first
132
-
133
- (most_significant_byte << 16) | (middle_byte << 8) | least_significant_byte
134
- }
122
+ if @closed
123
+ raise ReaderClosedError
135
124
  end
136
125
 
137
- if @native_format.channels > 1
138
- samples = samples.each_slice(@native_format.channels).to_a
139
- end
140
-
141
- buffer = Buffer.new(samples, @native_format)
142
- buffer.convert(@format)
126
+ @data_chunk_reader.read(sample_frame_count)
143
127
  end
144
128
 
145
129
 
146
- # Returns true if the Reader is closed, and false if it is open and available for reading.
130
+ # Public: Returns true if the Reader is closed, and false if it is open and available for reading.
147
131
  def closed?
148
- @file.closed?
132
+ @closed
149
133
  end
150
134
 
151
135
 
152
- # Closes the Reader. After a Reader is closed, no more sample data can be read from it.
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.
153
139
  #
154
140
  # Returns nothing.
155
- # Raises IOError if the Reader is already closed.
141
+ # Raises ReaderClosedError if the Reader is already closed.
156
142
  def close
157
- @file.close
143
+ if @closed
144
+ raise ReaderClosedError
145
+ end
146
+
147
+ if @io_source == :file_name
148
+ @io.close
149
+ end
150
+
151
+ @closed = true
158
152
  end
159
153
 
160
- # Returns a Duration instance for the total number of sample frames in the file
154
+ # Public: Returns a Duration instance for the total number of sample frames in the file
161
155
  def total_duration
162
- Duration.new(total_sample_frames, @format.sample_rate)
156
+ Duration.new(total_sample_frames, @data_chunk_reader.format.sample_rate)
163
157
  end
164
158
 
165
- # Returns a Format object describing the sample format of the Wave file being read.
159
+ # Public: Returns a Format object describing the sample format of the Wave file being read.
166
160
  # This is not necessarily the format that the sample data will be read as - to determine
167
161
  # that, use #format.
168
162
  def native_format
169
- @raw_native_format
163
+ @data_chunk_reader.raw_native_format
170
164
  end
171
165
 
172
- # Returns true if this is a valid Wave file and contains sample data that is in a format
166
+ # Public: Returns true if this is a valid Wave file and contains sample data that is in a format
173
167
  # that this class can read, and returns false if this is a valid Wave file but does not
174
168
  # contain a sample format supported by this class.
175
169
  def readable_format?
176
- @readable_format
170
+ @data_chunk_reader.readable_format
177
171
  end
178
172
 
179
- # Returns the name of the Wave file that is being read
180
- attr_reader :file_name
181
-
182
- # Returns a Format object describing how sample data is being read from the Wave file (number of
183
- # channels, sample format and bits per sample, etc). Note that this might be different from the
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
184
175
  # underlying format of the Wave file on disk.
185
- attr_reader :format
176
+ def format
177
+ @data_chunk_reader.format
178
+ end
186
179
 
187
- # Returns the index of the sample frame which is "cued up" for reading. I.e., the index
188
- # of the next sample frame that will be read. A sample frame contains a single sample
189
- # for each channel. So if there are 1,000 sample frames in a stereo file, this means
180
+ # Public: Returns the index of the sample frame which is "cued up" for reading. I.e., the index
181
+ # of the next sample frame that will be read. A sample frame contains a single sample
182
+ # for each channel. So if there are 1,000 sample frames in a stereo file, this means
190
183
  # there are 1,000 left-channel samples and 1,000 right-channel samples.
191
- attr_reader :current_sample_frame
184
+ def current_sample_frame
185
+ @data_chunk_reader.current_sample_frame
186
+ end
192
187
 
193
- # Returns the total number of sample frames in the file. A sample frame contains a single
194
- # sample for each channel. So if there are 1,000 sample frames in a stereo file, this means
188
+ # Public: Returns the total number of sample frames in the file. A sample frame contains a single
189
+ # sample for each channel. So if there are 1,000 sample frames in a stereo file, this means
195
190
  # there are 1,000 left-channel samples and 1,000 right-channel samples.
196
- attr_reader :total_sample_frames
197
-
198
- private
199
-
200
- # The number of sample frames in the file after the current sample frame
201
- def sample_frames_remaining
202
- @total_sample_frames - @current_sample_frame
191
+ def total_sample_frames
192
+ @data_chunk_reader.total_sample_frames
203
193
  end
204
194
  end
205
195
  end