bbbecd2a7c4306863a54560b6124c144623c6694
[sonitus.git] / src / main / java / net / pterodactylus / sonitus / io / OggVorbisIdentifier.java
1 /*
2  * Sonitus - OggVorbisIdentifier.java - Copyright © 2013 David Roden
3  *
4  * This program is free software: you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
16  */
17
18 package net.pterodactylus.sonitus.io;
19
20 import java.io.IOException;
21 import java.io.InputStream;
22
23 import net.pterodactylus.sonitus.data.Metadata;
24
25 import com.google.common.base.Optional;
26 import com.jcraft.jogg.Packet;
27 import com.jcraft.jogg.Page;
28 import com.jcraft.jogg.StreamState;
29 import com.jcraft.jogg.SyncState;
30 import com.jcraft.jorbis.Comment;
31 import com.jcraft.jorbis.Info;
32
33 /**
34  * Identifies Ogg Vorbis files. <p> All knowledge used in this class has been
35  * taken from <a href="http://www.jcraft.com/jorbis/tutorial/Tutorial.html">jcraft.com/jorbis/tutorial/Tutorial.html</a>.
36  * </p>
37  *
38  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
39  */
40 public class OggVorbisIdentifier {
41
42         /** The default size of the read buffer. */
43         private static final int BUFFER_SIZE = 4096;
44
45         /** Suppress default constructor. */
46         private OggVorbisIdentifier() {
47                 /* nothing here. */
48         }
49
50         /**
51          * Tries to parse the given stream as Ogg Vorbis file and returns a {@link
52          * Metadata} describing the stream.
53          *
54          * @param inputStream
55          *              The input stream to identify as Ogg Vorbis
56          * @return The identified metadata, or {@link Optional#absent()} if the stream
57          *         could not be identified
58          * @throws IOException
59          *              if an I/O error occurs
60          */
61         public static Optional<Metadata> identify(InputStream inputStream) throws IOException {
62
63                 /* stuff needed to decode Ogg. */
64                 Packet packet = new Packet();
65                 Page page = new Page();
66                 StreamState streamState = new StreamState();
67                 SyncState syncState = new SyncState();
68
69                 /* stuff needed to decode Vorbis. */
70                 Comment comment = new Comment();
71                 Info info = new Info();
72
73                 /* initialize jorbis. */
74                 syncState.init();
75                 int bufferSize = BUFFER_SIZE;
76                 int index = syncState.buffer(bufferSize);
77                 byte[] buffer = syncState.data;
78
79                 boolean streamStateInitialized = false;
80                 int packetsRead = 0;
81
82                 /* read until we have read the three packets required to decode the header. */
83                 while (packetsRead < 3) {
84                         int read = inputStream.read(buffer, index, bufferSize);
85                         syncState.wrote(read);
86                         switch (syncState.pageout(page)) {
87                                 case -1:
88                                         return Optional.absent();
89                                 case 1:
90                                         if (!streamStateInitialized) {
91                                                 /* init stream state. */
92                                                 streamState.init(page.serialno());
93                                                 streamState.reset();
94                                                 info.init();
95                                                 comment.init();
96                                                 streamStateInitialized = true;
97                                         }
98                                         if (streamState.pagein(page) == -1) {
99                                                 return Optional.absent();
100                                         }
101                                         switch (streamState.packetout(packet)) {
102                                                 case -1:
103                                                         return Optional.absent();
104                                                 case 1:
105                                                         info.synthesis_headerin(comment, packet);
106                                                         packetsRead++;
107                                                 default:
108                                                         /* continue. */
109                                         }
110
111                                 default:
112                                         /* continue. */
113                         }
114                         index = syncState.buffer(bufferSize);
115                         buffer = syncState.data;
116                 }
117
118                 Metadata metadata = new Metadata(info.channels, info.rate, "Vorbis");
119                 for (int c = 0; c < comment.comments; ++c) {
120                         String field = comment.getComment(c);
121                         Optional<String> extractedField = extractField(field, "ARTIST");
122                         if (extractedField.isPresent()) {
123                                 metadata = metadata.artist(extractedField.get());
124                                 continue;
125                         }
126                         extractedField = extractField(field, "TITLE");
127                         if (extractedField.isPresent()) {
128                                 metadata = metadata.name(extractedField.get());
129                                 continue;
130                         }
131                 }
132                 return Optional.of(metadata);
133         }
134
135         /**
136          * Extracts the content of the field from the comment if the comment contains
137          * the given field.
138          *
139          * @param comment
140          *              The comment to extract the value from
141          * @param fieldName
142          *              The name of the field to extract
143          * @return The extracted field, or {@link Optional#absent()} if the comment
144          *         does not contain the given field
145          */
146         private static Optional<String> extractField(String comment, String fieldName) {
147                 if (comment.startsWith(fieldName + "=")) {
148                         return Optional.of(comment.substring(fieldName.length() + 1));
149                 }
150                 return Optional.absent();
151         }
152
153 }