Play MP3 or OGG using javax.sound.sampled, mp3spi, vorbisspi

I tried to come up with the simplest possible way of writing a Java class that can play mp3 and ogg files, using standard Java Sound APIs, with purely Open Source libraries from the public Maven Central repositories.

The LGPL-licensed mp3spi and vorbisspi libraries from javazoom.net satisfy these requirements and worked for me right away. As service provider implementations (SPI), they transparently add support for the mp3 and ogg audio formats to javax.sound.sampled, simply by being in the classpath.

For my AudioFilePlayer class below I basically took the example code from javazoom and simplified it as much as possible. Please note that it requires Java 7 as it uses try-with-resources.

Maven dependencies

  <!-- 
    We have to explicitly instruct Maven to use tritonus-share 0.3.7-2 
    and NOT 0.3.7-1, otherwise vorbisspi won't work.
   -->
<dependency>
  <groupId>com.googlecode.soundlibs</groupId>
  <artifactId>tritonus-share</artifactId>
  <version>0.3.7-2</version>
</dependency>
<dependency>
  <groupId>com.googlecode.soundlibs</groupId>
  <artifactId>mp3spi</artifactId>
  <version>1.9.5-1</version>
</dependency>
<dependency>
  <groupId>com.googlecode.soundlibs</groupId>
  <artifactId>vorbisspi</artifactId>
  <version>1.0.3-1</version>
</dependency>

AudioFilePlayer.java

package net.doepner.audio;

import java.io.File;
import java.io.IOException;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine.Info;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.UnsupportedAudioFileException;

import static javax.sound.sampled.AudioSystem.getAudioInputStream;
import static javax.sound.sampled.AudioFormat.Encoding.PCM_SIGNED;

public class AudioFilePlayer {

    public static void main(String[] args) {
        final AudioFilePlayer player = new AudioFilePlayer ();
        player.play("something.mp3");
        player.play("something.ogg");
    }

    public void play(String filePath) {
        final File file = new File(filePath);

        try (final AudioInputStream in = getAudioInputStream(file)) {
            
            final AudioFormat outFormat = getOutFormat(in.getFormat());
            final Info info = new Info(SourceDataLine.class, outFormat);

            try (final SourceDataLine line =
                     (SourceDataLine) AudioSystem.getLine(info)) {

                if (line != null) {
                    line.open(outFormat);
                    line.start();
                    stream(getAudioInputStream(outFormat, in), line);
                    line.drain();
                    line.stop();
                }
            }

        } catch (UnsupportedAudioFileException 
               | LineUnavailableException 
               | IOException e) {
            throw new IllegalStateException(e);
        }
    }

    private AudioFormat getOutFormat(AudioFormat inFormat) {
        final int ch = inFormat.getChannels();
        final float rate = inFormat.getSampleRate();
        return new AudioFormat(PCM_SIGNED, rate, 16, ch, ch * 2, rate, false);
    }

    private void stream(AudioInputStream in, SourceDataLine line) 
        throws IOException {
        final byte[] buffer = new byte[65536];
        for (int n = 0; n != -1; n = in.read(buffer, 0, buffer.length)) {
            line.write(buffer, 0, n);
        }
    }
}

Minimal pom.xml for executable jar

When you develop a stand-alone Java application, Maven can create the executable jar for you, with main-class and classpath manifest entries. You just need the configuration of the maven-jar-plugin shown below.

The sample pom.xml also specifies that we use Java 7 and UTF-8. You can take out the sourceEncoding and maven-compiler-plugin configurations, if you want to go with defaults instead.

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>

    <groupId>net.doepner</groupId>
    <artifactId>executable-jar-sample</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.0</version>
                <configuration>
                    <source>1.7</source>
                    <target>1.7</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>2.4</version>
                <configuration>
                    <archive>
                        <manifest>
                            <addClasspath>true</addClasspath>
                            <mainClass>net.doepner.sample.Main</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

Here is a matching minimal main class, src/main/java/net/doepner/sample/Main.java:

package net.doepner.sample;

import javax.swing.JOptionPane;

/**
 * The class that has the main method
 */
public class Main {

    public static void main(String[] args) {
        JOptionPane.showMessageDialog(null, "It works!");
    }
}

Maven integration (m2e) for Eclipse 3.7.x (Indigo)

Sonatype’s Maven integration for Eclipse (m2e) has been migrated to eclipse.org and is now available from the default update site for Indigo.

This means that you no longer have to add any Sonatype update sites as mentioned on the old (now outdated) m2eclipse site.

In particular, this also means that the old “m2e-extras” update site is obsolete for Eclipse 3.7.x and later. Things like WTP integration or Subclipse integration are now available as “m2e connectors” through Window – Preferences – Maven – Discovery.

All of this is very poorly (or not at all) documented on the new m2e home page and some people had problems with these changes.

Generate and display Maven Build timestamp in WAR

On all the pages of my webapp I want to see when the WAR was built (i.e. a build timestamp). Here is how I did it:

Add this to the pom.xml of your webapp module:

<build>
  <resources>
    <resource>
      <directory>src/main/resources</directory>
      <filtering>true</filtering>
    </resource>
  </resources>
</build> 

<properties>
  <mavenBuildTimestamp>${maven.build.timestamp}</mavenBuildTimestamp>
</properties>

Create a file src/main/resources/build.properties in  in your webapp module with the following content:

# Build Time Information
build.timestamp=${mavenBuildTimestamp}

The Maven build will replace the Maven property and the resulting file WEB-INF/classes/build.properties will look like this:

# Build Time Information
build.timestamp=20101016-0303

Now we just need a Spring PropertiesFactoryBean definition to make properties available at runtime:

<bean id="appProperties" class="org.springframework.beans.factory.config.PropertiesFactoryBean">
  <property name="locations">
    <list>
      <value>classpath:build.properties</value>
    </list>
  </property>
</bean>

I used a simple Spring bean to make the timestamp easily available in Spring Web Flow / Spring Faces EL expressions:

@Component
public class ViewUtil {

  private Properties appProperties;

  /**
   * @param appProperties Global Application properties
   */
  @Resource
  public void setAppProperties(Properties appProperties) {
    this.appProperties = appProperties;
  }

  /**
   * @return The Build Timestamp as generated by Maven
   */
  public String getBuildTimestamp() {
    return appProperties.getProperty("build.timestamp", "UNKNOWN");
  }
}

The EL expression code in the JSF page is then something like this:

Build timestamp: #{viewUtil.buildTimestamp} 

And this is what the result looks like on the page:

Build Timestamp screenshot

If you want to be able to refer to properties directly by name in your Spring configuration you can define a Spring PropertyPlaceholderConfigurer:

<bean id="propertyConfigurer"
      class="org.springframework.web.context.support.ServletContextPropertyPlaceholderConfigurer">
  <property name="properties" ref="appProperties" />
</bean>

Maven ‘skip tests’ & test-jar dependency errors

If you use test-jar dependencies in a multi-module Maven project (see my previous posting) you will run into a problem when you run Maven install with the maven.test.skip option turned on.

Problem

Maven looks for dependencies on test-jars even when maven.test.skip is turned on. This posting on the Maven issue tracker site perfectly summarizes the problem.

The maven.test.skip option (which is equivalent to the ‘Skip tests’ checkbox in IntelliJ’s Maven Runner options) prevents test code compilation, packaging and execution.

Hence no test-jars are built and that leads to the dependency error described above.

Solution

Disable the ‘Skip Tests’ checkbox in IntelliJ and INSTEAD add a property skipTests with value true.

This causes all test code to be built (and packaged as test-jars – when configured as described in my last posting) and only disables the execution of the tests.

Maven test execution options

maven.test.skip
Disables both running the tests and compiling the tests.

skipTests
Set this to true to skip running tests, but still compile them

Maven test-jar and test code reuse

In a multi module Maven project you might want to reuse test-related code (in src/test) across modules.

The recommended way is described here:
http://maven.apache.org/guides/mini/guide-attached-tests.html

You might want to exclude test classes from test-jar, to prevent that they run multiple times. Add this to your maven-jar-plugin definition (right after the <goals>..</goals> section) :

<configuration>
    <excludes>
        <exclude>**/*Test.class</exclude>
    </excludes>
</configuration>

This assumes that your test classes and only your test classes are named “…Test”.

More info on maven-jar-plugin configuration options for the test-jar goal:
http://maven.apache.org/plugins/maven-jar-plugin/test-jar-mojo.html