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