wahwah 0.1.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
module WahWah
|
6
|
+
class Mp3Tag < Tag
|
7
|
+
extend TagDelegate
|
8
|
+
extend Forwardable
|
9
|
+
|
10
|
+
def_delegator :@mpeg_frame_header, :version, :mpeg_version
|
11
|
+
def_delegator :@mpeg_frame_header, :layer, :mpeg_layer
|
12
|
+
def_delegator :@mpeg_frame_header, :kind, :mpeg_kind
|
13
|
+
def_delegators :@mpeg_frame_header, :channel_mode, :sample_rate
|
14
|
+
|
15
|
+
tag_delegate :@id3_tag,
|
16
|
+
:title,
|
17
|
+
:artist,
|
18
|
+
:album,
|
19
|
+
:albumartist,
|
20
|
+
:composer,
|
21
|
+
:comments,
|
22
|
+
:track,
|
23
|
+
:track_total,
|
24
|
+
:genre,
|
25
|
+
:year,
|
26
|
+
:disc,
|
27
|
+
:disc_total,
|
28
|
+
:images
|
29
|
+
|
30
|
+
def id3v2?
|
31
|
+
@id3_tag.instance_of? ID3::V2
|
32
|
+
end
|
33
|
+
|
34
|
+
def invalid_id3?
|
35
|
+
@id3_tag.nil?
|
36
|
+
end
|
37
|
+
|
38
|
+
def id3_version
|
39
|
+
@id3_tag&.version
|
40
|
+
end
|
41
|
+
|
42
|
+
def is_vbr?
|
43
|
+
xing_header.valid? || vbri_header.valid?
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
def parse
|
48
|
+
@id3_tag = parse_id3_tag
|
49
|
+
parse_duration if mpeg_frame_header.valid?
|
50
|
+
end
|
51
|
+
|
52
|
+
def parse_id3_tag
|
53
|
+
id3_v1_tag = ID3::V1.new(@file_io)
|
54
|
+
id3_v2_tag = ID3::V2.new(@file_io)
|
55
|
+
|
56
|
+
return id3_v2_tag if id3_v2_tag.valid?
|
57
|
+
id3_v1_tag if id3_v1_tag.valid?
|
58
|
+
end
|
59
|
+
|
60
|
+
def parse_duration
|
61
|
+
if is_vbr?
|
62
|
+
@duration = (frames_count * (mpeg_frame_header.samples_per_frame / sample_rate.to_f)).round
|
63
|
+
@bitrate = bytes_count * 8 / @duration / 1000 unless @duration.zero?
|
64
|
+
else
|
65
|
+
@bitrate = mpeg_frame_header.frame_bitrate
|
66
|
+
@duration = (file_size - (@id3_tag&.size || 0)) * 8 / (@bitrate * 1000) unless @bitrate.zero?
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def mpeg_frame_header
|
71
|
+
# Because id3v2 tag on the file header so skip id3v2 tag
|
72
|
+
@mpeg_frame_header ||= Mp3::MpegFrameHeader.new(@file_io, id3v2? ? @id3_tag&.size : 0)
|
73
|
+
end
|
74
|
+
|
75
|
+
def xing_header
|
76
|
+
@xing_header ||= Mp3::XingHeader.new(@file_io, xing_header_offset)
|
77
|
+
end
|
78
|
+
|
79
|
+
def vbri_header
|
80
|
+
@vbri_header ||= Mp3::VbriHeader.new(@file_io, vbri_header_offset)
|
81
|
+
end
|
82
|
+
|
83
|
+
def xing_header_offset
|
84
|
+
mpeg_frame_header_position = mpeg_frame_header.position
|
85
|
+
mpeg_frame_header_size = Mp3::MpegFrameHeader::HEADER_SIZE
|
86
|
+
mpeg_frame_side_info_size = mpeg_version == 'MPEG1' ?
|
87
|
+
(channel_mode == 'Single Channel' ? 17 : 32) :
|
88
|
+
(channel_mode == 'Single Channel' ? 9 : 17)
|
89
|
+
|
90
|
+
mpeg_frame_header_position + mpeg_frame_header_size + mpeg_frame_side_info_size
|
91
|
+
end
|
92
|
+
|
93
|
+
def vbri_header_offset
|
94
|
+
mpeg_frame_header_position = mpeg_frame_header.position
|
95
|
+
mpeg_frame_header_size = Mp3::MpegFrameHeader::HEADER_SIZE
|
96
|
+
|
97
|
+
mpeg_frame_header_position + mpeg_frame_header_size + 32
|
98
|
+
end
|
99
|
+
|
100
|
+
def frames_count
|
101
|
+
return xing_header.frames_count if xing_header.valid?
|
102
|
+
vbri_header.frames_count if vbri_header.valid?
|
103
|
+
end
|
104
|
+
|
105
|
+
def bytes_count
|
106
|
+
return xing_header.bytes_count if xing_header.valid?
|
107
|
+
vbri_header.bytes_count if vbri_header.valid?
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WahWah
|
4
|
+
module Mp4
|
5
|
+
class Atom
|
6
|
+
VERSIONED_ATOMS = %w(meta stsd)
|
7
|
+
FLAGGED_ATOMS = %w(stsd)
|
8
|
+
HEADER_SIZE = 8
|
9
|
+
HEADER_SIZE_FIELD_SIZE = 4
|
10
|
+
EXTENDED_HEADER_SIZE = 8
|
11
|
+
|
12
|
+
attr_reader :size, :type
|
13
|
+
|
14
|
+
def self.find(file_io, *atom_path)
|
15
|
+
file_io.rewind
|
16
|
+
|
17
|
+
atom_type = atom_path.shift
|
18
|
+
|
19
|
+
until file_io.eof?
|
20
|
+
atom = new(file_io)
|
21
|
+
|
22
|
+
next unless atom.valid?
|
23
|
+
file_io.seek(atom.size, IO::SEEK_CUR); next unless atom.type == atom_type
|
24
|
+
|
25
|
+
return atom if atom_path.empty?
|
26
|
+
return atom.find(*atom_path)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Return empty atom if can not found
|
30
|
+
new(StringIO.new(''))
|
31
|
+
end
|
32
|
+
|
33
|
+
# An atom header consists of the following fields:
|
34
|
+
#
|
35
|
+
# Atom size:
|
36
|
+
# A 32-bit integer that indicates the size of the atom, including both the atom header and the atom’s contents,
|
37
|
+
# including any contained atoms. Normally, the size field contains the actual size of the atom.
|
38
|
+
#
|
39
|
+
# Type:
|
40
|
+
# A 32-bit integer that contains the type of the atom.
|
41
|
+
# This can often be usefully treated as a four-character field with a mnemonic value .
|
42
|
+
def initialize(file_io)
|
43
|
+
@size, @type = file_io.read(HEADER_SIZE)&.unpack('Na4')
|
44
|
+
return unless valid?
|
45
|
+
|
46
|
+
# If the size field of an atom is set to 1, the type field is followed by a 64-bit extended size field,
|
47
|
+
# which contains the actual size of the atom as a 64-bit unsigned integer.
|
48
|
+
@size = file_io.read(EXTENDED_HEADER_SIZE).unpack('Q>').first - EXTENDED_HEADER_SIZE if @size == 1
|
49
|
+
|
50
|
+
# If the size field of an atom is set to 0, which is allowed only for a top-level atom,
|
51
|
+
# designates the last atom in the file and indicates that the atom extends to the end of the file.
|
52
|
+
@size = file_io.size if @size == 0
|
53
|
+
@size = @size - HEADER_SIZE
|
54
|
+
@file_io = file_io
|
55
|
+
@position = file_io.pos
|
56
|
+
end
|
57
|
+
|
58
|
+
def data
|
59
|
+
@file_io.seek(@position)
|
60
|
+
@file_io.read(size)
|
61
|
+
end
|
62
|
+
|
63
|
+
def valid?
|
64
|
+
!@size.nil? && @size >= HEADER_SIZE
|
65
|
+
end
|
66
|
+
|
67
|
+
def find(*atom_path)
|
68
|
+
child_atom_index = data.index(atom_path.first)
|
69
|
+
|
70
|
+
# Return empty atom if can not found
|
71
|
+
return self.class.new(StringIO.new('')) if child_atom_index.nil?
|
72
|
+
|
73
|
+
# Because before atom type field there are 4 bytes of size field,
|
74
|
+
# So the child_atom_index should reduce 4.
|
75
|
+
self.class.find(StringIO.new(data[child_atom_index - HEADER_SIZE_FIELD_SIZE..-1]), *atom_path)
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
def children
|
80
|
+
@children ||= parse_children_atoms
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
def parse_children_atoms
|
85
|
+
children_atoms = []
|
86
|
+
atoms_data = data
|
87
|
+
|
88
|
+
# Some atoms data contain extra content before child atom data.
|
89
|
+
# So reduce those extra content to get child atom data.
|
90
|
+
atoms_data = atoms_data[4..-1] if VERSIONED_ATOMS.include? type # Skip 4 bytes for version
|
91
|
+
atoms_data = atoms_data[4..-1] if FLAGGED_ATOMS.include? type # Skip 4 bytes for flag
|
92
|
+
atoms_data_io = StringIO.new(atoms_data)
|
93
|
+
|
94
|
+
until atoms_data_io.eof?
|
95
|
+
atom = self.class.new(atoms_data_io)
|
96
|
+
children_atoms.push(atom)
|
97
|
+
|
98
|
+
atoms_data_io.seek(atom.size, IO::SEEK_CUR)
|
99
|
+
end
|
100
|
+
|
101
|
+
children_atoms
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WahWah
|
4
|
+
class Mp4Tag < Tag
|
5
|
+
META_ATOM_MAPPING = {
|
6
|
+
"\xA9alb".b => :album,
|
7
|
+
"\xA9ART".b => :artist,
|
8
|
+
"\xA9cmt".b => :comment,
|
9
|
+
"\xA9wrt".b => :composer,
|
10
|
+
"\xA9day".b => :year,
|
11
|
+
"\xA9gen".b => :genre,
|
12
|
+
"\xA9nam".b => :title,
|
13
|
+
'covr'.b => :image,
|
14
|
+
'disk'.b => :disc,
|
15
|
+
'trkn'.b => :track,
|
16
|
+
'aART'.b => :albumartist
|
17
|
+
}
|
18
|
+
|
19
|
+
META_ATOM_DECODE_BY_TYPE = {
|
20
|
+
0 => -> (data) { data }, # reserved
|
21
|
+
1 => -> (data) { Helper.encode_to_utf8(data) }, # UTF-8
|
22
|
+
2 => -> (data) { Helper.encode_to_utf8(data, 'UTF-16BE') }, # UTF-16BE
|
23
|
+
3 => -> (data) { Helper.encode_to_utf8(data, 'SJIS') }, # SJIS
|
24
|
+
|
25
|
+
13 => -> (data) { { data: data, mime_type: 'image/jpeg', type: :cover } }, # JPEG
|
26
|
+
14 => -> (data) { { data: data, mime_type: 'image/png', type: :cover } }, # PNG
|
27
|
+
|
28
|
+
21 => -> (data) { data.unpack('i>').first }, # Big endian signed integer
|
29
|
+
22 => -> (data) { data.unpack('I>').first }, # Big endian unsigned integer
|
30
|
+
23 => -> (data) { data.unpack('g').first }, # Big endian 32-bit floating point value
|
31
|
+
24 => -> (data) { data.unpack('G').first }, # Big endian 64-bit floating point value
|
32
|
+
|
33
|
+
65 => -> (data) { data.unpack('c').first }, # 8-bit signed integer
|
34
|
+
66 => -> (data) { data.unpack('s>').first }, # Big-endian 16-bit signed integer
|
35
|
+
67 => -> (data) { data.unpack('l>').first }, # Big-endian 32-bit signed integer
|
36
|
+
74 => -> (data) { data.unpack('q>').first }, # Big-endian 64-bit signed integer
|
37
|
+
|
38
|
+
75 => -> (data) { data.unpack('C').first }, # 8-bit unsigned integer
|
39
|
+
76 => -> (data) { data.unpack('S>').first }, # Big-endian 16-bit unsigned integer
|
40
|
+
77 => -> (data) { data.unpack('L>').first }, # Big-endian 32-bit unsigned integer
|
41
|
+
78 => -> (data) { data.unpack('Q>').first } # Big-endian 64-bit unsigned integer
|
42
|
+
}
|
43
|
+
|
44
|
+
private
|
45
|
+
def parse
|
46
|
+
movie_atom = Mp4::Atom.find(@file_io, 'moov')
|
47
|
+
return unless movie_atom.valid?
|
48
|
+
|
49
|
+
parse_meta_list_atom movie_atom.find('udta', 'meta', 'ilst')
|
50
|
+
parse_mvhd_atom movie_atom.find('mvhd')
|
51
|
+
parse_stsd_atom movie_atom.find('trak', 'mdia', 'minf', 'stbl', 'stsd')
|
52
|
+
end
|
53
|
+
|
54
|
+
def parse_meta_list_atom(atom)
|
55
|
+
return unless atom.valid?
|
56
|
+
|
57
|
+
# The metadata item list atom holds a list of actual metadata values that are present in the metadata atom.
|
58
|
+
# The metadata items are formatted as a list of items.
|
59
|
+
# The metadata item list atom is of type ‘ilst’ and contains a number of metadata items, each of which is an atom.
|
60
|
+
# each metadata item atom contains a Value Atom, to hold the value of the metadata item
|
61
|
+
atom.children.each do |child_atom|
|
62
|
+
attribute_name = META_ATOM_MAPPING[child_atom.type]
|
63
|
+
|
64
|
+
# The value of the metadata item is expressed as immediate data in a value atom.
|
65
|
+
# The value atom starts with two fields: a type indicator, and a locale indicator.
|
66
|
+
# Both the type and locale indicators are four bytes long.
|
67
|
+
# There may be multiple ‘value’ entries, using different type
|
68
|
+
data_atom = child_atom.find('data')
|
69
|
+
return unless data_atom.valid?
|
70
|
+
|
71
|
+
data_type, data_value = data_atom.data.unpack('Nx4a*')
|
72
|
+
encoded_data_value = META_ATOM_DECODE_BY_TYPE[data_type]&.call(data_value)
|
73
|
+
|
74
|
+
next if attribute_name.nil? || encoded_data_value.nil?
|
75
|
+
|
76
|
+
case attribute_name
|
77
|
+
when :image
|
78
|
+
@images.push(encoded_data_value)
|
79
|
+
when :comment
|
80
|
+
@comments.push(encoded_data_value)
|
81
|
+
when :track, :disc
|
82
|
+
count, total_count = encoded_data_value.unpack('x2nn')
|
83
|
+
|
84
|
+
instance_variable_set("@#{attribute_name}", count) unless count.zero?
|
85
|
+
instance_variable_set("@#{attribute_name}_total", total_count) unless total_count.zero?
|
86
|
+
else
|
87
|
+
instance_variable_set("@#{attribute_name}", encoded_data_value)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def parse_mvhd_atom(atom)
|
93
|
+
return unless atom.valid?
|
94
|
+
|
95
|
+
atom_data = StringIO.new(atom.data)
|
96
|
+
version = atom_data.read(1).unpack('c').first
|
97
|
+
|
98
|
+
# Skip flags
|
99
|
+
atom_data.seek(3, IO::SEEK_CUR)
|
100
|
+
|
101
|
+
if version == 0
|
102
|
+
# Skip creation and modification time
|
103
|
+
atom_data.seek(8, IO::SEEK_CUR)
|
104
|
+
|
105
|
+
time_scale, duration = atom_data.read(8).unpack('l>l>')
|
106
|
+
elsif version == 1
|
107
|
+
# Skip creation and modification time
|
108
|
+
atom_data.seek(16, IO::SEEK_CUR)
|
109
|
+
|
110
|
+
time_scale, duration = atom_data.read(12).unpack('l>q>')
|
111
|
+
end
|
112
|
+
|
113
|
+
@duration = (duration / time_scale.to_f).round
|
114
|
+
end
|
115
|
+
|
116
|
+
def parse_stsd_atom(atom)
|
117
|
+
return unless atom.valid?
|
118
|
+
|
119
|
+
mp4a_atom = atom.find('mp4a')
|
120
|
+
esds_atom = atom.find('esds')
|
121
|
+
|
122
|
+
@sample_rate = mp4a_atom.data.unpack('x22I>').first if mp4a_atom.valid?
|
123
|
+
@bitrate = esds_atom.data.unpack('x26I>').first / 1000 if esds_atom.valid?
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WahWah
|
4
|
+
module Ogg
|
5
|
+
class FlacTag
|
6
|
+
include VorbisComment
|
7
|
+
include Flac::StreaminfoBlock
|
8
|
+
|
9
|
+
attr_reader :bitrate, :duration, :sample_rate, *COMMET_FIELD_MAPPING.values
|
10
|
+
|
11
|
+
def initialize(identification_packet, comment_packet)
|
12
|
+
# Identification packet structure:
|
13
|
+
#
|
14
|
+
# The one-byte packet type 0x7F
|
15
|
+
# The four-byte ASCII signature "FLAC", i.e. 0x46, 0x4C, 0x41, 0x43
|
16
|
+
# A one-byte binary major version number for the mapping, e.g. 0x01 for mapping version 1.0
|
17
|
+
# A one-byte binary minor version number for the mapping, e.g. 0x00 for mapping version 1.0
|
18
|
+
# A two-byte, big-endian binary number signifying the number of header (non-audio) packets, not including this one.
|
19
|
+
# The four-byte ASCII native FLAC signature "fLaC" according to the FLAC format specification
|
20
|
+
# The STREAMINFO metadata block for the stream.
|
21
|
+
#
|
22
|
+
# The first identification packet is followed by one or more header packets.
|
23
|
+
# Each such packet will contain a single native FLAC metadata block.
|
24
|
+
# The first of these must be a VORBIS_COMMENT block.
|
25
|
+
|
26
|
+
id, streaminfo_block_data = identification_packet.unpack('x9A4A*')
|
27
|
+
|
28
|
+
return unless id == 'fLaC'
|
29
|
+
streaminfo_block = Flac::Block.new(StringIO.new(streaminfo_block_data))
|
30
|
+
vorbis_comment_block = Flac::Block.new(StringIO.new(comment_packet))
|
31
|
+
|
32
|
+
parse_streaminfo_block(streaminfo_block.data)
|
33
|
+
parse_vorbis_comment(vorbis_comment_block.data)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WahWah
|
4
|
+
module Ogg
|
5
|
+
class OpusTag
|
6
|
+
include VorbisComment
|
7
|
+
|
8
|
+
attr_reader :sample_rate, :pre_skip, *COMMET_FIELD_MAPPING.values
|
9
|
+
|
10
|
+
def initialize(identification_packet, comment_packet)
|
11
|
+
# Identification packet structure:
|
12
|
+
#
|
13
|
+
# 1) "OpusHead"
|
14
|
+
# 2) [version] = read 8 bits as unsigned integer
|
15
|
+
# 3) [audio_channels] = read 8 bit as unsigned integer
|
16
|
+
# 4) [pre_skip] = read 16 bits as unsigned little endian integer
|
17
|
+
# 5) [input_sample_rate] = read 32 bits as unsigned little endian integer
|
18
|
+
# 6) [output_gain] = read 16 bits as unsigned little endian integer
|
19
|
+
# 7) [channel_mapping_family] = read 8 bit as unsigned integer
|
20
|
+
# 8) [channel_mapping_table]
|
21
|
+
@sample_rate = 48000
|
22
|
+
@pre_skip = identification_packet[10..11].unpack('v').first
|
23
|
+
|
24
|
+
comment_packet_id, comment_packet_body = [comment_packet[0..7], comment_packet[8..-1]]
|
25
|
+
|
26
|
+
# Opus comment packet start with 'OpusTags'
|
27
|
+
return unless comment_packet_id == 'OpusTags'
|
28
|
+
|
29
|
+
parse_vorbis_comment(comment_packet_body)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WahWah
|
4
|
+
module Ogg
|
5
|
+
# From Ogg's perspective, packets can be of any arbitrary size. A
|
6
|
+
# specific media mapping will define how to group or break up packets
|
7
|
+
# from a specific media encoder. As Ogg pages have a maximum size of
|
8
|
+
# about 64 kBytes, sometimes a packet has to be distributed over
|
9
|
+
# several pages. To simplify that process, Ogg divides each packet
|
10
|
+
# into 255 byte long chunks plus a final shorter chunk. These chunks
|
11
|
+
# are called "Ogg Segments". They are only a logical construct and do
|
12
|
+
# not have a header for themselves.
|
13
|
+
class Packets
|
14
|
+
include Enumerable
|
15
|
+
|
16
|
+
def initialize(file_io)
|
17
|
+
@file_io = file_io
|
18
|
+
end
|
19
|
+
|
20
|
+
def each
|
21
|
+
@file_io.rewind
|
22
|
+
|
23
|
+
packet = +''
|
24
|
+
pages = Ogg::Pages.new(@file_io)
|
25
|
+
|
26
|
+
pages.each do |page|
|
27
|
+
page.segments.each do |segment|
|
28
|
+
packet << segment
|
29
|
+
|
30
|
+
# Ogg divides each packet into 255 byte long segments plus a final shorter segment.
|
31
|
+
# So when segment length is less than 255 byte, it's the final segment.
|
32
|
+
if segment.length < 255
|
33
|
+
yield packet
|
34
|
+
packet = +''
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|