wahwah 0.1.0 → 1.0.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 +21 -0
- data/lib/wahwah.rb +74 -3
- data/lib/wahwah/asf/object.rb +39 -0
- data/lib/wahwah/asf_tag.rb +220 -0
- data/lib/wahwah/errors.rb +6 -0
- data/lib/wahwah/flac/block.rb +57 -0
- data/lib/wahwah/flac/streaminfo_block.rb +51 -0
- data/lib/wahwah/flac_tag.rb +84 -0
- data/lib/wahwah/helper.rb +37 -0
- data/lib/wahwah/id3/comment_frame_body.rb +21 -0
- data/lib/wahwah/id3/frame.rb +180 -0
- data/lib/wahwah/id3/frame_body.rb +36 -0
- data/lib/wahwah/id3/genre_frame_body.rb +15 -0
- data/lib/wahwah/id3/image_frame_body.rb +60 -0
- data/lib/wahwah/id3/text_frame_body.rb +16 -0
- data/lib/wahwah/id3/v1.rb +96 -0
- data/lib/wahwah/id3/v2.rb +60 -0
- data/lib/wahwah/id3/v2_header.rb +53 -0
- data/lib/wahwah/mp3/mpeg_frame_header.rb +141 -0
- data/lib/wahwah/mp3/vbri_header.rb +47 -0
- data/lib/wahwah/mp3/xing_header.rb +45 -0
- data/lib/wahwah/mp3_tag.rb +110 -0
- data/lib/wahwah/mp4/atom.rb +105 -0
- data/lib/wahwah/mp4_tag.rb +126 -0
- data/lib/wahwah/ogg/flac_tag.rb +37 -0
- data/lib/wahwah/ogg/opus_tag.rb +33 -0
- data/lib/wahwah/ogg/packets.rb +41 -0
- data/lib/wahwah/ogg/page.rb +121 -0
- data/lib/wahwah/ogg/pages.rb +24 -0
- data/lib/wahwah/ogg/vorbis_comment.rb +51 -0
- data/lib/wahwah/ogg/vorbis_tag.rb +35 -0
- data/lib/wahwah/ogg_tag.rb +66 -0
- data/lib/wahwah/riff/chunk.rb +54 -0
- data/lib/wahwah/riff_tag.rb +140 -0
- data/lib/wahwah/tag.rb +59 -0
- data/lib/wahwah/tag_delegate.rb +16 -0
- data/lib/wahwah/version.rb +4 -2
- metadata +94 -23
- data/.gitignore +0 -8
- data/.travis.yml +0 -5
- data/Gemfile +0 -6
- data/Gemfile.lock +0 -22
- data/README.md +0 -35
- data/Rakefile +0 -10
- data/bin/console +0 -14
- data/bin/setup +0 -8
- data/wahwah.gemspec +0 -27
@@ -0,0 +1,121 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WahWah
|
4
|
+
module Ogg
|
5
|
+
# The Ogg page header has the following format:
|
6
|
+
#
|
7
|
+
# 0 1 2 3
|
8
|
+
# 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1| Byte
|
9
|
+
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
10
|
+
# | capture_pattern: Magic number for page start "OggS" | 0-3
|
11
|
+
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
12
|
+
# | version | header_type | granule_position | 4-7
|
13
|
+
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
14
|
+
# | | 8-11
|
15
|
+
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
16
|
+
# | | bitstream_serial_number | 12-15
|
17
|
+
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
18
|
+
# | | page_sequence_number | 16-19
|
19
|
+
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
20
|
+
# | | CRC_checksum | 20-23
|
21
|
+
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
22
|
+
# | |page_segments | segment_table | 24-27
|
23
|
+
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
24
|
+
# | ... | 28-
|
25
|
+
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
26
|
+
#
|
27
|
+
#
|
28
|
+
# The fields in the page header have the following meaning:
|
29
|
+
#
|
30
|
+
# 1. capture_pattern: a 4 Byte field that signifies the beginning of a
|
31
|
+
# page. It contains the magic numbers:
|
32
|
+
|
33
|
+
# 0x4f 'O'
|
34
|
+
|
35
|
+
# 0x67 'g'
|
36
|
+
|
37
|
+
# 0x67 'g'
|
38
|
+
|
39
|
+
# 0x53 'S'
|
40
|
+
|
41
|
+
# It helps a decoder to find the page boundaries and regain
|
42
|
+
# synchronisation after parsing a corrupted stream. Once the
|
43
|
+
# capture pattern is found, the decoder verifies page sync and
|
44
|
+
# integrity by computing and comparing the checksum.
|
45
|
+
|
46
|
+
# 2. stream_structure_version: 1 Byte signifying the version number of
|
47
|
+
# the Ogg file format used in this stream (this document specifies
|
48
|
+
# version 0).
|
49
|
+
|
50
|
+
# 3. header_type_flag: the bits in this 1 Byte field identify the
|
51
|
+
# specific type of this page.
|
52
|
+
|
53
|
+
# * bit 0x01
|
54
|
+
|
55
|
+
# set: page contains data of a packet continued from the previous
|
56
|
+
# page
|
57
|
+
|
58
|
+
# unset: page contains a fresh packet
|
59
|
+
|
60
|
+
# * bit 0x02
|
61
|
+
|
62
|
+
# set: this is the first page of a logical bitstream (bos)
|
63
|
+
|
64
|
+
# unset: this page is not a first page
|
65
|
+
|
66
|
+
# * bit 0x04
|
67
|
+
|
68
|
+
# set: this is the last page of a logical bitstream (eos)
|
69
|
+
|
70
|
+
# unset: this page is not a last page
|
71
|
+
|
72
|
+
# 4. granule_position: an 8 Byte field containing position information.
|
73
|
+
# For example, for an audio stream, it MAY contain the total number
|
74
|
+
# of PCM samples encoded after including all frames finished on this
|
75
|
+
# page. For a video stream it MAY contain the total number of video
|
76
|
+
# frames encoded after this page. This is a hint for the decoder
|
77
|
+
# and gives it some timing and position information. Its meaning is
|
78
|
+
# dependent on the codec for that logical bitstream and specified in
|
79
|
+
# a specific media mapping. A special value of -1 (in two's
|
80
|
+
# complement) indicates that no packets finish on this page.
|
81
|
+
|
82
|
+
# 5. bitstream_serial_number: a 4 Byte field containing the unique
|
83
|
+
# serial number by which the logical bitstream is identified.
|
84
|
+
|
85
|
+
# 6. page_sequence_number: a 4 Byte field containing the sequence
|
86
|
+
# number of the page so the decoder can identify page loss. This
|
87
|
+
# sequence number is increasing on each logical bitstream
|
88
|
+
# separately.
|
89
|
+
|
90
|
+
# 7. CRC_checksum: a 4 Byte field containing a 32 bit CRC checksum of
|
91
|
+
# the page (including header with zero CRC field and page content).
|
92
|
+
# The generator polynomial is 0x04c11db7.
|
93
|
+
|
94
|
+
# 8. number_page_segments: 1 Byte giving the number of segment entries
|
95
|
+
# encoded in the segment table.
|
96
|
+
|
97
|
+
# 9. segment_table: number_page_segments Bytes containing the lacing
|
98
|
+
# values of all segments in this page. Each Byte contains one
|
99
|
+
# lacing value.
|
100
|
+
class Page
|
101
|
+
HEADER_SIZE = 27
|
102
|
+
HEADER_FORMAT = 'A4CxQx12C'
|
103
|
+
|
104
|
+
attr_reader :segments, :granule_position
|
105
|
+
|
106
|
+
def initialize(file_io)
|
107
|
+
header_content = file_io.read(HEADER_SIZE)
|
108
|
+
@capture_pattern, @version, @granule_position, page_segments = header_content.unpack(HEADER_FORMAT) if header_content.size >= HEADER_SIZE
|
109
|
+
|
110
|
+
return unless valid?
|
111
|
+
|
112
|
+
segment_table = file_io.read(page_segments).unpack('C' * page_segments)
|
113
|
+
@segments = segment_table.map { |segment_length| file_io.read(segment_length) }
|
114
|
+
end
|
115
|
+
|
116
|
+
def valid?
|
117
|
+
@capture_pattern == 'OggS' && @version == 0
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WahWah
|
4
|
+
module Ogg
|
5
|
+
class Pages
|
6
|
+
include Enumerable
|
7
|
+
|
8
|
+
def initialize(file_io)
|
9
|
+
@file_io = file_io
|
10
|
+
end
|
11
|
+
|
12
|
+
def each
|
13
|
+
@file_io.rewind
|
14
|
+
|
15
|
+
until @file_io.eof?
|
16
|
+
page = Ogg::Page.new(@file_io)
|
17
|
+
break unless page.valid?
|
18
|
+
|
19
|
+
yield page
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WahWah
|
4
|
+
module Ogg
|
5
|
+
# Vorbis comment structure:
|
6
|
+
#
|
7
|
+
# 1) [vendor_length] = read an unsigned integer of 32 bits
|
8
|
+
# 2) [vendor_string] = read a UTF-8 vector as [vendor_length] octets
|
9
|
+
# 3) [user_comment_list_length] = read an unsigned integer of 32 bits
|
10
|
+
# 4) iterate [user_comment_list_length] times {
|
11
|
+
# 5) [length] = read an unsigned integer of 32 bits
|
12
|
+
# 6) this iteration’s user comment = read a UTF-8 vector as [length] octets
|
13
|
+
# }
|
14
|
+
# 7) [framing_bit] = read a single bit as boolean
|
15
|
+
# 8) if ( [framing_bit] unset or end-of-packet ) then ERROR
|
16
|
+
# 9) done.
|
17
|
+
module VorbisComment
|
18
|
+
COMMET_FIELD_MAPPING = {
|
19
|
+
'TITLE' => :title,
|
20
|
+
'ALBUM' => :album,
|
21
|
+
'ALBUMARTIST' => :albumartist,
|
22
|
+
'TRACKNUMBER' => :track,
|
23
|
+
'ARTIST' => :artist,
|
24
|
+
'DATE' => :year,
|
25
|
+
'GENRE' => :genre,
|
26
|
+
'DISCNUMBER' => :disc,
|
27
|
+
'COMPOSER' => :composer
|
28
|
+
}
|
29
|
+
|
30
|
+
def parse_vorbis_comment(comment_content)
|
31
|
+
comment_content = StringIO.new(comment_content)
|
32
|
+
|
33
|
+
vendor_length = comment_content.read(4).unpack('V').first
|
34
|
+
comment_content.seek(vendor_length, IO::SEEK_CUR) # Skip vendor_string
|
35
|
+
|
36
|
+
comment_list_length = comment_content.read(4).unpack('V').first
|
37
|
+
|
38
|
+
comment_list_length.times do
|
39
|
+
comment_length = comment_content.read(4).unpack('V').first
|
40
|
+
comment = Helper.encode_to_utf8(comment_content.read(comment_length))
|
41
|
+
field_name, field_value = comment.split('=', 2)
|
42
|
+
attr_name = COMMET_FIELD_MAPPING[field_name]
|
43
|
+
|
44
|
+
field_value = field_value.to_i if %i(track disc).include? attr_name
|
45
|
+
|
46
|
+
instance_variable_set("@#{attr_name}", field_value) unless attr_name.nil?
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WahWah
|
4
|
+
module Ogg
|
5
|
+
class VorbisTag
|
6
|
+
include VorbisComment
|
7
|
+
|
8
|
+
attr_reader :bitrate, :sample_rate, *COMMET_FIELD_MAPPING.values
|
9
|
+
|
10
|
+
def initialize(identification_packet, comment_packet)
|
11
|
+
# Identification packet structure:
|
12
|
+
#
|
13
|
+
# 1) "\x01vorbis"
|
14
|
+
# 2) [vorbis_version] = read 32 bits as unsigned integer
|
15
|
+
# 3) [audio_channels] = read 8 bit integer as unsigned
|
16
|
+
# 4) [audio_sample_rate] = read 32 bits as unsigned integer
|
17
|
+
# 5) [bitrate_maximum] = read 32 bits as signed integer
|
18
|
+
# 6) [bitrate_nominal] = read 32 bits as signed integer
|
19
|
+
# 7) [bitrate_minimum] = read 32 bits as signed integer
|
20
|
+
# 8) [blocksize_0] = 2 exponent (read 4 bits as unsigned integer)
|
21
|
+
# 9) [blocksize_1] = 2 exponent (read 4 bits as unsigned integer)
|
22
|
+
# 10) [framing_flag] = read one bit
|
23
|
+
@sample_rate, bitrate = identification_packet[12, 12].unpack('Vx4V')
|
24
|
+
@bitrate = bitrate / 1000
|
25
|
+
|
26
|
+
comment_packet_id, comment_packet_body = [comment_packet[0..6], comment_packet[7..-1]]
|
27
|
+
|
28
|
+
# Vorbis comment packet start with "\x03vorbis"
|
29
|
+
return unless comment_packet_id == "\x03vorbis"
|
30
|
+
|
31
|
+
parse_vorbis_comment(comment_packet_body)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WahWah
|
4
|
+
class OggTag < Tag
|
5
|
+
extend TagDelegate
|
6
|
+
|
7
|
+
tag_delegate :@tag,
|
8
|
+
:title,
|
9
|
+
:album,
|
10
|
+
:albumartist,
|
11
|
+
:track,
|
12
|
+
:artist,
|
13
|
+
:year,
|
14
|
+
:genre,
|
15
|
+
:disc,
|
16
|
+
:composer,
|
17
|
+
:sample_rate
|
18
|
+
|
19
|
+
def duration
|
20
|
+
@duration ||= parse_duration
|
21
|
+
end
|
22
|
+
|
23
|
+
def bitrate
|
24
|
+
@bitrate ||= parse_bitrate
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
def packets
|
29
|
+
@packets ||= Ogg::Packets.new(@file_io)
|
30
|
+
end
|
31
|
+
|
32
|
+
def pages
|
33
|
+
@pages ||= Ogg::Pages.new(@file_io)
|
34
|
+
end
|
35
|
+
|
36
|
+
def parse
|
37
|
+
identification_packet, comment_packet = packets.first(2)
|
38
|
+
return if identification_packet.nil? || comment_packet.nil?
|
39
|
+
|
40
|
+
@overhead_packets_size = identification_packet.size + comment_packet.size
|
41
|
+
|
42
|
+
@tag = case true
|
43
|
+
when identification_packet.start_with?("\x01vorbis")
|
44
|
+
Ogg::VorbisTag.new(identification_packet, comment_packet)
|
45
|
+
when identification_packet.start_with?('OpusHead')
|
46
|
+
Ogg::OpusTag.new(identification_packet, comment_packet)
|
47
|
+
when identification_packet.start_with?("\x7FFLAC")
|
48
|
+
Ogg::FlacTag.new(identification_packet, comment_packet)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def parse_duration
|
53
|
+
return @tag.duration if @tag.respond_to? :duration
|
54
|
+
|
55
|
+
last_page = pages.to_a.last
|
56
|
+
pre_skip = @tag.respond_to?(:pre_skip) ? @tag.pre_skip : 0
|
57
|
+
|
58
|
+
((last_page.granule_position - pre_skip) / @tag.sample_rate.to_f).round
|
59
|
+
end
|
60
|
+
|
61
|
+
def parse_bitrate
|
62
|
+
return @tag.bitrate if @tag.respond_to? :bitrate
|
63
|
+
((file_size - @overhead_packets_size) * 8.0 / duration / 1000).round
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WahWah
|
4
|
+
module Riff
|
5
|
+
# RIFF files consist entirely of "chunks".
|
6
|
+
|
7
|
+
# All chunks have the following format:
|
8
|
+
|
9
|
+
# 4 bytes: an ASCII identifier for this chunk (examples are "fmt " and "data"; note the space in "fmt ").
|
10
|
+
# 4 bytes: an unsigned, little-endian 32-bit integer with the length of this chunk (except this field itself and the chunk identifier).
|
11
|
+
# variable-sized field: the chunk data itself, of the size given in the previous field.
|
12
|
+
# a pad byte, if the chunk's length is not even.
|
13
|
+
|
14
|
+
# chunk identifiers, "RIFF" and "LIST", introduce a chunk that can contain subchunks. The RIFF and LIST chunk data (appearing after the identifier and length) have the following format:
|
15
|
+
|
16
|
+
# 4 bytes: an ASCII identifier for this particular RIFF or LIST chunk (for RIFF in the typical case, these 4 bytes describe the content of the entire file, such as "AVI " or "WAVE").
|
17
|
+
# rest of data: subchunks.
|
18
|
+
class Chunk
|
19
|
+
HEADER_SIZE = 8
|
20
|
+
HEADER_FORMAT = 'A4V'
|
21
|
+
HEADER_TYPE_SIZE = 4
|
22
|
+
|
23
|
+
attr_reader :id, :type
|
24
|
+
|
25
|
+
def initialize(file_io)
|
26
|
+
@id, @size = file_io.read(HEADER_SIZE)&.unpack(HEADER_FORMAT)
|
27
|
+
return unless valid?
|
28
|
+
|
29
|
+
@type = file_io.read(HEADER_TYPE_SIZE).unpack('A4').first if have_type?
|
30
|
+
@file_io = file_io
|
31
|
+
@position = file_io.pos
|
32
|
+
end
|
33
|
+
|
34
|
+
def size
|
35
|
+
@size = @size + 1 if @size.odd?
|
36
|
+
have_type? ? @size - HEADER_TYPE_SIZE : @size
|
37
|
+
end
|
38
|
+
|
39
|
+
def data
|
40
|
+
@file_io.seek(@position)
|
41
|
+
@file_io.read(size)
|
42
|
+
end
|
43
|
+
|
44
|
+
def valid?
|
45
|
+
!@id.empty? && !@size.nil? && @size > 0
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
def have_type?
|
50
|
+
%w(RIFF LIST).include? @id
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WahWah
|
4
|
+
class RiffTag < Tag
|
5
|
+
extend TagDelegate
|
6
|
+
|
7
|
+
# see https://exiftool.org/TagNames/RIFF.html#Info for more info
|
8
|
+
INFO_ID_MAPPING = {
|
9
|
+
INAM: :title,
|
10
|
+
TITL: :title,
|
11
|
+
IART: :artist,
|
12
|
+
IPRD: :album,
|
13
|
+
ICMT: :comment,
|
14
|
+
ICRD: :year,
|
15
|
+
YEAR: :year,
|
16
|
+
IGNR: :genre,
|
17
|
+
TRCK: :track
|
18
|
+
}
|
19
|
+
|
20
|
+
CHANNEL_MODE_INDEX = %w(Mono Stereo)
|
21
|
+
|
22
|
+
tag_delegate :@id3_tag,
|
23
|
+
:title,
|
24
|
+
:artist,
|
25
|
+
:album,
|
26
|
+
:albumartist,
|
27
|
+
:composer,
|
28
|
+
:comments,
|
29
|
+
:track,
|
30
|
+
:track_total,
|
31
|
+
:genre,
|
32
|
+
:year,
|
33
|
+
:disc,
|
34
|
+
:disc_total,
|
35
|
+
:images
|
36
|
+
|
37
|
+
def channel_mode
|
38
|
+
CHANNEL_MODE_INDEX[@channel - 1]
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
def parse
|
43
|
+
top_chunk = Riff::Chunk.new(@file_io)
|
44
|
+
return unless top_chunk.valid?
|
45
|
+
|
46
|
+
total_chunk_size = top_chunk.size + Riff::Chunk::HEADER_SIZE
|
47
|
+
|
48
|
+
# The top "RIFF" chunks include an additional field in the first four bytes of the data field.
|
49
|
+
# This additional field provides the form type of the field.
|
50
|
+
# For wav file, the value of the type field is 'WAVE'
|
51
|
+
return unless top_chunk.id == 'RIFF' && top_chunk.type == 'WAVE'
|
52
|
+
|
53
|
+
until total_chunk_size <= @file_io.pos || @file_io.eof? do
|
54
|
+
sub_chunk = Riff::Chunk.new(@file_io)
|
55
|
+
parse_sub_chunk(sub_chunk)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def parse_sub_chunk(sub_chunk)
|
60
|
+
return unless sub_chunk.valid?
|
61
|
+
|
62
|
+
case sub_chunk.id
|
63
|
+
when 'fmt'
|
64
|
+
parse_fmt_chunk(sub_chunk)
|
65
|
+
when 'data'
|
66
|
+
parse_data_chunk(sub_chunk)
|
67
|
+
when 'LIST'
|
68
|
+
parse_list_chunk(sub_chunk)
|
69
|
+
when 'id3', 'ID3'
|
70
|
+
parse_id3_chunk(sub_chunk)
|
71
|
+
else
|
72
|
+
@file_io.seek(sub_chunk.size, IO::SEEK_CUR)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# The fmt chunk data structure:
|
77
|
+
# Length Meaning Description
|
78
|
+
#
|
79
|
+
# 2(little endian) AudioFormat PCM = 1 (i.e. Linear quantization)
|
80
|
+
# Values other than 1 indicate some
|
81
|
+
# form of compression.
|
82
|
+
#
|
83
|
+
# 2(little endian) NumChannels Mono = 1, Stereo = 2, etc.
|
84
|
+
#
|
85
|
+
# 4(little endian) SampleRate 8000, 44100, etc.
|
86
|
+
#
|
87
|
+
# 4(little endian) ByteRate == SampleRate * NumChannels * BitsPerSample/8
|
88
|
+
#
|
89
|
+
# 2(little endian) BlockAlign == NumChannels * BitsPerSample/8
|
90
|
+
# The number of bytes for one sample including
|
91
|
+
# all channels.
|
92
|
+
#
|
93
|
+
# 2(little endian) BitsPerSample 8 bits = 8, 16 bits = 16, etc.
|
94
|
+
def parse_fmt_chunk(chunk)
|
95
|
+
_, @channel, @sample_rate, _, _, @bits_per_sample = chunk.data.unpack('vvVVvv')
|
96
|
+
@bitrate = @sample_rate * @channel * @bits_per_sample / 1000
|
97
|
+
end
|
98
|
+
|
99
|
+
def parse_data_chunk(chunk)
|
100
|
+
@duration = chunk.size * 8 / (@bitrate * 1000)
|
101
|
+
@file_io.seek(chunk.size, IO::SEEK_CUR)
|
102
|
+
end
|
103
|
+
|
104
|
+
def parse_list_chunk(chunk)
|
105
|
+
list_chunk_end_position = @file_io.pos + chunk.size
|
106
|
+
|
107
|
+
# RIFF can be tagged with metadata in the INFO chunk.
|
108
|
+
# And INFO chunk as a subchunk for LIST chunk.
|
109
|
+
if chunk.type != 'INFO'
|
110
|
+
@file_io.seek(chunk.size, IO::SEEK_CUR)
|
111
|
+
else
|
112
|
+
until list_chunk_end_position <= @file_io.pos do
|
113
|
+
info_chunk = Riff::Chunk.new(@file_io)
|
114
|
+
|
115
|
+
unless INFO_ID_MAPPING.keys.include? info_chunk.id.to_sym
|
116
|
+
@file_io.seek(info_chunk.size, IO::SEEK_CUR); next
|
117
|
+
end
|
118
|
+
|
119
|
+
update_attribute(info_chunk)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def parse_id3_chunk(chunk)
|
125
|
+
@id3_tag = ID3::V2.new(StringIO.new(chunk.data))
|
126
|
+
end
|
127
|
+
|
128
|
+
def update_attribute(chunk)
|
129
|
+
attribute_name = INFO_ID_MAPPING[chunk.id.to_sym]
|
130
|
+
chunk_data = Helper.encode_to_utf8(chunk.data)
|
131
|
+
|
132
|
+
case attribute_name
|
133
|
+
when :comment
|
134
|
+
@comments.push(chunk_data)
|
135
|
+
else
|
136
|
+
instance_variable_set("@#{attribute_name}", chunk_data)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|