wahwah 0.1.0 → 1.2.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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +21 -0
  3. data/lib/wahwah/asf/object.rb +34 -0
  4. data/lib/wahwah/asf_tag.rb +221 -0
  5. data/lib/wahwah/errors.rb +7 -0
  6. data/lib/wahwah/flac/block.rb +51 -0
  7. data/lib/wahwah/flac/streaminfo_block.rb +51 -0
  8. data/lib/wahwah/flac_tag.rb +86 -0
  9. data/lib/wahwah/helper.rb +37 -0
  10. data/lib/wahwah/id3/comment_frame_body.rb +21 -0
  11. data/lib/wahwah/id3/frame.rb +176 -0
  12. data/lib/wahwah/id3/frame_body.rb +35 -0
  13. data/lib/wahwah/id3/genre_frame_body.rb +15 -0
  14. data/lib/wahwah/id3/image_frame_body.rb +60 -0
  15. data/lib/wahwah/id3/text_frame_body.rb +16 -0
  16. data/lib/wahwah/id3/v1.rb +97 -0
  17. data/lib/wahwah/id3/v2.rb +67 -0
  18. data/lib/wahwah/id3/v2_header.rb +53 -0
  19. data/lib/wahwah/lazy_read.rb +40 -0
  20. data/lib/wahwah/mp3/mpeg_frame_header.rb +143 -0
  21. data/lib/wahwah/mp3/vbri_header.rb +47 -0
  22. data/lib/wahwah/mp3/xing_header.rb +45 -0
  23. data/lib/wahwah/mp3_tag.rb +111 -0
  24. data/lib/wahwah/mp4/atom.rb +101 -0
  25. data/lib/wahwah/mp4_tag.rb +137 -0
  26. data/lib/wahwah/ogg/flac_tag.rb +37 -0
  27. data/lib/wahwah/ogg/opus_tag.rb +33 -0
  28. data/lib/wahwah/ogg/packets.rb +41 -0
  29. data/lib/wahwah/ogg/page.rb +121 -0
  30. data/lib/wahwah/ogg/pages.rb +24 -0
  31. data/lib/wahwah/ogg/vorbis_comment.rb +51 -0
  32. data/lib/wahwah/ogg/vorbis_tag.rb +35 -0
  33. data/lib/wahwah/ogg_tag.rb +66 -0
  34. data/lib/wahwah/riff/chunk.rb +50 -0
  35. data/lib/wahwah/riff_tag.rb +142 -0
  36. data/lib/wahwah/tag.rb +71 -0
  37. data/lib/wahwah/tag_delegate.rb +16 -0
  38. data/lib/wahwah/version.rb +4 -2
  39. data/lib/wahwah.rb +78 -2
  40. metadata +109 -24
  41. data/.gitignore +0 -8
  42. data/.travis.yml +0 -5
  43. data/Gemfile +0 -6
  44. data/Gemfile.lock +0 -22
  45. data/README.md +0 -35
  46. data/Rakefile +0 -10
  47. data/bin/console +0 -14
  48. data/bin/setup +0 -8
  49. data/wahwah.gemspec +0 -27
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WahWah
4
+ module Mp3
5
+ # mpeg frame header structure:
6
+ #
7
+ # Position Length Meaning
8
+ # 0 11 Frame sync to find the header (all bits are always set)
9
+ #
10
+ # 11 2 Audio version ID
11
+ # 00 - MPEG Version 2.5 (unofficial extension of MPEG 2)
12
+ # 01 - reserved
13
+ # 10 - MPEG Version 2 (ISO/IEC 13818-3)
14
+ # 11 - MPEG Version 1 (ISO/IEC 11172-3)
15
+ #
16
+ # 13 2 Layer index
17
+ # 00 - reserved
18
+ # 01 - Layer III
19
+ # 10 - Layer II
20
+ # 11 - Layer I
21
+ #
22
+ # 15 1 Protection bit
23
+ #
24
+ # 16 4 Bitrate index, see FRAME_BITRATE_INDEX constant
25
+ #
26
+ # 20 2 Sampling rate index, see SAMPLE_RATE_INDEX constant
27
+ #
28
+ # 22 1 Padding bit
29
+ #
30
+ # 23 1 Private bit
31
+ #
32
+ # 24 2 Channel mode
33
+ # 00 - Stereo
34
+ # 01 - Joint Stereo (Stereo)
35
+ # 10 - Dual channel (Two mono channels)
36
+ # 11 - Single channel (Mono)
37
+ #
38
+ # 26 2 Mode extension (Only used in Joint Stereo)
39
+ #
40
+ # 28 1 Copyright bit (only informative)
41
+ #
42
+ # 29 1 Original bit (only informative)
43
+ #
44
+ # 30 2 Emphasis
45
+ class MpegFrameHeader
46
+ HEADER_SIZE = 4
47
+
48
+ FRAME_BITRATE_INDEX = {
49
+ "MPEG1 layer1" => [0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, 0],
50
+ "MPEG1 layer2" => [0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 0],
51
+ "MPEG1 layer3" => [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0],
52
+
53
+ "MPEG2 layer1" => [0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256, 0],
54
+ "MPEG2 layer2" => [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0],
55
+ "MPEG2 layer3" => [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0],
56
+
57
+ "MPEG2.5 layer1" => [0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256, 0],
58
+ "MPEG2.5 layer2" => [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0],
59
+ "MPEG2.5 layer3" => [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0]
60
+ }
61
+
62
+ VERSIONS_INDEX = ["MPEG2.5", nil, "MPEG2", "MPEG1"]
63
+ LAYER_INDEX = [nil, "layer3", "layer2", "layer1"]
64
+ CHANNEL_MODE_INDEX = ["Stereo", "Joint Stereo", "Dual Channel", "Single Channel"]
65
+
66
+ SAMPLE_RATE_INDEX = {
67
+ "MPEG1" => [44100, 48000, 32000],
68
+ "MPEG2" => [22050, 24000, 16000],
69
+ "MPEG2.5" => [11025, 12000, 8000]
70
+ }
71
+
72
+ SAMPLES_PER_FRAME_INDEX = {
73
+ "MPEG1 layer1" => 384,
74
+ "MPEG1 layer2" => 1152,
75
+ "MPEG1 layer3" => 1152,
76
+
77
+ "MPEG2 layer1" => 384,
78
+ "MPEG2 layer2" => 1152,
79
+ "MPEG2 layer3" => 576,
80
+
81
+ "MPEG2.5 layer1" => 384,
82
+ "MPEG2.5 layer2" => 1152,
83
+ "MPEG2.5 layer3" => 576
84
+ }
85
+
86
+ attr_reader :version, :layer, :frame_bitrate, :channel_mode, :sample_rate
87
+
88
+ def initialize(file_io, offset = 0)
89
+ # mpeg frame header start with '11111111111' sync bits,
90
+ # So look through file until find it.
91
+ loop do
92
+ file_io.rewind
93
+ file_io.seek(offset)
94
+
95
+ break if file_io.eof?
96
+
97
+ header = file_io.read(HEADER_SIZE)
98
+ sync_bits = header.unpack1("B11")
99
+
100
+ if sync_bits == ("1" * 11).b
101
+ @header = header.unpack1("B*")
102
+ @position = offset
103
+
104
+ parse
105
+ break
106
+ end
107
+
108
+ offset += 1
109
+ end
110
+ end
111
+
112
+ def valid?
113
+ !@header.nil?
114
+ end
115
+
116
+ def position
117
+ return 0 unless valid?
118
+ @position
119
+ end
120
+
121
+ def kind
122
+ return if @version.nil? && @layer.nil?
123
+ "#{@version} #{@layer}"
124
+ end
125
+
126
+ def samples_per_frame
127
+ SAMPLES_PER_FRAME_INDEX[kind]
128
+ end
129
+
130
+ private
131
+
132
+ def parse
133
+ return unless valid?
134
+
135
+ @version = VERSIONS_INDEX[@header[11..12].to_i(2)]
136
+ @layer = LAYER_INDEX[@header[13..14].to_i(2)]
137
+ @frame_bitrate = FRAME_BITRATE_INDEX[kind]&.fetch(@header[16..19].to_i(2))
138
+ @channel_mode = CHANNEL_MODE_INDEX[@header[24..25].to_i(2)]
139
+ @sample_rate = SAMPLE_RATE_INDEX[@version]&.fetch(@header[20..21].to_i(2))
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WahWah
4
+ module Mp3
5
+ # VBRI header structure:
6
+ #
7
+ # Position Length Meaning
8
+ # 0 4 VBR header ID in 4 ASCII chars, always 'VBRI', not NULL-terminated
9
+ #
10
+ # 4 2 Version ID as Big-Endian 16-bit unsigned
11
+ #
12
+ # 6 2 Delay as Big-Endian float
13
+ #
14
+ # 8 2 Quality indicator
15
+ #
16
+ # 10 4 Number of Bytes as Big-Endian 32-bit unsigned
17
+ #
18
+ # 14 4 Number of Frames as Big-Endian 32-bit unsigned
19
+ #
20
+ # 18 2 Number of entries within TOC table as Big-Endian 16-bit unsigned
21
+ #
22
+ # 20 2 Scale factor of TOC table entries as Big-Endian 32-bit unsigned
23
+ #
24
+ # 22 2 Size per table entry in bytes (max 4) as Big-Endian 16-bit unsigned
25
+ #
26
+ # 24 2 Frames per table entry as Big-Endian 16-bit unsigned
27
+ #
28
+ # 26 TOC entries for seeking as Big-Endian integral.
29
+ # From size per table entry and number of entries,
30
+ # you can calculate the length of this field.
31
+ class VbriHeader
32
+ HEADER_SIZE = 32
33
+ HEADER_FORMAT = "A4x6NN"
34
+
35
+ attr_reader :frames_count, :bytes_count
36
+
37
+ def initialize(file_io, offset = 0)
38
+ file_io.seek(offset)
39
+ @id, @bytes_count, @frames_count = file_io.read(HEADER_SIZE)&.unpack(HEADER_FORMAT)
40
+ end
41
+
42
+ def valid?
43
+ @id == "VBRI"
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WahWah
4
+ module Mp3
5
+ # Xing header structure:
6
+ #
7
+ # Position Length Meaning
8
+ # 0 4 VBR header ID in 4 ASCII chars, either 'Xing' or 'Info',
9
+ # not NULL-terminated
10
+ #
11
+ # 4 4 Flags which indicate what fields are present,
12
+ # flags are combined with a logical OR. Field is mandatory.
13
+ #
14
+ # 0x0001 - Frames field is present
15
+ # 0x0002 - Bytes field is present
16
+ # 0x0004 - TOC field is present
17
+ # 0x0008 - Quality indicator field is present
18
+ #
19
+ # 8 4 Number of Frames as Big-Endian 32-bit unsigned (optional)
20
+ #
21
+ # 8 or 12 4 Number of Bytes in file as Big-Endian 32-bit unsigned (optional)
22
+ #
23
+ # 8,12 or 16 100 100 TOC entries for seeking as integral BYTE (optional)
24
+ #
25
+ # 8,12,16,108,112 or 116 4 Quality indicator as Big-Endian 32-bit unsigned
26
+ # from 0 - best quality to 100 - worst quality (optional)
27
+ class XingHeader
28
+ attr_reader :frames_count, :bytes_count
29
+
30
+ def initialize(file_io, offset = 0)
31
+ file_io.seek(offset)
32
+
33
+ @id, @flags = file_io.read(8)&.unpack("A4N")
34
+ return unless valid?
35
+
36
+ @frames_count = @flags & 1 == 1 ? file_io.read(4).unpack1("N") : 0
37
+ @bytes_count = @flags & 2 == 2 ? file_io.read(4).unpack1("N") : 0
38
+ end
39
+
40
+ def valid?
41
+ %w[Xing Info].include? @id
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WahWah
4
+ class Mp3Tag < Tag
5
+ extend TagDelegate
6
+ extend Forwardable
7
+
8
+ def_delegator :@mpeg_frame_header, :version, :mpeg_version
9
+ def_delegator :@mpeg_frame_header, :layer, :mpeg_layer
10
+ def_delegator :@mpeg_frame_header, :kind, :mpeg_kind
11
+ def_delegators :@mpeg_frame_header, :channel_mode, :sample_rate
12
+
13
+ tag_delegate :@id3_tag,
14
+ :title,
15
+ :artist,
16
+ :album,
17
+ :albumartist,
18
+ :composer,
19
+ :comments,
20
+ :track,
21
+ :track_total,
22
+ :genre,
23
+ :year,
24
+ :disc,
25
+ :disc_total,
26
+ :images
27
+
28
+ def id3v2?
29
+ @id3_tag.instance_of? ID3::V2
30
+ end
31
+
32
+ def invalid_id3?
33
+ @id3_tag.nil?
34
+ end
35
+
36
+ def id3_version
37
+ @id3_tag&.version
38
+ end
39
+
40
+ def is_vbr?
41
+ mpeg_frame_header.valid? && (xing_header.valid? || vbri_header.valid?)
42
+ end
43
+
44
+ private
45
+
46
+ def parse
47
+ @id3_tag = parse_id3_tag
48
+ parse_duration if mpeg_frame_header.valid?
49
+ end
50
+
51
+ def parse_id3_tag
52
+ id3_v1_tag = ID3::V1.new(@file_io.dup)
53
+ id3_v2_tag = ID3::V2.new(@file_io.dup)
54
+
55
+ return id3_v2_tag if id3_v2_tag.valid?
56
+ id3_v1_tag if id3_v1_tag.valid?
57
+ end
58
+
59
+ def parse_duration
60
+ if is_vbr?
61
+ @duration = frames_count * (mpeg_frame_header.samples_per_frame / sample_rate.to_f)
62
+ @bitrate = (bytes_count * 8 / @duration / 1000).round unless @duration.zero?
63
+ else
64
+ @bitrate = mpeg_frame_header.frame_bitrate
65
+ @duration = (file_size - (@id3_tag&.size || 0)) * 8 / (@bitrate * 1000).to_f unless @bitrate.zero?
66
+ end
67
+ end
68
+
69
+ def mpeg_frame_header
70
+ # Because id3v2 tag on the file header so skip id3v2 tag
71
+ @mpeg_frame_header ||= Mp3::MpegFrameHeader.new(@file_io, id3v2? ? @id3_tag&.size : 0)
72
+ end
73
+
74
+ def xing_header
75
+ @xing_header ||= Mp3::XingHeader.new(@file_io, xing_header_offset)
76
+ end
77
+
78
+ def vbri_header
79
+ @vbri_header ||= Mp3::VbriHeader.new(@file_io, vbri_header_offset)
80
+ end
81
+
82
+ def xing_header_offset
83
+ mpeg_frame_header_position = mpeg_frame_header.position
84
+ mpeg_frame_header_size = Mp3::MpegFrameHeader::HEADER_SIZE
85
+ mpeg_frame_side_info_size = if mpeg_version == "MPEG1"
86
+ channel_mode == "Single Channel" ? 17 : 32
87
+ else
88
+ channel_mode == "Single Channel" ? 9 : 17
89
+ end
90
+
91
+ mpeg_frame_header_position + mpeg_frame_header_size + mpeg_frame_side_info_size
92
+ end
93
+
94
+ def vbri_header_offset
95
+ mpeg_frame_header_position = mpeg_frame_header.position
96
+ mpeg_frame_header_size = Mp3::MpegFrameHeader::HEADER_SIZE
97
+
98
+ mpeg_frame_header_position + mpeg_frame_header_size + 32
99
+ end
100
+
101
+ def frames_count
102
+ return xing_header.frames_count if xing_header.valid?
103
+ vbri_header.frames_count if vbri_header.valid?
104
+ end
105
+
106
+ def bytes_count
107
+ return xing_header.bytes_count if xing_header.valid?
108
+ vbri_header.bytes_count if vbri_header.valid?
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WahWah
4
+ module Mp4
5
+ class Atom
6
+ prepend LazyRead
7
+
8
+ VERSIONED_ATOMS = %w[meta stsd]
9
+ FLAGGED_ATOMS = %w[stsd]
10
+ HEADER_SIZE = 8
11
+ HEADER_SIZE_FIELD_SIZE = 4
12
+ EXTENDED_HEADER_SIZE = 8
13
+
14
+ attr_reader :type
15
+
16
+ def self.find(file_io, *atom_path)
17
+ file_io.rewind
18
+
19
+ atom_type = atom_path.shift
20
+
21
+ until file_io.eof?
22
+ atom = new(file_io)
23
+
24
+ next unless atom.valid?
25
+ file_io.seek(atom.size, IO::SEEK_CUR)
26
+ next unless atom.type == atom_type
27
+
28
+ return atom if atom_path.empty?
29
+ return atom.find(*atom_path)
30
+ end
31
+
32
+ # Return empty atom if can not found
33
+ new(StringIO.new(""))
34
+ end
35
+
36
+ # An atom header consists of the following fields:
37
+ #
38
+ # Atom size:
39
+ # A 32-bit integer that indicates the size of the atom, including both the atom header and the atom’s contents,
40
+ # including any contained atoms. Normally, the size field contains the actual size of the atom.
41
+ #
42
+ # Type:
43
+ # A 32-bit integer that contains the type of the atom.
44
+ # This can often be usefully treated as a four-character field with a mnemonic value .
45
+ def initialize
46
+ @size, @type = @file_io.read(HEADER_SIZE)&.unpack("Na4")
47
+ return unless valid?
48
+
49
+ # If the size field of an atom is set to 1, the type field is followed by a 64-bit extended size field,
50
+ # which contains the actual size of the atom as a 64-bit unsigned integer.
51
+ @size = @file_io.read(EXTENDED_HEADER_SIZE).unpack1("Q>") - EXTENDED_HEADER_SIZE if @size == 1
52
+
53
+ # If the size field of an atom is set to 0, which is allowed only for a top-level atom,
54
+ # designates the last atom in the file and indicates that the atom extends to the end of the file.
55
+ @size = @file_io.size if @size == 0
56
+ @size -= HEADER_SIZE
57
+ end
58
+
59
+ def valid?
60
+ !@size.nil? && @size >= HEADER_SIZE
61
+ end
62
+
63
+ def find(*atom_path)
64
+ child_atom_index = data.index(atom_path.first)
65
+
66
+ # Return empty atom if can not found
67
+ return self.class.new(StringIO.new("")) if child_atom_index.nil?
68
+
69
+ # Because before atom type field there are 4 bytes of size field,
70
+ # So the child_atom_index should reduce 4.
71
+ self.class.find(StringIO.new(data[child_atom_index - HEADER_SIZE_FIELD_SIZE..]), *atom_path)
72
+ end
73
+
74
+ def children
75
+ @children ||= parse_children_atoms
76
+ end
77
+
78
+ private
79
+
80
+ def parse_children_atoms
81
+ children_atoms = []
82
+ atoms_data = data
83
+
84
+ # Some atoms data contain extra content before child atom data.
85
+ # So reduce those extra content to get child atom data.
86
+ atoms_data = atoms_data[4..] if VERSIONED_ATOMS.include? type # Skip 4 bytes for version
87
+ atoms_data = atoms_data[4..] if FLAGGED_ATOMS.include? type # Skip 4 bytes for flag
88
+ atoms_data_io = StringIO.new(atoms_data)
89
+
90
+ until atoms_data_io.eof?
91
+ atom = self.class.new(atoms_data_io)
92
+ children_atoms.push(atom)
93
+
94
+ atom.skip
95
+ end
96
+
97
+ children_atoms
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,137 @@
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.unpack1("i>") }, # Big endian signed integer
29
+ 22 => ->(data) { data.unpack1("I>") }, # Big endian unsigned integer
30
+ 23 => ->(data) { data.unpack1("g") }, # Big endian 32-bit floating point value
31
+ 24 => ->(data) { data.unpack1("G") }, # Big endian 64-bit floating point value
32
+
33
+ 65 => ->(data) { data.unpack1("c") }, # 8-bit signed integer
34
+ 66 => ->(data) { data.unpack1("s>") }, # Big-endian 16-bit signed integer
35
+ 67 => ->(data) { data.unpack1("l>") }, # Big-endian 32-bit signed integer
36
+ 74 => ->(data) { data.unpack1("q>") }, # Big-endian 64-bit signed integer
37
+
38
+ 75 => ->(data) { data.unpack1("C") }, # 8-bit unsigned integer
39
+ 76 => ->(data) { data.unpack1("S>") }, # Big-endian 16-bit unsigned integer
40
+ 77 => ->(data) { data.unpack1("L>") }, # Big-endian 32-bit unsigned integer
41
+ 78 => ->(data) { data.unpack1("Q>") } # Big-endian 64-bit unsigned integer
42
+ }
43
+
44
+ private
45
+
46
+ def parse
47
+ movie_atom = Mp4::Atom.find(@file_io, "moov")
48
+ return unless movie_atom.valid?
49
+
50
+ parse_meta_list_atom movie_atom.find("udta", "meta", "ilst")
51
+ parse_mvhd_atom movie_atom.find("mvhd")
52
+ parse_stsd_atom movie_atom.find("trak", "mdia", "minf", "stbl", "stsd")
53
+ end
54
+
55
+ def parse_meta_list_atom(atom)
56
+ return unless atom.valid?
57
+
58
+ # The metadata item list atom holds a list of actual metadata values that are present in the metadata atom.
59
+ # The metadata items are formatted as a list of items.
60
+ # The metadata item list atom is of type ‘ilst’ and contains a number of metadata items, each of which is an atom.
61
+ # each metadata item atom contains a Value Atom, to hold the value of the metadata item
62
+ atom.children.each do |child_atom|
63
+ attr_name = META_ATOM_MAPPING[child_atom.type]
64
+
65
+ # The value of the metadata item is expressed as immediate data in a value atom.
66
+ # The value atom starts with two fields: a type indicator, and a locale indicator.
67
+ # Both the type and locale indicators are four bytes long.
68
+ # There may be multiple ‘value’ entries, using different type
69
+ data_atom = child_atom.find("data")
70
+ next unless data_atom.valid?
71
+
72
+ if attr_name == :image
73
+ @images_data.push(data_atom)
74
+ next
75
+ end
76
+
77
+ encoded_data_value = parse_meta_data_atom(data_atom)
78
+ next if attr_name.nil? || encoded_data_value.nil?
79
+
80
+ case attr_name
81
+ when :comment
82
+ @comments.push(encoded_data_value)
83
+ when :track, :disc
84
+ count, total_count = encoded_data_value.unpack("x2nn")
85
+
86
+ instance_variable_set("@#{attr_name}", count) unless count.zero?
87
+ instance_variable_set("@#{attr_name}_total", total_count) unless total_count.zero?
88
+ else
89
+ instance_variable_set("@#{attr_name}", encoded_data_value)
90
+ end
91
+ end
92
+ end
93
+
94
+ def parse_meta_data_atom(atom)
95
+ data_type, data_value = atom.data.unpack("Nx4a*")
96
+ META_ATOM_DECODE_BY_TYPE[data_type]&.call(data_value)
97
+ end
98
+
99
+ def parse_mvhd_atom(atom)
100
+ return unless atom.valid?
101
+
102
+ atom_data = StringIO.new(atom.data)
103
+ version = atom_data.read(1).unpack1("c")
104
+
105
+ # Skip flags
106
+ atom_data.seek(3, IO::SEEK_CUR)
107
+
108
+ if version == 0
109
+ # Skip creation and modification time
110
+ atom_data.seek(8, IO::SEEK_CUR)
111
+
112
+ time_scale, duration = atom_data.read(8).unpack("l>l>")
113
+ elsif version == 1
114
+ # Skip creation and modification time
115
+ atom_data.seek(16, IO::SEEK_CUR)
116
+
117
+ time_scale, duration = atom_data.read(12).unpack("l>q>")
118
+ end
119
+
120
+ @duration = duration / time_scale.to_f
121
+ end
122
+
123
+ def parse_stsd_atom(atom)
124
+ return unless atom.valid?
125
+
126
+ mp4a_atom = atom.find("mp4a")
127
+ esds_atom = atom.find("esds")
128
+
129
+ @sample_rate = mp4a_atom.data.unpack1("x22I>") if mp4a_atom.valid?
130
+ @bitrate = esds_atom.data.unpack1("x26I>") / 1000 if esds_atom.valid?
131
+ end
132
+
133
+ def parse_image_data(image_data_atom)
134
+ parse_meta_data_atom(image_data_atom)
135
+ end
136
+ end
137
+ 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, :bit_depth, *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].unpack1("v")
23
+
24
+ comment_packet_id, comment_packet_body = [comment_packet[0..7], comment_packet[8..]]
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