diff --git a/core/pom.xml b/core/pom.xml index c0b15e0..57b2df7 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -43,6 +43,14 @@ + + org.apache.maven.plugins + maven-compiler-plugin + + 8 + 8 + + diff --git a/core/src/main/java/org/gagravarr/flac/FlacFile.java b/core/src/main/java/org/gagravarr/flac/FlacFile.java index 3c3f40b..4932784 100644 --- a/core/src/main/java/org/gagravarr/flac/FlacFile.java +++ b/core/src/main/java/org/gagravarr/flac/FlacFile.java @@ -14,13 +14,17 @@ package org.gagravarr.flac; import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; import java.io.Closeable; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import java.io.SequenceInputStream; +import java.util.Enumeration; import java.util.List; +import java.util.Vector; import org.gagravarr.ogg.IOUtils; import org.gagravarr.ogg.OggFile; @@ -52,7 +56,7 @@ public static FlacFile open(InputStream inp) throws IOException, FileNotFoundExc byte[] header = new byte[4]; IOUtils.readFully(inp, header); inp.reset(); - + if(header[0] == (byte)'O' && header[1] == (byte)'g' && header[2] == (byte)'g' && header[3] == (byte)'S') { return new FlacOggFile(new OggFile(inp)); @@ -89,8 +93,15 @@ public FlacTags getTags() { /** * In Reading mode, will close the underlying ogg/flac * file and free its resources. - * In Writing mode, will write out the Info and + * In Writing mode, will write out the Info and * Comments objects, and then the audio data. */ public abstract void close() throws IOException; + + /** + *

Return {@link InputStream} of {@link FlacFile}. If tags modified, then return modified.

+ * @return + */ + public abstract InputStream getInputStream(); + } diff --git a/core/src/main/java/org/gagravarr/flac/FlacMetadataBlock.java b/core/src/main/java/org/gagravarr/flac/FlacMetadataBlock.java index 2c3da83..fa3e493 100644 --- a/core/src/main/java/org/gagravarr/flac/FlacMetadataBlock.java +++ b/core/src/main/java/org/gagravarr/flac/FlacMetadataBlock.java @@ -100,7 +100,7 @@ public byte[] getData() { // Fix the length byte[] data = baos.toByteArray(); - IOUtils.putInt3BE(data, 1, data.length); + IOUtils.putInt3BE(data, 1, data.length - 4); // All done return data; diff --git a/core/src/main/java/org/gagravarr/flac/FlacNativeFile.java b/core/src/main/java/org/gagravarr/flac/FlacNativeFile.java index 35e9335..5bc4e7c 100644 --- a/core/src/main/java/org/gagravarr/flac/FlacNativeFile.java +++ b/core/src/main/java/org/gagravarr/flac/FlacNativeFile.java @@ -13,106 +13,164 @@ */ package org.gagravarr.flac; +import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import java.io.SequenceInputStream; +import java.nio.file.Path; import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.Vector; import org.gagravarr.flac.FlacTags.FlacTagsAsMetadata; import org.gagravarr.ogg.IOUtils; +import org.gagravarr.ogg.OggPacket; +import org.gagravarr.ogg.OggStreamPacket; /** * This lets you work with FLAC files that - * are contained in a native FLAC Stream + * are contained in a native FLAC Stream */ public class FlacNativeFile extends FlacFile { - private InputStream input; - - /** - * Opens the given file for reading - */ - public FlacNativeFile(File f) throws IOException, FileNotFoundException { - this(new FileInputStream(f)); - } - - /** - * Opens the given FLAC file - */ - public FlacNativeFile(InputStream inp) throws IOException { - // Check the header - byte[] header = new byte[4]; - IOUtils.readFully(inp, header); - if(header[0] == (byte)'f' && header[1] == (byte)'L' && - header[2] == (byte)'a' && header[3] == (byte)'C') { - // Good - } else { - throw new IllegalArgumentException("Not a FLAC file"); - } - - // First must be the FLAC info - info = (FlacInfo)FlacMetadataBlock.create(inp); - - // Read the rest of the Metadata blocks - otherMetadata = new ArrayList(); - while(true) { - FlacMetadataBlock m = FlacMetadataBlock.create(inp); - if(m instanceof FlacTagsAsMetadata) { - tags = ((FlacTagsAsMetadata)m).getTags(); - } else { - otherMetadata.add(m); - } - - if(m.isLastMetadataBlock()) { - break; - } - } - - // Rest is audio - this.input = inp; - } - - - public FlacAudioFrame getNextAudioPacket() throws IOException { - int skipped = 0; - int b1 = 0; - int b2 = input.read(); - while (b1 != -1 && b2 != -1) { - b1 = b2; - b2 = input.read(); - if (FlacAudioFrame.isFrameHeaderStart(b1, b2)) { - if (skipped > 0) - System.err.println("Warning - had to skip " + skipped + - " bytes of junk data before finding the next packet header"); - return new FlacAudioFrame(b1, b2, input, info); - } - skipped++; - } - return null; - } - - /** - * Skips the audio data to the next packet with a granule - * of at least the given granule position. - * Note that skipping backwards is not currently supported! - */ - public void skipToGranule(long granulePosition) throws IOException { - throw new RuntimeException("Not supported"); - } - - /** - * In Reading mode, will close the underlying ogg/flac - * file and free its resources. - * In Writing mode, will write out the Info and - * Comments objects, and then the audio data. - */ - public void close() throws IOException { - if(input != null) { - input.close(); - input = null; - } else { - throw new RuntimeException("Not supported"); - } - } + private InputStream input; + private final LinkedList blocksInOrder = new LinkedList<>(); + + /** + * Opens the given file for reading + */ + public FlacNativeFile(File f) throws IOException, FileNotFoundException { + this(new FileInputStream(f)); + } + + /** + * Opens the given FLAC file + */ + public FlacNativeFile(InputStream inp) throws IOException { + // Check the header + byte[] header = new byte[4]; + IOUtils.readFully(inp, header); + if (header[0] == (byte) 'f' && header[1] == (byte) 'L' && + header[2] == (byte) 'a' && header[3] == (byte) 'C') { + // Good + } else { + throw new IllegalArgumentException("Not a FLAC file"); + } + + // First must be the FLAC info + info = (FlacInfo) FlacMetadataBlock.create(inp); + blocksInOrder.addLast(info); + + // Read the rest of the Metadata blocks + otherMetadata = new ArrayList<>(); + while (true) { + FlacMetadataBlock m = FlacMetadataBlock.create(inp); + if (m instanceof FlacTagsAsMetadata) { + tags = ((FlacTagsAsMetadata) m).getTags(); + blocksInOrder.addLast(new OggStreamPacketDecorator(tags)); + } else { + otherMetadata.add(m); + blocksInOrder.addLast(m); + } + + if (m.isLastMetadataBlock()) { + break; + } + } + + // Rest is audio + this.input = inp; + } + + public FlacAudioFrame getNextAudioPacket() throws IOException { + int skipped = 0; + int b1 = 0; + int b2 = input.read(); + while (b1 != -1 && b2 != -1) { + b1 = b2; + b2 = input.read(); + if (FlacAudioFrame.isFrameHeaderStart(b1, b2)) { + if (skipped > 0) + System.err.println("Warning - had to skip " + skipped + + " bytes of junk data before finding the next packet header"); + return new FlacAudioFrame(b1, b2, input, info); + } + skipped++; + } + return null; + } + + /** + * Skips the audio data to the next packet with a granule + * of at least the given granule position. + * Note that skipping backwards is not currently supported! + */ + public void skipToGranule(long granulePosition) throws IOException { + throw new RuntimeException("Not supported"); + } + + /** + * In Reading mode, will close the underlying ogg/flac + * file and free its resources. + * In Writing mode, will write out the Info and + * Comments objects, and then the audio data. + */ + public void close() throws IOException { + if (input != null) { + input.close(); + input = null; + } else { + throw new RuntimeException("Not supported"); + } + } + + @Override + public InputStream getInputStream() { + Vector streams = new Vector<>(); + byte[] header = {'f', 'L', 'a', 'C'}; + streams.add(new ByteArrayInputStream(header)); + blocksInOrder.stream().forEach(block -> this.addStream(streams, block.getData())); + streams.add(input); + + return new SequenceInputStream(streams.elements()); + } + + private void addStream(Vector streams, byte[] data) { + if (Objects.nonNull(data) && data.length > 0) + streams.add(new ByteArrayInputStream(data)); + } + + private static final class OggStreamPacketDecorator extends FlacFrame implements OggStreamPacket { + + private OggStreamPacket decorated; + + public OggStreamPacketDecorator(OggStreamPacket decorated) { + this.decorated = decorated; + } + + public byte[] getData() { + return decorated.getData(); + } + + @Override + public void setData(byte[] data) { + + } + + @Override + public int getOggOverheadSize() { + return 0; + } + + @Override + public OggPacket write() { + return null; + } + + } } diff --git a/core/src/main/java/org/gagravarr/flac/FlacOggFile.java b/core/src/main/java/org/gagravarr/flac/FlacOggFile.java index 8635728..1a200af 100644 --- a/core/src/main/java/org/gagravarr/flac/FlacOggFile.java +++ b/core/src/main/java/org/gagravarr/flac/FlacOggFile.java @@ -18,6 +18,7 @@ import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.List; @@ -223,6 +224,11 @@ public void close() throws IOException { } } + @Override + public InputStream getInputStream() { + return null; + } + /** * Return the Ogg-specific version of the Flac Info */ diff --git a/core/src/main/java/org/gagravarr/flac/FlacTags.java b/core/src/main/java/org/gagravarr/flac/FlacTags.java index c1adc53..4c9448b 100644 --- a/core/src/main/java/org/gagravarr/flac/FlacTags.java +++ b/core/src/main/java/org/gagravarr/flac/FlacTags.java @@ -15,6 +15,7 @@ import java.io.IOException; import java.io.OutputStream; +import java.util.Arrays; import org.gagravarr.ogg.IOUtils; import org.gagravarr.ogg.OggPacket; @@ -24,75 +25,81 @@ /** * This is a {@link VorbisComments} with a Flac metadata - * block header, rather than the usual vorbis one. + * block header, rather than the usual vorbis one. */ public class FlacTags extends VorbisStyleComments implements OggAudioTagsHeader { - public FlacTags(OggPacket packet) { - super(packet, 4); - - // Verify the type - byte type = getData()[0]; - if(type != FlacMetadataBlock.VORBIS_COMMENT) { - throw new IllegalArgumentException("Invalid type " + type); - } - } - public FlacTags() { - super(); - } - - /** - * Type plus three byte length - */ - @Override - public int getHeaderSize() { - return 4; - } - /** - * Flac doesn't do the framing bit if the tags are - * null padded. - */ - @Override - protected boolean hasFramingBit() { - return false; - } - /** - * Type plus three byte length - */ - @Override - public void populateMetadataHeader(byte[] b, int dataLength) { - b[0] = FlacMetadataBlock.VORBIS_COMMENT; - IOUtils.putInt3BE(b, 1, dataLength); - } - @Override - protected void populateMetadataFooter(OutputStream out) { - // No footer needed on FLAC Tag Packets - } - - protected static class FlacTagsAsMetadata extends FlacMetadataBlock { - private FlacTags tags; - public FlacTagsAsMetadata(byte type, byte[] data) { - super(type); - // This is the only metadata which needs the type - // and length in addition to the main data - byte[] d = new byte[data.length+4]; - d[0] = FlacMetadataBlock.VORBIS_COMMENT; - System.arraycopy(data, 0, d, 4, data.length); - this.tags = new FlacTags(new OggPacket(d)); - } + public FlacTags(OggPacket packet) { + super(packet, 4); - @Override - public byte[] getData() { - return tags.getData(); - } - - @Override - protected void write(OutputStream out) throws IOException { - throw new IllegalStateException("Must not call directly"); - } + // Verify the type + byte type = getData()[0]; + if (type != FlacMetadataBlock.VORBIS_COMMENT) { + throw new IllegalArgumentException("Invalid type " + type); + } + } - public FlacTags getTags() { - return tags; - } - } + public FlacTags() { + super(); + } + + /** + * Type plus three byte length + */ + @Override + public int getHeaderSize() { + return 4; + } + + /** + * Flac doesn't do the framing bit if the tags are + * null padded. + */ + @Override + protected boolean hasFramingBit() { + return false; + } + + /** + * Type plus three byte length + */ + @Override + public void populateMetadataHeader(byte[] b, int dataLength) { + b[0] = FlacMetadataBlock.VORBIS_COMMENT; + IOUtils.putInt3BE(b, 1, dataLength); + } + + @Override + protected void populateMetadataFooter(OutputStream out) { + // No footer needed on FLAC Tag Packets + } + + protected static class FlacTagsAsMetadata extends FlacMetadataBlock { + private FlacTags tags; + + public FlacTagsAsMetadata(byte type, byte[] data) { + super(type); + // This is the only metadata which needs the type + // and length in addition to the main data + byte[] d = new byte[data.length + 4]; + d[0] = FlacMetadataBlock.VORBIS_COMMENT; + System.arraycopy(data, 0, d, 4, data.length); + IOUtils.putInt3BE(d, 1, data.length); + this.tags = new FlacTags(new OggPacket(d)); + } + + @Override + public byte[] getData() { + return tags.getData(); + } + + @Override + protected void write(OutputStream out) throws IOException { + throw new IllegalStateException("Must not call directly"); + } + + public FlacTags getTags() { + return tags; + } + } } diff --git a/core/src/main/java/org/gagravarr/ogg/HighLevelOggStreamPacket.java b/core/src/main/java/org/gagravarr/ogg/HighLevelOggStreamPacket.java index 68e728f..02bca91 100644 --- a/core/src/main/java/org/gagravarr/ogg/HighLevelOggStreamPacket.java +++ b/core/src/main/java/org/gagravarr/ogg/HighLevelOggStreamPacket.java @@ -15,9 +15,9 @@ /** * A high level stream packet sat atop - * of an OggPacket. + * of an OggPacket. * Provides support for reading and writing - * new and existing OggPacket instances. + * new and existing OggPacket instances. */ public abstract class HighLevelOggStreamPacket implements OggStreamPacket { private OggPacket oggPacket; @@ -26,6 +26,7 @@ public abstract class HighLevelOggStreamPacket implements OggStreamPacket { protected HighLevelOggStreamPacket(OggPacket oggPacket) { this.oggPacket = oggPacket; } + protected HighLevelOggStreamPacket() { this.oggPacket = null; } @@ -35,25 +36,30 @@ protected OggPacket getOggPacket() { } public byte[] getData() { - if(data != null) { + return getDecoratedData(); + } + + public void setData(byte[] data) { + this.data = data; + } + + private byte[] getDecoratedData() { + if (data != null) { return data; } - if(oggPacket != null) { + if (oggPacket != null) { return oggPacket.getData(); } return null; } - public void setData(byte[] data) { - this.data = data; - } /** * Returns the approximate number of bytes overhead - * from the underlying {@link OggPacket} / {@link OggPage} - * structures into which this data is stored. + * from the underlying {@link OggPacket} / {@link OggPage} + * structures into which this data is stored. *

Will return 0 for packets not yet associated with a page. *

This information is normally only of interest to information, - * diagnostic and debugging tools. + * diagnostic and debugging tools. */ public int getOggOverheadSize() { if (oggPacket != null) { @@ -64,7 +70,7 @@ public int getOggOverheadSize() { } public OggPacket write() { - this.oggPacket = new OggPacket(getData()); + this.oggPacket = new OggPacket(getDecoratedData()); return this.oggPacket; } } diff --git a/core/src/main/java/org/gagravarr/vorbis/VorbisStyleComments.java b/core/src/main/java/org/gagravarr/vorbis/VorbisStyleComments.java index 9858180..bd94ee4 100644 --- a/core/src/main/java/org/gagravarr/vorbis/VorbisStyleComments.java +++ b/core/src/main/java/org/gagravarr/vorbis/VorbisStyleComments.java @@ -30,7 +30,7 @@ /** * General class for all Vorbis-style comments/tags, as used - * by things like Vorbis, Opus and FLAC. + * by things like Vorbis, Opus and FLAC. */ public abstract class VorbisStyleComments extends HighLevelOggStreamPacket implements OggAudioTagsHeader { public static final String KEY_ARTIST = "artist"; @@ -41,39 +41,39 @@ public abstract class VorbisStyleComments extends HighLevelOggStreamPacket imple public static final String KEY_DATE = "date"; private String vendor; - private Map> comments = - new HashMap>(); + private Map> comments = new HashMap>(); + private boolean modified; public VorbisStyleComments(OggPacket pkt, int dataBeginsAt) { super(pkt); byte[] d = pkt.getData(); int vlen = getInt4(d, dataBeginsAt); - vendor = IOUtils.getUTF8(d, dataBeginsAt+4, vlen); + vendor = IOUtils.getUTF8(d, dataBeginsAt + 4, vlen); int offset = dataBeginsAt + 4 + vlen; int numComments = getInt4(d, offset); offset += 4; - for(int i=0; i= 0x20 && (int)c <= 0x7d && - (int)c != 0x3d) { + for (char c : tag.toLowerCase(Locale.ROOT).toCharArray()) { + if ((int) c >= 0x20 && (int) c <= 0x7d && + (int) c != 0x3d) { nt.append(c); } } @@ -110,7 +111,7 @@ protected static String normaliseTag(String tag) { protected String getSingleComment(String normalisedTag) { List c = comments.get(normalisedTag); - if(c != null && c.size() > 0) { + if (c != null && c.size() > 0) { return c.get(0); } return null; @@ -119,58 +120,64 @@ protected String getSingleComment(String normalisedTag) { /** * Returns the (first) Artist, or null if no - * Artist tags present. + * Artist tags present. */ public String getArtist() { return getSingleComment(KEY_ARTIST); } + /** * Returns the (first) Album, or null if no - * Album tags present. + * Album tags present. */ public String getAlbum() { return getSingleComment(KEY_ALBUM); } + /** * Returns the (first) Title, or null if no - * Title tags present. + * Title tags present. */ public String getTitle() { return getSingleComment(KEY_TITLE); } + /** * Returns the (first) Genre, or null if no - * Genre tags present. + * Genre tags present. */ public String getGenre() { return getSingleComment(KEY_GENRE); } + /** * Returns the (first) track number as a literal - * string, eg "4" or "09", or null if - * no track number tags present; + * string, eg "4" or "09", or null if + * no track number tags present; */ public String getTrackNumber() { return getSingleComment(KEY_TRACKNUMBER); } + /** * Returns the track number, as converted into - * an integer, or -1 if not available / not numeric + * an integer, or -1 if not available / not numeric */ public int getTrackNumberNumeric() { String number = getTrackNumber(); - if(number == null) return -1; + if (number == null) return -1; try { return Integer.parseInt(number); - } catch(NumberFormatException e) { + } catch (NumberFormatException e) { return -1; } } + /** * Returns the (first) Date, or null if no - * Date tags present. Dates are normally stored - * in ISO8601 date format, i.e. YYYY-MM-DD + * Date tags present. Dates are normally stored + * in ISO8601 date format, i.e. YYYY-MM-DD */ public String getDate() { return getSingleComment("date"); @@ -178,12 +185,12 @@ public String getDate() { /** * Returns all comments for a given tag, in - * file order. Will return an empty list for - * tags which aren't present. + * file order. Will return an empty list for + * tags which aren't present. */ public List getComments(String tag) { - List c = comments.get( normaliseTag(tag) ); - if(c == null) { + List c = comments.get(normaliseTag(tag)); + if (c == null) { return new ArrayList(); } else { return c; @@ -194,13 +201,16 @@ public List getComments(String tag) { * Removes all comments for a given tag. */ public void removeComments(String tag) { - comments.remove( normaliseTag(tag) ); + comments.remove(normaliseTag(tag)); + this.modified = true; } + /** * Removes all comments across all tags */ public void removeAllComments() { comments.clear(); + this.modified = true; } /** @@ -208,21 +218,24 @@ public void removeAllComments() { */ public void addComment(String tag, String comment) { String nt = normaliseTag(tag); - if(! comments.containsKey(nt)) { + if (!comments.containsKey(nt)) { comments.put(nt, new ArrayList()); } comments.get(nt).add(comment); + this.modified = true; } + /** * Removes any existing comments for a given tag, - * and replaces them with the supplied list + * and replaces them with the supplied list */ public void setComments(String tag, List comments) { String nt = normaliseTag(tag); - if(this.comments.containsKey(nt)) { + if (this.comments.containsKey(nt)) { this.comments.remove(nt); } this.comments.put(nt, comments); + this.modified = true; } @@ -234,12 +247,24 @@ public Map> getAllComments() { } protected abstract int getHeaderSize(); + protected abstract boolean hasFramingBit(); + protected abstract void populateMetadataHeader(byte[] data, int packetLength); + protected abstract void populateMetadataFooter(OutputStream out); protected int getInt4(byte[] d, int offset) { - return (int)IOUtils.getInt4(d, offset); + return (int) IOUtils.getInt4(d, offset); + } + + @Override + public byte[] getData() { + if (modified) { +// write(); + modified = false; + } + return super.getData(); } @Override @@ -256,7 +281,7 @@ public OggPacket write() { // Next is the number of comments int numComments = 0; - for(List c : comments.values()) { + for (List c : comments.values()) { numComments += c.size(); } IOUtils.writeInt4(baos, numComments); @@ -265,8 +290,8 @@ public OggPacket write() { // an order, unit testing does! String[] tags = comments.keySet().toArray(new String[comments.size()]); Arrays.sort(tags); - for(String tag : tags) { - for(String value : comments.get(tag)) { + for (String tag : tags) { + for (String value : comments.get(tag)) { String comment = tag + '=' + value; IOUtils.writeUTF8WithLength(baos, comment); @@ -275,7 +300,7 @@ public OggPacket write() { // Do a header, if required for the format populateMetadataFooter(baos); - } catch(IOException e) { + } catch (IOException e) { // Should never happen! throw new RuntimeException(e); } diff --git a/core/src/test/java/org/gagravarr/flac/TestFlacComments.java b/core/src/test/java/org/gagravarr/flac/TestFlacComments.java index 7584168..20d0b65 100644 --- a/core/src/test/java/org/gagravarr/flac/TestFlacComments.java +++ b/core/src/test/java/org/gagravarr/flac/TestFlacComments.java @@ -15,6 +15,10 @@ import java.io.IOException; import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; import junit.framework.TestCase; @@ -25,6 +29,7 @@ public class TestFlacComments extends TestCase { private InputStream getTestOggFile() throws IOException { return this.getClass().getResourceAsStream("/testFLAC.oga"); } + private InputStream getTestFlacFile() throws IOException { return this.getClass().getResourceAsStream("/testFLAC.flac"); } @@ -52,12 +57,40 @@ public void testReadOgg() throws IOException { flac.close(); ogg.close(); } + public void testReadFlac() throws IOException { FlacNativeFile flac = new FlacNativeFile(getTestFlacFile()); doTestComments(flac.getTags()); flac.close(); } + public void testWriteFlac() throws IOException { + Path tempFile = Files.createTempFile("test", ".flac"); + try (FlacNativeFile flac = new FlacNativeFile(getTestFlacFile())) { + FlacTags tags = flac.getTags(); + doTestComments(tags); + String newComment = "new_comment"; + String someText = "some text"; + tags.addComment(newComment, someText); + + try (InputStream inputStream = flac.getInputStream()) { + Files.copy(inputStream, tempFile, StandardCopyOption.REPLACE_EXISTING); + } + + try (FlacNativeFile changed = new FlacNativeFile(Files.newInputStream(tempFile))) { + FlacTags newTags = changed.getTags(); + assertEquals(1, newTags.getComments(newComment).size()); + assertEquals(someText, newTags.getComments(newComment).get(0)); + } + + + } finally { +// Files.deleteIfExists(tempFile); + Files.copy(tempFile, Paths.get("/Users/valenpo/Developer/logs/flac/stream.flac"), StandardCopyOption.REPLACE_EXISTING); + System.out.println(tempFile); + } + } + private void doTestComments(FlacTags tags) { assertEquals("reference libFLAC 1.2.1 20070917", tags.getVendor());