Tagged!

I've been using the JAudiotagger library for a number of years now, to tag music files in bliss. It's stable and well supported, but while profiling the other day I noticed a potential performance improvement.

JAudiotagger is a Java API for tagging music files. When I first started work on bliss I chose this library for a number of reasons...

  • Excellent support for most of the common music file formats
  • A healthy community
  • Active development
  • Pure Java, with no nasty native bits

I was recently profiling bliss to see how I could cut down on IO and CPU usage to give an all round faster experience. I hooked up the excellent YourKit profiler. It showed me a lot of CPU time being spent in a JAudiotagger method, StandardArtwork.setImageFromData().

Here's the code:

/**
 * Should be called when you wish to prime the artwork for saving
 *
 * @return
 */
public boolean setImageFromData()
{
    try
    {
        BufferedImage image = (BufferedImage)getImage();
        setWidth(image.getWidth());
        setHeight(image.getHeight());
    }
    catch(IOException ioe)
    {
        return false;
    }
    return true;
}

The method is called by Tag implementations so that image metadata can be stored. Image metadata include such data as the width and the height of the image. Thus, the image is loaded using the Java Image I/O API and the width and height populated.

The problem is that the Java Image I/O API can be pretty slow, or at least, reading a full BufferedImage can be slow, depending on the image. Worse, consider this call is made on a per-tag, in other words per-file, basis. Common use of this method means setting the same cover art, resulting in a call to setImageFromData, for every track in a given album. This means the same image is loaded again and again.

I worked out a simple efficiency saving here. By passing in my own implementation of Artwork, I overrode the setImageFromData() method to do nothing, and I set the width, height and other metadata before I passed in the Artwork instance. Here's the new code (note: "artwork" is my own struct for holding image metadata and it contains information found from previously reading the image using the Java Image I/O API):

Artwork jatArtwork = new StandardArtwork() {
	@Override
	public boolean setImageFromData() {
		// do nothing, already primed and want to avoid overhead of loading the buffered image
		return true;
	}
};
try {
	openInputStream = artwork.openInputStream();
	if(openInputStream==null) LOG.warn("Attempting to read null " + InputStream.class.getSimpleName() + " from " + artwork.getSource() + ". Will write null artwork");
	jatArtwork.setBinaryData(openInputStream == null ? null : IOUtils.toByteArray(openInputStream));
} finally {
	if(openInputStream!=null) openInputStream.close();
}
jatArtwork.setDescription(artwork.getDescription());
jatArtwork.setMimeType(artwork.getMimeType());
jatArtwork.setPictureType(artwork.getPictureType());
jatArtwork.setImageUrl(artwork.getImageUrl());
jatArtwork.setLinked(artwork.isLinked());
if(!artwork.isLinked()) {
	jatArtwork.setWidth(artwork.getCoverArtLocation().getWidth());
	jatArtwork.setHeight(artwork.getCoverArtLocation().getHeight());
}

At least in terms of setting artwork, this means we create a BufferedImage O(1) times rather than O(N), a huge difference, especially on those larger albums!

An extra tip

Instead of loading image metadata using BufferedImage, look at the ImageReader classes. These are a lot faster for the simple tasks of reading width and height.

Here's my code to read image dimensions for InputStreams using this API ("Option" is my own type for representing optional values, similar to Scala's Option and just preferable to returning null):

public static Option<CoverArtDimension> dimensionsOf(InputStream is) throws IOException {
	
	if(is==null) return Option.none();
	
	ImageInputStream in = ImageIO.createImageInputStream(is);
	try {
		return read(in).map(new Option.Function<ImageReader, CoverArtDimension>() {
			@Override
			public CoverArtDimension run(ImageReader reader) {
				try {
					return new CoverArtDimension(reader.getWidth(0), reader.getHeight(0));
				} catch (IOException e) {
					throw Throwables.propagate(e);
				} finally {
					reader.dispose();
				}
			}
		});
	} finally {
		if (in != null)	in.close();
	}
}

/**
 * Return an {@link ImageReader} for the {@link ImageInputStream}, if one can be found.
 * Clients must dispose() of the {@link ImageReader} once they are finished with it. They
 * should also close the {@link ImageInputStream} they passed in.
 * @param in
 * @return
 * @throws IOException
 */
private static Option<ImageReader> read(ImageInputStream in) throws IOException {
	
	final Iterator readers = ImageIO.getImageReaders(in);
	while (readers.hasNext()) {
		ImageReader reader = (ImageReader) readers.next();
		reader.setInput(in);
		return Option.some(reader);
	}
	return Option.none();
}

HTH!

Thanks to JD Hancock for the image above.
comments powered by Disqus
© 2017 elsten software limited, Unit 4934, PO Box 6945, London, W1A 6US, UK | terms and conditions | privacy policy