wavefile 0.7.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
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