Implementing audio and effects

Implementing audio

The Player class is used for playing samples. On Series 40 devices, a maximum of eight players are allowed at the same time, and only some of the players can play simultaneously because of memory restrictions. Due to these limitations, the MIDlet must monitor the number of players created and how many of them are playing.

While the Drumkit MIDlet demonstrates how to manage audio in a low-latency application, in the Explonoid MIDlet the latency is a minor issue. Of course, it is convenient that a sample plays fairly fast, but the MIDlet does not require the audio handling to be implemented in exactly the same way to reach sufficient results. Basically, if the sample must play as fast as possible, the MIDlet must create a player and prefetch the sample in advance. However, as the Explonoid MIDlet has more than eight samples to be played, you cannot reserve one player per sample or predict which sample is played next. Consequently, the simple solution is to create a new player every time the MIDlet needs to play a sample. If there are too many players already playing, the one that has been playing the longest is stopped and deallocated. Notice that the audio is played in a separate thread for optimization. Below is the thread loop that plays the contents of a synchronized playlist every 100 ms. The sample is wrapped in a PlayItem class, which contains the player, sample filename and a flag for looping the sample. If the sample needs to be looped, it is flagged and given a PlayerListener for END_OF_MEDIA events. The PlayerListener is implemented by the AudioManager.

public void run() {
        while (true) {
            if (audioEnabled) {
                Enumeration e = null;
                PlayItem[] queue;
                synchronized (playBuffer) {
                    queue = new PlayItem[playBuffer.size()];
                    e = playBuffer.elements();
                    for (int i = 0; i < playBuffer.size(); i++) {
                        queue[i] = (PlayItem) e.nextElement();
                    }
                    playBuffer.removeAllElements();
                }
                for (int i = 0; i < queue.length; i++) {
                    PlayItem playItem = queue[i];
                    if (players[index] != null) {                        
                        stop(players[index].getPlayer());
                    }
                    players[index] = playItem;
                    index++;
                    if (index >= MAX_PLAYERS) {
                        index = 0;
                    }

                    try {
                        InputStream stream = AudioManager.class.getResourceAsStream(playItem.getFilename());
                        playItem.setPlayer(Manager.createPlayer(stream, "audio/mp3"));
                        playItem.getPlayer().start();
                        if (playItem.looped) {
                            playItem.getPlayer().addPlayerListener(this);
                        }
                    } catch (IOException ex) {
                        ex.printStackTrace();
                    } catch (MediaException ex) {
                        ex.printStackTrace();
                    } catch (RuntimeException rex) {
                        rex.getMessage();
                        rex.printStackTrace();
                    }
                }
            }
            try {
                Thread.sleep(100);
            } catch (InterruptedException ex) {
                ex.printStackTrace();
            }
        }
    }

The playSample() –method simply creates a new PlayItem and adds it to the end of the synchronized playBuffer. If the playBuffer size exceeds the amount of max players allowed, the first item is removed from the list.

    public void playSample(final String filename) {
        PlayItem playItem = new PlayItem(filename, false);
        synchronized (playBuffer) {
            playBuffer.addElement(playItem);
            while (playBuffer.size() >= MAX_PLAYERS) {
                playBuffer.removeElementAt(0);
            }
        }
    }

To loop a sample, you can use the player.setLoopCount(-1) method. However, it does not always result in an indefinite loop. Listening to the END_OF_MEDIA event and then starting the player again manually seems to be a more reliable way to implement looping. The loopSample(String filename) –method is similar to the playSample methods, with the difference that is sets the flag for looping to true.

    public boolean loopSample(String filename) {
        PlayItem playItem = new PlayItem(filename, true);
        synchronized (playBuffer) {
            if (playBuffer.size() < MAX_PLAYERS) {
                playBuffer.addElement(playItem);
            }
        }
        return true;
    }

Implementing effects

To make the Explonoid MIDlet look better than just an Arkanoid clone, it contains some additional effects, such as vibrations and the following:

  • Spark bursts

    Every time the ball hits a brick, the brick explodes with a spark burst. The spark burst consists of Spark sprites that hold a fade-out animation. When shooting sparks, each spark gets a random velocity and direction and the animation is started from a random place.

  • Shaking

    Shaking is a simple Timer-based effect, which shakes the game area with a given magnitude and subsides as the time goes on.

  • Shock waves

    Shock waves occur when the ball slips past the plate. They are basically just ellipses that grow every time they are painted and disappear after their maximum size has been reached.

  • Electrical arcs

    Electrical arcs occur in the menu view when the user touches the screen. The arcs are randomly generated from the pointer coordinates outward.

Figure: Explonoid effects

When handling effects, you must consider memory as well. The garbage collector cannot always keep up if the MIDlet generates new objects too fast. To prevent problems, for example, with spark bursts, limit the maximum number of sparks and reuse the same spark instances over and over again.