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.
- checksums.yaml +4 -4
- data/LICENSE +2 -2
- data/README.markdown +67 -47
- data/Rakefile +23 -0
- data/lib/wavefile.rb +4 -2
- data/lib/wavefile/buffer.rb +40 -25
- data/lib/wavefile/chunk_readers.rb +7 -1
- data/lib/wavefile/chunk_readers/base_chunk_reader.rb +10 -0
- data/lib/wavefile/chunk_readers/data_chunk_reader.rb +77 -0
- data/lib/wavefile/chunk_readers/format_chunk_reader.rb +59 -0
- data/lib/wavefile/chunk_readers/generic_chunk_reader.rb +15 -0
- data/lib/wavefile/chunk_readers/riff_chunk_reader.rb +19 -0
- data/lib/wavefile/chunk_readers/riff_reader.rb +67 -0
- data/lib/wavefile/duration.rb +47 -12
- data/lib/wavefile/format.rb +44 -23
- data/lib/wavefile/reader.rb +101 -111
- data/lib/wavefile/unvalidated_format.rb +36 -6
- data/lib/wavefile/writer.rb +138 -40
- data/test/buffer_test.rb +21 -17
- data/test/chunk_readers/format_chunk_reader_test.rb +130 -0
- data/test/duration_test.rb +42 -1
- data/test/fixtures/actual_output/total_duration_mono_float_32_44100.wav +0 -0
- data/test/fixtures/actual_output/total_duration_mono_float_64_44100.wav +0 -0
- data/test/fixtures/actual_output/total_duration_mono_pcm_16_44100.wav +0 -0
- data/test/fixtures/actual_output/total_duration_mono_pcm_24_44100.wav +0 -0
- data/test/fixtures/actual_output/total_duration_mono_pcm_32_44100.wav +0 -0
- data/test/fixtures/actual_output/total_duration_mono_pcm_8_44100.wav +0 -0
- data/test/fixtures/actual_output/total_duration_stereo_float_32_44100.wav +0 -0
- data/test/fixtures/actual_output/total_duration_stereo_float_64_44100.wav +0 -0
- data/test/fixtures/actual_output/total_duration_stereo_pcm_16_44100.wav +0 -0
- data/test/fixtures/actual_output/total_duration_stereo_pcm_24_44100.wav +0 -0
- data/test/fixtures/actual_output/total_duration_stereo_pcm_32_44100.wav +0 -0
- data/test/fixtures/actual_output/total_duration_stereo_pcm_8_44100.wav +0 -0
- data/test/fixtures/actual_output/total_duration_tri_float_32_44100.wav +0 -0
- data/test/fixtures/actual_output/total_duration_tri_float_64_44100.wav +0 -0
- data/test/fixtures/actual_output/total_duration_tri_pcm_16_44100.wav +0 -0
- data/test/fixtures/actual_output/total_duration_tri_pcm_24_44100.wav +0 -0
- data/test/fixtures/actual_output/total_duration_tri_pcm_32_44100.wav +0 -0
- data/test/fixtures/actual_output/total_duration_tri_pcm_8_44100.wav +0 -0
- data/test/fixtures/unsupported/README.markdown +1 -1
- data/test/fixtures/unsupported/bad_channel_count.wav +0 -0
- data/test/fixtures/unsupported/extensible_container_size_bigger_than_sample_size.wav +0 -0
- data/test/fixtures/unsupported/extensible_unsupported_subformat_guid.wav +0 -0
- data/test/fixtures/valid/valid_extensible_mono_float_32_44100.wav +0 -0
- data/test/fixtures/valid/valid_extensible_mono_float_64_44100.wav +0 -0
- data/test/fixtures/valid/valid_extensible_mono_pcm_16_44100.wav +0 -0
- data/test/fixtures/valid/valid_extensible_mono_pcm_24_44100.wav +0 -0
- data/test/fixtures/valid/valid_extensible_mono_pcm_32_44100.wav +0 -0
- data/test/fixtures/valid/valid_extensible_mono_pcm_8_44100.wav +0 -0
- data/test/fixtures/valid/valid_extensible_stereo_float_32_44100.wav +0 -0
- data/test/fixtures/valid/valid_extensible_stereo_float_64_44100.wav +0 -0
- data/test/fixtures/valid/valid_extensible_stereo_pcm_16_44100.wav +0 -0
- data/test/fixtures/valid/valid_extensible_stereo_pcm_24_44100.wav +0 -0
- data/test/fixtures/valid/valid_extensible_stereo_pcm_32_44100.wav +0 -0
- data/test/fixtures/valid/valid_extensible_stereo_pcm_8_44100.wav +0 -0
- data/test/fixtures/valid/valid_extensible_tri_float_32_44100.wav +0 -0
- data/test/fixtures/valid/valid_extensible_tri_float_64_44100.wav +0 -0
- data/test/fixtures/valid/valid_extensible_tri_pcm_16_44100.wav +0 -0
- data/test/fixtures/valid/valid_extensible_tri_pcm_24_44100.wav +0 -0
- data/test/fixtures/valid/valid_extensible_tri_pcm_32_44100.wav +0 -0
- data/test/fixtures/valid/valid_extensible_tri_pcm_8_44100.wav +0 -0
- data/test/format_test.rb +22 -48
- data/test/reader_test.rb +188 -93
- data/test/unvalidated_format_test.rb +130 -4
- data/test/wavefile_io_test_helper.rb +6 -4
- data/test/writer_test.rb +118 -95
- metadata +47 -4
- data/lib/wavefile/chunk_readers/header_reader.rb +0 -163
- 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
|
data/lib/wavefile/duration.rb
CHANGED
@@ -1,18 +1,18 @@
|
|
1
1
|
module WaveFile
|
2
|
-
# Calculates playback time given the number of sample frames and the sample rate.
|
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
|
-
#
|
28
|
-
# the total of
|
29
|
-
#
|
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
|
-
|
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
|
data/lib/wavefile/format.rb
CHANGED
@@ -1,21 +1,34 @@
|
|
1
1
|
module WaveFile
|
2
|
-
#
|
3
|
-
#
|
4
|
-
#
|
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
|
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, :
|
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
|
-
#
|
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
|
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,
|
data/lib/wavefile/reader.rb
CHANGED
@@ -1,22 +1,11 @@
|
|
1
1
|
module WaveFile
|
2
|
-
# Error that is raised when
|
3
|
-
|
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
|
-
#
|
8
|
-
#
|
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
|
21
|
+
# Public: Returns a Reader object that is ready to start reading the specified file's
|
22
|
+
# sample data.
|
33
23
|
#
|
34
|
-
#
|
35
|
-
#
|
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(
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
@
|
47
|
-
|
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
|
-
|
41
|
+
@closed = false
|
56
42
|
|
57
|
-
@readable_format = true
|
58
43
|
begin
|
59
|
-
|
60
|
-
|
61
|
-
|
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
|
-
@
|
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
|
81
|
-
# sample data to be read. When all sample data has been read, the Reader is automatically
|
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
|
-
#
|
85
|
-
#
|
86
|
-
#
|
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
|
-
#
|
94
|
-
|
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
|
106
|
-
# at most sample_frame_count sample frames, but could have less if the file doesn't have enough
|
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
|
-
|
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
|
-
|
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
|
-
@
|
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
|
141
|
+
# Raises ReaderClosedError if the Reader is already closed.
|
156
142
|
def close
|
157
|
-
@
|
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
|
180
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|