Triggering audio samples

The MIDlet uses regular uncompressed audio samples in 8-bit mono WAVE format with a sample rate of 44100 Hz. On a Series 40 devices, the JAR file size limit enforces some constraints, but compressed audio files, such as MP3, are not ideal for this kind of application, since they require a short silence in the beginning of the audio file. Also, with WAVE files, it is possible to have better sounding samples.

Creating Players

The audio files are played back using Players. A Player can be created using the Manager object with the method createPlayer. The createPlayer method takes an InputStream object and the media format as parameters. During the process, an IOException or MediaException can occur, which need to be caught. Here is an example how the snare.wav sample could be loaded:

try {
    InputStream is = this.getClass().getResourceAsStream("/snare.wav");
    Player snare = Manager.createPlayer(is, "audio/wav");
    snare.prefetch();
} catch (IOException ioe) {
    // ...
} catch (MediaException me) {
    // ...
}

The sample can then be played with the following code:

try {
    snare.setMediaTime(0);
    snare.start();
} catch (MediaException me) {
    // ...
}

Handling multiple sound sources

In Java ME, the number of prefetched audio samples and the number of currently playing samples is limited. The prefetchedPlayers and startedPlayers vectors keep track of which sounds are loaded and playing, so that when a new sound needs to be loaded or played, the least recently used sound is discarded:

    private static int maxPrefetchedPlayers = 8;
    private static int maxStartedPlayers = supportsMixing() ? 3 : 1;
    private static final Vector prefetchedPlayers = new Vector();
    private static final Vector startedPlayers = new Vector();

The supportsMixing method determines whether the device supports mixing:

    public static boolean supportsMixing() {
        String s = System.getProperty("supports.mixing");
        return s != null && s.equalsIgnoreCase("true") && !Main.isS60Phone();
    }

When a sound needs to be played, and the sound has already been prefetched, it is simply restarted. If the sound has not been prefetched, first the limitPrefetchedPlayers method is called to make room for a new sound in the prefetchedPlayers vector, and then a new Player is created and started. On some lower-end Nokia Asha devices, for example, the maximum number of prefetched Players is less than 8, which is considered the default. If the maximum is reached, the value of maxPrefetchedPlayers needs to be reduced accordingly; otherwise, some samples would not be played.

    public static void playSound(int sound) {
        if (restart(sound)) {
            return;
        }

        Player player = null;
        InputStream stream = SoundManager.class.getResourceAsStream(resources[sound]);
        try {
            limitPrefetchedPlayers();
            player = Manager.createPlayer(stream, "audio/wav");
            player.realize();
            player.prefetch();
            start(sound, player);
        }
        catch (Exception e) {
            // Either the device is in silent mode...
            if (prefetchedPlayers.isEmpty()) {
                Main.getInstance().sampleAlert();
            }
            // ...or does not support having all 8 players prefetched
            else if (maxPrefetchedPlayers > 1) {
                // Reduce amount of players and try again
                maxPrefetchedPlayers--;
                playSound(sound);
            }
        }
    }

The restart method tries to find the sound from the prefetchedPlayers vector. If the sound is found, the prefetchedPlayers vector is updated, so that the Players are in playing order in the vector and the found Player is restarted:

    private static boolean restart(int sound) {
        synchronized (prefetchedPlayers) {
            for (int i = 0; i < prefetchedPlayers.size(); i++) {
                SoundPlayer sp = (SoundPlayer) prefetchedPlayers.elementAt(i);
                if (sp.sound == sound) {
                    prefetchedPlayers.removeElement(sp);
                    prefetchedPlayers.addElement(sp);
                    stop(sp.player);
                    try {
                        sp.player.setMediaTime(0);
                    }
                    catch (Exception e) {
                    }
                    start(sp.player);
                    return true;
                }
            }
            return false;
        }
    }

The limitPrefetchedPlayers method cleans up Players, so that, after the method has run, there is room for a new Player:

    private static void limitPrefetchedPlayers() {
        synchronized (prefetchedPlayers) {
            try {
                while (prefetchedPlayers.size() >= maxPrefetchedPlayers) {
                    SoundPlayer sp = (SoundPlayer) prefetchedPlayers.firstElement();
                    clean(sp);
                }
            }
            catch (Exception e) {
            }
        }
    }

    // ...

    private static void clean(SoundPlayer sp) {
        synchronized (prefetchedPlayers) {
            prefetchedPlayers.removeElement(sp);
            stop(sp.player);
            try {
                sp.player.deallocate();
                sp.player.close();
            }
            catch (Exception e) {
            }
        }
    }

The first start method, which takes the sound and a new Player as parameters, adds the Player to the loadedPlayers vector and starts playback:

    private static void start(int sound, Player player) {
        synchronized (prefetchedPlayers) {
            prefetchedPlayers.addElement(new SoundPlayer(sound, player));
            start(player);
        }
    }

The second start method and the stop method control how many Players are concurrently playing:

    private static void start(Player player) {
        synchronized (startedPlayers) {
            try {
                while (startedPlayers.size() >= maxStartedPlayers) {
                    Player p = (Player) startedPlayers.firstElement();
                    startedPlayers.removeElementAt(0);
                    stop(p);
                }
            }
            catch (Exception e) {
            }
            startedPlayers.addElement(player);
            try {
                player.start();
            }
            catch (Exception e) {
            }
        }
    }

    // ...

    private static void stop(Player player) {
        synchronized (startedPlayers) {
            startedPlayers.removeElement(player);
            try {
                if (player.getState() == Player.STARTED) {
                    try {
                        player.stop();
                    }
                    catch (Exception e) {
                    }
                }
            }
            catch (Exception e) {
            }
        }
    }