14: Threads and Other Topics

What We Will Cover


Illuminations

Questions from last class?

Project Questions?

  1. You should have finished your project design by now
  2. By this week you should have completed the first implementation

14.1: Thread Fundamentals

Learner Outcomes

At the end of the lesson the student will be able to:

  • Explain the basic difference between a program that runs in a single thread and a program that runs under multiple threads.
  • Name three common reasons for using threads in a Java application.
  • List the three Java API classes or interfaces that have methods related to threading.
  • Use the Thread class or the Runnable interface to create a thread.
  • Explain the advantage of creating a thread by extending the Runnable interface rather than by inheriting the Thread class.
  • Use the interrupt() method and the InterruptedException class to create a thread that can be interrupted.

14.1.1: About Threads

Thread: a single sequential flow of control within a program

  • All Java programs run in one or more threads
  • Using threads, you can define tasks that act independently of each other
  • Each task can seemingly operate in parallel
  • A thread scheduler determines which thread to run next
  • Below is an example of threads operating in Java
    • Start each of the applets by clicking on them with the mouse
  • Each sort appears to be operating at the same time because of threads
    Bubble Sort Bi-Directional
    Bubble Sort
    Quick Sort
  • The program works because the thread scheduler allows each thread to run a short while
  • After a short time, a context switch occurs that changes which thread is running
  • Since context switches occur frequently, it appears that multiple tasks are happening at the same time

Typical Uses for Threads

  • To give the appearance of parallel processing
    • Like that shown above
  • To make use of multiple processors and perform tasks in parallel
    • Multiple-processor chips are becoming common
  • When there are many tasks to perform and one is often waiting
    • For example, I/O

thread tasks

  • To improve the responsiveness of a user interface
    • Allow time-consuming tasks to occur in the background

14.1.2: Classes and Interfaces for Working with Threads

  • There are two main classes and one interface commonly used for working with threads:

    Class/Interface Description
    Thread A class that defines a thread.
    Runnable An interface that must be implemented by the class of any object that is going to be executed by a thread.
    Object Several methods of the Object class are used for working with threads.
  • You can see the inheritance relationship of these classes and diagrams below:

    thread inheritance

  • The Runnable interface has just one method: run()
  • Any thread calls the run() method when it starts running
  • In addition, threads end when they complete the run() method
  • Constructors and methods of the Thread class are shown below
    • We will cover these methods in the following sections
  • In addition, methods of the Object class used for threading are shown
  • These methods are used for signal-wait synchronization

Commonly Used Constructors of the Thread Class

Constructor Description
Thread() Constructs a new Thread object with default settings.
Thread(Runnable) Constructs a new Thread object from any object that implements the Runnable interface
Thread(String) Constructs a new Thread object with the specified name.
Thread(Runnable, String) Constructs a new Thread object with the specified name from any object that implements the Runnable interface.

Commonly Used Methods of the Thread Class

Method Description
run() Called by the thread scheduler to run the thread. All subclasses of Thread should override this method.
start() Called to start the execution of the thread.
currentThread() A static method that returns a reference to the currently executing thread.
sleep(long millis) A static method that stops the current thread from executing for the specified number of milliseconds.
yield() Causes the currently executing thread object to temporarily pause and allow other threads to execute.
getName() Returns the name of the thread.
getState() Returns the state of the thread.
interrupt() Interrupts this thread.
isInterrupted() Tests whether this thread has been interrupted.
join() Waits for the thread on which it is called to finish and join the calling thread.
setPriority(int) Changes the priority of the thread.

Methods of the Object Class Used for Threading

Method Description
wait() Causes current thread to wait until another thread invokes the notify() method or the notifyAll() method for the current object.
notify() Wakes up a single arbitrary thread that is waiting on this object's monitor.
notifyAll() Wakes up all threads that are waiting on this object's monitor.

14.1.3: Using the sleep() Method

  • When you review the list of methods in the Thread class, you might notice the sleep() method
  • This method simply stops the current thread from running for a period of time
  • Since it is a static method, we can call it without a reference to the current Thread object
  • However, the method can throw an InterruptedException which we must handle
  • Thus, we could write a simple method to pause the current thread for a number of milliseconds
  • For example:
    public void pause(int milliseconds) {
        try {
            Thread.sleep(milliseconds);
        } catch (InterruptedException e) {
            System.out.println("Thread interrupted!");
            System.exit(1);
        }
    }
    
  • This might be useful for simple animation like that shown in the example below

Example of Simple Animation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;

// Unresponsive GUI
public class Bubbles extends JFrame
        implements ActionListener {
    public final static int X_LOC = 100, Y_LOC = 100,
                            WIDTH = 400, HEIGHT = 300;
    public static final int DELAY = 100;
    public static final int COLORS = 16581375;
    public static final int MAX_SIZE = 40;
    public static final int NUM_SHAPES = 400;

    private JButton startButton;
    private JButton stopButton;
    private JPanel canvas;

    public static void main(String[] args) {
        new Bubbles();
    }

    public Bubbles() {
        super("Bubbles Animation");
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        Container pane = getContentPane();

        canvas = new JPanel();
        pane.add(canvas, BorderLayout.CENTER);

        startButton = new JButton("Start");
        stopButton = new JButton("Stop");
        JPanel panel = new JPanel();
        panel.add(startButton);
        startButton.addActionListener(this);
        panel.add(stopButton);
        stopButton.addActionListener(this);
        pane.add(panel, BorderLayout.SOUTH);

        setBounds(X_LOC, Y_LOC, WIDTH, HEIGHT);
        setVisible(true);
    }

    public void actionPerformed(ActionEvent ae) {
        Object source = ae.getSource();
        if (source == startButton) {
            draw();
        } else if (source == stopButton) {
            System.out.println("Goodbye!");
            System.exit(0);
        }
    }

    public void draw() {
        Graphics g = canvas.getGraphics();
        Color c;
        for (int i = 0; i < NUM_SHAPES; i++) {
            int x1 = (int)(Math.random() * WIDTH);
            int y1 = (int)(Math.random() * HEIGHT);
            int size = (int)(Math.random() * MAX_SIZE);
            c = new Color((int)(Math.random() * COLORS));
            g.setColor(c);
            g.drawOval(x1, y1, size, size);
            pause(DELAY);
        }
    }

    public void pause(int milliseconds) {
        try {
            Thread.sleep(milliseconds);
        } catch (InterruptedException e) {
            System.out.println("Thread interrupted!");
            System.exit(0);
        }
    }
}

14.1.4: Subclassing Class Thread

  • When we run the previous example, we notice that we cannot use any of the controls until the animation is finished
  • To fix this nonresponsive interface, we can use a thread to run the animation in the background
  • This will free up the GUI thread and allow us to use the controls during the animation
  • There are two ways to implement a thread
    • Subclass Thread and override the run method
    • Implement the Runnable interface
  • The most straightforward way to implement threads is to subclass the Thread class
  • We implement the Thread subclass as an inner class to keep our animation class self-contained
    private class Bubbler extends Thread
    
  • The Thread class implements the run() method with an empty body:
    public void run() { }
  • Thus any subclass of Thread is expected to override the run() method
  • We put our animation code in the run() method since that is where the thread runs:
    public void run() {
        // animation code
    }
    
  • Note that we do not call run() directly
  • Instead, to start the thread, we call the start() method
    Bubbler bub = new Bubbler();
    bub.start();
    
  • The JVM calls run() after we call the start() method
    • This allows the JVM to set up the internal data structures needed to handle the thread
  • When start() is called, the thread starts executing independently

Example of Simple Animation Subclassing Thread

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;

// Multithreaded GUI
public class Bubbles2 extends JFrame
        implements ActionListener {
    public final static int X_LOC = 100, Y_LOC = 100,
                            WIDTH = 400, HEIGHT = 300;
    public static final int DELAY = 100;
    public static final int COLORS = 16581375;
    public static final int MAX_SIZE = 40;
    public static final int NUM_SHAPES = 400;

    private JButton startButton;
    private JButton stopButton;
    private JPanel canvas;

    public static void main(String[] args) {
        new Bubbles2();
    }

    public Bubbles2() {
        super("Bubbles Animation");
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        Container pane = getContentPane();

        canvas = new JPanel();
        pane.add(canvas, BorderLayout.CENTER);

        startButton = new JButton("Start");
        stopButton = new JButton("Stop");
        JPanel panel = new JPanel();
        panel.add(startButton);
        startButton.addActionListener(this);
        panel.add(stopButton);
        stopButton.addActionListener(this);
        pane.add(panel, BorderLayout.SOUTH);

        setBounds(X_LOC, Y_LOC, WIDTH, HEIGHT);
        setVisible(true);
    }

    public void actionPerformed(ActionEvent ae) {
        Object source = ae.getSource();
        if (source == startButton) {
            Bubbler bub = new Bubbler();
            bub.start();
        } else if (source == stopButton) {
            System.out.println("Goodbye!");
            System.exit(0);
        }
    }

    private class Bubbler extends Thread {

        public void run() {
            Graphics g = canvas.getGraphics();
            Color c;
            for (int i = 0; i < NUM_SHAPES; i++) {
                int x1 = (int)(Math.random() * WIDTH);
                int y1 = (int)(Math.random() * HEIGHT);
                int size = (int)(Math.random() * MAX_SIZE);
                c = new Color((int)(Math.random() * COLORS));
                g.setColor(c);
                g.drawOval(x1, y1, size, size);
                pause(DELAY);
            }
        }

        public void pause(int milliseconds) {
            try {
                Thread.sleep(milliseconds);
            } catch (InterruptedException e) {
                System.out.println("Thread interrupted!");
                System.exit(0);
            }
        }
    }
}

14.1.5: Implementing Interface Runnable

  • There are times when you would rather not make a thread class subclass Thread
  • The alternative is to make your class implement the Runnable interface
  • The Runnable interface has only one method to implement:
    public void run();
  • A class that implements Runnable must still run from an instance of class Thread
  • This is usually done by passing the Runnable object as an argument to a thread constructor
  • Here is a template of how to use Runnable:
    public class MyClass implements Runnable {
    
        public void run() {
            // code to run like when subclassing Thread
        }
    
        // Somewhere you start the thread
        Thread runner = new Thread(this);
        runner.start();
    }
    
  • Where:
    • MyClass is the name of the class to run in a separate thread
  • Note that this could be any reference to the MyClass object
  • We apply this template to our animation in the following example

Example of Simple Animation Implementing Runnable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;

// Multithreaded runable GUI
public class Bubbles3 extends JFrame
        implements ActionListener, Runnable {
    public final static int X_LOC = 100, Y_LOC = 100,
                            WIDTH = 400, HEIGHT = 300;
    public static final int DELAY = 100;
    public static final int COLORS = 16581375;
    public static final int MAX_SIZE = 40;
    public static final int NUM_SHAPES = 400;

    private JButton startButton;
    private JButton stopButton;
    private JPanel canvas;

    public static void main(String[] args) {
        new Bubbles3();
    }

    public Bubbles3() {
        super("Bubbles Animation");
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        Container pane = getContentPane();

        canvas = new JPanel();
        pane.add(canvas, BorderLayout.CENTER);

        startButton = new JButton("Start");
        stopButton = new JButton("Stop");
        JPanel panel = new JPanel();
        panel.add(startButton);
        startButton.addActionListener(this);
        panel.add(stopButton);
        stopButton.addActionListener(this);
        pane.add(panel, BorderLayout.SOUTH);

        setBounds(X_LOC, Y_LOC, WIDTH, HEIGHT);
        setVisible(true);
    }

    public void actionPerformed(ActionEvent ae) {
        Object source = ae.getSource();
        if (source == startButton) {
            Thread runner = new Thread(this);
            runner.start();
        } else if (source == stopButton) {
            System.out.println("Goodbye!");
            System.exit(0);
        }
    }

    public void run() {
        Graphics g = canvas.getGraphics();
        Color c;
        for (int i = 0; i < NUM_SHAPES; i++) {
            int x1 = (int)(Math.random() * WIDTH);
            int y1 = (int)(Math.random() * HEIGHT);
            int size = (int)(Math.random() * MAX_SIZE);
            c = new Color((int)(Math.random() * COLORS));
            g.setColor(c);
            g.drawOval(x1, y1, size, size);
            pause(DELAY);
        }
    }

    public void pause(int milliseconds) {
        try {
            Thread.sleep(milliseconds);
        } catch (InterruptedException e) {
            System.out.println("Thread interrupted!");
            System.exit(0);
        }
    }
}

14.1.6: Stopping Threads

  • Once a thread starts, how do you get it to stop?
  • The answer is that a thread should arrange for its own end
  • There is no reliable method that another thread can call to stop a thread
  • The natural way for a thread to end is to finish its run() method
  • To allow a thread to end prematurely, the thread needs to periodically check to see if it should end
  • An easy way is to use a boolean variable to control the thread like this:
    private volatile boolean done = false;
    
    public void run() {
        while (!done) {
            // perform actions repeatedly
        }
    }
    
    public void setDone() {
        done = true;
    }
    
  • Here we have the thread processing in a loop
  • As part of the loop condition, we check the boolean variable to see if we should continue
  • To tell the thread to stop, you call a method that sets the boolean variable
  • We can see an example of stopping a thread below

Why Use Volatile?

  • Threads can have local copies of variables to improve performance
  • The value of the local variable can be different than the "correct" value of the main memory variable
  • This is not a problem unless another thread needs to access the variable
  • Since we want other threads to be able to stop the animation thread, we need to ensure all threads access the same variable
  • Using volatile prohibits a variable from being copied to a thread's local memory
  • Thus changes to that variable by other threads will be seen by the animation thread

Interrupting a Thread

  • If your thread waits for long periods, the Thread class has an interrupt() method to wake the thread
  • Calling the interrupt() method either:
    • Sets the status of the thread to interrupted
    • Throws an exception if the thread is blocked (not runnable)
  • To check the interrupt status, you use the method isInterrupted()
  • However, if the thread is sleeping (blocked), it cannot execute the code that sets the interrupted status
  • Instead, the thread clears the interrupt status and throws an InterruptedException
  • Thus we must consider both cases and implement code to:
    • Stop a thread while executing
    • Stop a thread if blocked (not executing)
  • To do this, we create a boolean instance variable named done
    private volatile boolean done;
  • We redefine the Stop button so that whenever it is pressed we set the stop variable to true
    if (source == stopButton) {
        if (runner != null) {
            done = true; // first set variable
            runner.interrupt(); // then interrupt
        }
    }
    
  • Since the Stop button can be pressed without pressing the Start button, we protect against this case with an if statement as shown
  • Next we code the run() method to check for a stop condition each time through its loop:
    for (int i = 0; i < NUM_SHAPES && !done; i++)
    
  • In addition to these exceptions, there are others that may be thrown.
  • For our simple application, however, we will let these exceptions be caught by the general exception handler
  • The complete example is shown below

Example of Simple Animation With Stop Enabled

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;

// Stopable GUI
public class Bubbles4 extends JFrame
        implements ActionListener, Runnable {
    public final static int X_LOC = 100, Y_LOC = 100,
                            WIDTH = 400, HEIGHT = 300;
    public static final int DELAY = 100;
    public static final int COLORS = 16581375;
    public static final int MAX_SIZE = 40;
    public static final int NUM_SHAPES = 400;

    private JButton startButton;
    private JButton stopButton;
    private JPanel canvas;
    private Thread runner;
    private volatile boolean done;

    public static void main(String[] args) {
        new Bubbles4();
    }

    public Bubbles4() {
        super("Bubbles Animation");
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        Container pane = getContentPane();

        canvas = new JPanel();
        pane.add(canvas, BorderLayout.CENTER);

        startButton = new JButton("Start");
        stopButton = new JButton("Stop");
        JPanel panel = new JPanel();
        panel.add(startButton);
        startButton.addActionListener(this);
        panel.add(stopButton);
        stopButton.addActionListener(this);
        pane.add(panel, BorderLayout.SOUTH);

        setBounds(X_LOC, Y_LOC, WIDTH, HEIGHT);
        setVisible(true);
    }

    public void actionPerformed(ActionEvent ae) {
        Object source = ae.getSource();
        if (source == startButton) {
            done = false;
            runner = new Thread(this);
            runner.start();
        } else if (source == stopButton) {
            if (runner != null) {
                done = true; // first set variable
                runner.interrupt(); // then interrupt
            }
       }
    }

    public void run() {
        Graphics g = canvas.getGraphics();
        Color c;
        for (int i = 0; i < NUM_SHAPES && !done; i++) {
            int x1 = (int)(Math.random() * WIDTH);
            int y1 = (int)(Math.random() * HEIGHT);
            int size = (int)(Math.random() * MAX_SIZE);
            c = new Color((int)(Math.random() * COLORS));
            g.setColor(c);
            g.drawOval(x1, y1, size, size);
            pause(DELAY);
        }
        System.out.println("Finished making bubbles!");
    }

    public void pause(int milliseconds) {
        try {
            Thread.sleep(milliseconds);
        } catch (InterruptedException e) {
            System.out.println("Thread interrupted!");
        }
    }
}

More Information

14.1.7: Summary

  • A thread is a single flow of control through a program
  • Java allows the programmer to write programs with multiple threads
  • Using threads, you can define tasks that act independently of each other
  • A thread scheduler determines which thread to run next
  • Multithreading is typically used to:
    • To give the appearance of parallel processing
    • To make use of multiple processors and perform tasks in parallel
    • When there are many tasks to perform and one is often waiting
    • To improve the responsiveness of a user interface
  • You can create a thread by either:
    • Extending the Thread class and instantiating the new class
    • Implementing the Runnable interface and passing a reference of the object to a constructor of the Thread class
  • To start a thread, you call the start() method of the Thread class
    • You never call the run() method directly
  • Stopping a thread is more difficult than starting one
  • The way to end a thread is to finish its run() method
  • Each thread is responsible for checking itself periodically to see if it should end
  • You can use methods interrupt() and isInterrupted() to help with this task
  • However, if a thread is blocked, it cannot execute the code that sets the interrupted status
  • Instead, the thread clears the interrupt status and throws an InterruptedException
  • Thus, you must write code that works both if the thread is running and if the thread is blocked

Check Yourself

  1. What is a thread?
  2. What are three typical reasons for using threads? (answer)
  3. What is the basic difference between a program that runs in a single thread and a program that runs under multiple threads?
  4. What two techniques can you use to create a thread?
  5. How do you start a thread?
  6. How do you use the sleep() method of the Thread class?
  7. How can you tell which thread will run next?
  8. How do you stop a thread that is running?

Exercise 14.1

Take one minute to review the Check Yourself questions. We will discuss the questions as time permits.

14.2: Simple Animation

Learner Outcomes

At the end of the lesson the student will be able to:

  • Describe how computer animations are generated
  • Write code to load images into a list
  • Write code to display a sequence of images

14.2.1: Introduction to Computer Animation

  • Let us look at a simple use of threads for computer animation
  • Animation is the illusion of motion created by displaying a series of images
  • For example, the following animation displays at 10 frames per second (FPS)

    Animation example

  • The speed of the display is fast enough that you cannot easily see the individual frames
  • Contrast this with the following image that displays at 2 frames per second

    Slow animation example

  • At 2 FPS, the animation is slow enough that you can see the individual frames
  • Both of these animations were produced by displaying these images, known as frames:

    animation frames

  • Note that these images are in the public domain and were obtained from Wikipedia

Creating an Animation Loop

  • To create an animation, we use an animation loop
  • There are three steps to an animation loop as shown in the following diagram:

    update,render,sleep

  • During the update portion of the loop, you calculate the position of your shape
  • During the render portion, you draw the shape
  • Then you wait a short while before repeating the process
  • There are two reasons for waiting before repeating:
    1. To slow down the animation's frame rate
    2. To allow other parts of the program to run
  • The second reason is important but not always obvious
  • Whenever you create a thread, you need to stop running from time-to-time
  • Otherwise, especially on a single-processor system, other threads may not get a chance to run
  • To prevent starvation, a thread needs to pass control to other threads from time to time
  • The best time is when the current thread does not need to run

14.2.2: Coding an Animation

  • As an example of an animation, we can draw a circle and move it around the graphics window
  • Recall that to draw a simple filled circle we can use:
    graphicsObj.fillOval(x, y, width, height);
    
  • Where:
    • graphicsObj: the name of the graphics object
    • x: the x coordinate of the upper left corner
    • y: the y coordinate of the upper left corner
    • y: the width of the oval
    • y: the height of the oval
  • All the measurements are in pixels from the upper left-hand corner of the screen
  • If we make the width and height the same, we have a circle
  • For example:
    g.fillOval(x, y, DIAMETER, DIAMETER);
    
  • To create movement, we need to change the x and y coordinates over time
  • We control how much to change the location using two variables:
    private float dx; // delta x in pixels per loop
    private float dy; // delta y in pixels per loop
    
  • To keep our calculations accurate, we use floating-point numbers:
    private float x;  // x position in pixels
    private float y;  // y position in pixels
    
  • Every time we want to move our shape, we update the postiion using code like:
    x += dx;
    y += dy;
    
  • We will need to round the float variables to type int, which is easy with Math.round()
  • We can use a static import (see lesson 5.1.4) to shorten the method calls in the Math package:
    import static java.lang.Math.*;
    
  • Thus our call to fillOval() will look like:
    g.fillOval(round(x), round(y), DIAMETER, DIAMETER);
    
  • The following code shows a complete animation example
  • The animation loop is in the run() method
  • Within the loop are calls to:
    • update(): updates the position and state of shapes
    • repaint(): calls paintComponent() to render the shapes
    • Thread.sleep(DELAY): pauses the loop for DELAY milliseconds
  • To start the animation thread, we call the startAnimation() method
  • To stop the animation, we call the stopAnimation() method

Example Animation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
import static java.lang.Math.*;
import java.awt.*;
import javax.swing.*;

public class SimpleAnimation1 extends JPanel
        implements Runnable {
    public static final int X_LOC = 100, Y_LOC = 100,
                            WIDTH = 600, HEIGHT = 400;
    public static final int DELAY = 33;

    private Thread runner;            // for animation
    private volatile boolean running; // stop animation

    // Ball variables
    public static final int DIAMETER = 27;
    private float x;  // x position in pixels
    private float y;  // y position in pixels
    private float dx; // delta x in pixels per loop
    private float dy; // delta y in pixels per loop

    public static void main(String[] args) {
        JFrame frame = new JFrame("Animation Demo");
        frame.setDefaultCloseOperation(
            JFrame.EXIT_ON_CLOSE);

        SimpleAnimation1 sa = new SimpleAnimation1();
        frame.add(sa);

        frame.setBounds(X_LOC, Y_LOC, WIDTH, HEIGHT);
        frame.setVisible(true);

        sa.startAnimation();
    }

    public SimpleAnimation1() {
        setBackground(Color.WHITE);
        y = 100;
        dx = 3.5f;
        dy = 0;
    }

    // Initialise and start the animation
    public void startAnimation() {
        if (runner == null || !running) {
            runner = new Thread(this);
            runner.start();
        }
    }

    // Called to stop execution
    public void stopAnimation() {
        running = false;
    }

    // Repeatedly update, render, sleep
    public void run() {
        running = true;
        while (running) {
            update();  // update position
            repaint(); // render
            try {
                Thread.sleep(DELAY); // pause
            } catch(InterruptedException ie) {
                running = false;
            }
        }
    }

    // Update the animation state
    public void update() {
        x += dx;
        y += dy;
    }

    // Render the animation
    public void paintComponent(Graphics g) {
        super.paintComponent(g); // paint background
        g.setColor(Color.RED);
        g.fillOval(round(x), round(y), DIAMETER, DIAMETER);
    }
}

14.2.3: More About Starting and Stopping the Animation

  • Note the following technical details about how we start and stop the animation
  • To start the animation, we first declare a Thread variable
    private Thread runner;
  • In addition, we declare a variable to control when to stop the animation:
    private volatile boolean running;
    
  • The thread startup commands are placed inside a method to make starting easy to use:
    private void startAnimation() {
        if (runner == null || !running) {
            runner = new Thread(this);
            runner.start();
        }
    }
    
  • We make sure we do not accidentally start a new thread when one is already running by using an if-statement

Stopping the Animation

  • To make stopping the animation easy, you call the stopAnimation() method
    public void stopAnimation() {
        running = false;
    }
    
  • The user interface, which runs in another thread, can call the stop() method while the animation thread is running
  • This will set the boolean variable running to false at the same time the animation thread is executing
  • Once a program contains two or more threads utilizing a shared variable, data structure, or resource, we need to worry about thread synchronization
  • For example, what will happen if a shared item is changed by one thread at the same moment that the other one reads it?
  • The Java Memory Model (JMM) states that accesses and updates to all variables, other than longs or doubles, are atomic (indivisible)
  • This means that one thread cannot write to a boolean variable while another thread reads the variable
  • Thus our stop() method is thread safe since we are only relying on an atomic operation

14.2.4: Bouncing off the Walls

  • To make our animation more interesting, we can bounce the ball off the walls
  • Imitating a real thing or process, like a bouncing ball, is known as a simulation
  • One use of computers is simulating, or modeling, key characteristic of systems
  • For our simulation, we use the sides, top and bottom of the window as the walls
  • Remember that our coordinate system in the upper left-hand area of the window:

Screen Coordinates

  • The upper limits of the graphics window are available from the JPanel by calling:
  • To test if the ball exceeds the boundaries in the x-direction, we use an if-else statement like:
    if (x < 0) {
        dx = abs(dx);
    } else if (x + DIAMETER > getWidth()) {
        dx = -abs(dx);
    }
    
  • If the ball exceeds the boundaries we reverse its direction
  • Similarly, we test if the ball exceeds the top and bottom boundaries with:
    if (y < 0) {
        dy = abs(dy);
    } else if (y + DIAMETER > getHeight()) {
        dy = -abs(dy);
    }
    
  • The bounce code goes in the update section of the animation loop as shown below

Example Animation With Bounce

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
import static java.lang.Math.*;
import java.awt.*;
import javax.swing.*;

public class SimpleAnimation2 extends JPanel
        implements Runnable {
    public static final int X_LOC = 100, Y_LOC = 100,
                            WIDTH = 600, HEIGHT = 400;
    public static final int DELAY = 33;

    private Thread runner;            // for animation
    private volatile boolean running; // stop animation

    // Ball variables
    public static final int DIAMETER = 27;
    private float x;  // x position in pixels
    private float y;  // y position in pixels
    private float dx; // delta x in pixels per loop
    private float dy; // delta y in pixels per loop

    public static void main(String[] args) {
        JFrame frame = new JFrame("Animation Demo");
        frame.setDefaultCloseOperation(
            JFrame.EXIT_ON_CLOSE);

        SimpleAnimation2 sa = new SimpleAnimation2();
        frame.add(sa);

        frame.setBounds(X_LOC, Y_LOC, WIDTH, HEIGHT);
        frame.setVisible(true);

        sa.startAnimation();
    }

    public SimpleAnimation2() {
        setBackground(Color.WHITE);
        dx = (float) random() * 2 + 3;
        dy = (float) random() * 2 + 3;
    }

    // Initialise and start the animation
    public void startAnimation() {
        if (runner == null || !running) {
            runner = new Thread(this);
            runner.start();
        }
    }

    // Called to stop execution
    public void stopAnimation() {
        running = false;
    }

    // Repeatedly update, render, sleep
    public void run() {
        running = true;
        while (running) {
            update();  // update position
            repaint(); // render
            try {
                Thread.sleep(DELAY); // pause
            } catch(InterruptedException ie) {
                running = false;
            }
        }
    }

    // Update the animation state
    public void update() {
        if (x < 0) {
            dx = abs(dx);
        } else if (x + DIAMETER > getWidth()) {
            dx = -abs(dx);
        }
        if (y < 0) {
            dy = abs(dy);
        } else if (y + DIAMETER > getHeight()) {
            dy = -abs(dy);
        }
        x += dx;
        y += dy;
    }

    // Render the animation
    public void paintComponent(Graphics g) {
        super.paintComponent(g); // paint background
        g.setColor(Color.RED);
        g.fillOval(round(x), round(y), DIAMETER, DIAMETER);
    }
}

14.2.5: Animating Two Objects

  • If we want to animate two shapes, we need separate variables for each object
  • As we add more shape objects, our code in the animation loop becomes more cluttered
  • To avoid the clutter and duplication, we can encapsulate the code for the shape in a class
  • Here is a Ball class that encapsulates the information for the moving shape
  • Following the Ball class is an animation application bouncing two balls
  • Notice how simple the update() and paintComponent() methods remain
  • Also notice how we pass a reference to the drawing area to the Ball class
  • This allows resizing the drawing area while the application is running
  • In addition, we pass a Color argument to the Ball class so it can draw its own color

Ball Class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import static java.lang.Math.*;
import java.awt.*;

public class Ball {
    public static final int DIAMETER = 27;
    private float x;  // x position in pixels
    private float y;  // y position in pixels
    private float dx; // delta x in pixels per loop
    private float dy; // delta y in pixels per loop
    private Component canvas;
    private Color color;

    public Ball(Component panel, Color c) {
        canvas = panel;
        color = c;
        dx = (float) random() * 2 + 3;
        dy = (float) random() * 2 + 3;
    }

    // Update state
    public void update() {
        if (x < 0) {
            dx = abs(dx);
        } else if (x + DIAMETER > canvas.getWidth()) {
            dx = -abs(dx);
        }
        if (y < 0) {
            dy = abs(dy);
        } else if (y + DIAMETER > canvas.getHeight()) {
            dy = -abs(dy);
        }
        x += dx;
        y += dy;
    }

    // Render
    public void draw(Graphics g) {
        g.setColor(color);
        g.fillOval(round(x), round(y), DIAMETER, DIAMETER);
    }
}

Example Animation with Two Balls

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
import java.awt.*;
import javax.swing.*;

public class SimpleAnimation3 extends JPanel
        implements Runnable {
    public static final int X_LOC = 100, Y_LOC = 100,
                            WIDTH = 600, HEIGHT = 400;
    public static final int DELAY = 33;

    private Thread runner;            // for animation
    private volatile boolean running; // stop animation

    // Ball variables
    private Ball b1;
    private Ball b2;

    public static void main(String[] args) {
        JFrame frame = new JFrame("Animation Demo");
        frame.setDefaultCloseOperation(
            JFrame.EXIT_ON_CLOSE);

        SimpleAnimation3 sa = new SimpleAnimation3();
        frame.add(sa);

        frame.setBounds(X_LOC, Y_LOC, WIDTH, HEIGHT);
        frame.setVisible(true);

        sa.startAnimation();
    }

    public SimpleAnimation3() {
        setBackground(Color.WHITE);
        b1 = new Ball(this, Color.RED);
        b2 = new Ball(this, Color.BLUE);
    }

    // Initialise and start the animation
    public void startAnimation() {
        if (runner == null || !running) {
            runner = new Thread(this);
            runner.start();
        }
    }

    // Called to stop execution
    public void stopAnimation() {
        running = false;
    }

    // Repeatedly update, render, sleep
    public void run() {
        running = true;
        while (running) {
            update();  // update position
            repaint(); // render
            try {
                Thread.sleep(DELAY); // pause
            } catch(InterruptedException ie) {
                running = false;
            }
        }
    }

    // Update the animation state
    public void update() {
        b1.update();
        b2.update();
    }

    // Render the animation
    public void paintComponent(Graphics g) {
        super.paintComponent(g); // paint background
        b1.draw(g);
        b2.draw(g);
    }
}

14.2.6: Animating Many Objects

  • We can take our animation one step further and animate many objects
  • To juggle several balls at once we use a list, such as an array or ArrayList, with a counting loop
  • You can see the ArrayList and loops in the following example

Example Animation with Many Balls

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
import java.awt.*;
import java.util.*;
import javax.swing.*;

public class SimpleAnimation4 extends JPanel
        implements Runnable {
    public static final int X_LOC = 100, Y_LOC = 100,
                            WIDTH = 600, HEIGHT = 400;
    public static final int DELAY = 33; // 30 FPS

    private Thread runner;            // for animation
    private volatile boolean running; // stop animation

    // Ball variables
    private static final int NUM_BALLS = 10;
    private ArrayList<Ball> balls;

    public static void main(String[] args) {
        JFrame frame = new JFrame("Animation Demo");
        frame.setDefaultCloseOperation(
            JFrame.EXIT_ON_CLOSE);

        SimpleAnimation4 sa = new SimpleAnimation4();
        frame.add(sa);

        frame.setBounds(X_LOC, Y_LOC, WIDTH, HEIGHT);
        frame.setVisible(true);

        sa.startAnimation();
    }

    public SimpleAnimation4() {
        setBackground(Color.WHITE);
        Random r = new Random();
        balls = new ArrayList<Ball>();
        for (int i = 0; i < NUM_BALLS; i++) {
            int red = r.nextInt(255);
            int green = r.nextInt(255);
            int blue = r.nextInt(255);
            Color c = new Color(red, green, blue);
            Ball b = new Ball(this, c);
            balls.add(b);
        }
    }

    // Initialise and start the animation
    public void startAnimation() {
        if (runner == null || !running) {
            runner = new Thread(this);
            runner.start();
        }
    }

    // Called to stop execution
    public void stopAnimation() {
        running = false;
    }

    // Repeatedly update, render, sleep
    public void run() {
        running = true;
        while (running) {
            update();  // update position
            repaint(); // render
            try {
                Thread.sleep(DELAY); // pause
            } catch(InterruptedException ie) {
                running = false;
            }
        }
    }

    // Update the animation state
    public void update() {
        for (int i = 0; i < balls.size(); i++) {
            Ball b = balls.get(i);
            b.update();
        }
    }

    // Render the animation
    public void paintComponent(Graphics g) {
        super.paintComponent(g); // paint background
        for (int i = 0; i < balls.size(); i++) {
            Ball b = balls.get(i);
            b.draw(g);
        }
    }
}

14.2.7: Animating Images

  • We can move images on the screen rather than a shape
  • To move images, we need to:
    1. Load images from a file into a suitable object
    2. Render the image in the animation loop
  • A good choice for storing an image in an object is BufferedImage
  • BufferedImage is a subclass of Image, and therefore you can use it in place of Image
  • Many methods of the Java API use either an Image or BufferedImage

Loading a BufferedImage from a File

  • Let us look at how we can load a GIF, PNG or JPEG file into a BufferedImage object
  • The easiest way to load a BufferedImage object is using ImageIO.read():
    BufferedImage im = ImageIO.read(fileName);
    
  • Note that ImageIO.read() is a static method that returns a BufferedImage object
  • Another advantage of using ImageIO.read() is that you can read additional image formats
  • To use this class and method, we need to import the following classes:
    import java.awt.image.BufferedImage;
    import java.io.IOException;
    import javax.imageio.ImageIO;
    

Loading Images from JAR Files

  • A JAR file is a way of packaging code and resources together into a single, compressed file
    • Like a ZIP file
  • Resources can be almost anything, including images and sounds
  • To load images from a JAR, you need to modify how the image is loaded:
    BufferedImage im =
        ImageIO.read(getClass().getResource(fileName));
    
  • To make loading a BufferedImage easy, we can write a loadImage() method like:
    private BufferedImage loadImage(String fileName) {
        BufferedImage im = null;
        try {
            im = ImageIO.read(getClass().getResource(fileName));
        } catch (IOException e) {
            System.out.println("Error loading " + fileName);
        }
        return im;
    }
    
  • Note the use of the try-catch statement
  • You need the try-catch statement because reading the image file can cause an error
    • For instance, if you try to read a file that does not exist
  • Note that this code will load from either a JAR or a regular file
  • Thus you might as well use the latter code to allow you the most flexibility storing your images

Rendering the Image

  • To display the images, you change the drawing command in the paintComponent() method:
    g.drawImage(image, round(x), round(y), null);
    
  • We make changes to the ball and animation classes as shown below
  • We can draw using this ball image: Ball image

Ball Class to Handle Images

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import static java.lang.Math.*;
import java.awt.*;
import java.awt.image.BufferedImage;

public class Ball2 {
    private float x;  // x position in pixels
    private float y;  // y position in pixels
    private float dx; // delta x in pixels per loop
    private float dy; // delta y in pixels per loop
    private Component canvas;
    private BufferedImage im;

    public Ball2(Component panel, BufferedImage image) {
        canvas = panel;
        im = image;
        dx = (float) random() * 2 + 3;
        dy = (float) random() * 2 + 3;
    }

    // Update state
    public void update() {
        if (x < 0) {
            dx = abs(dx);
        } else if (x + im.getWidth() > canvas.getWidth()) {
            dx = -abs(dx);
        }
        if (y < 0) {
            dy = abs(dy);
        } else if (y + im.getHeight() > canvas.getHeight()) {
            dy = -abs(dy);
        }
        x += dx;
        y += dy;
    }

    // Render
    public void draw(Graphics g) {
        g.drawImage(im, round(x), round(y), null);
    }
}

Example Animation with Images

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.*;
import javax.imageio.ImageIO;
import javax.swing.*;

public class SimpleAnimation5 extends JPanel
        implements Runnable {
    public static final int X_LOC = 100, Y_LOC = 100,
                            WIDTH = 600, HEIGHT = 400;
    public static final int DELAY = 33; // 30 FPS

    private Thread runner;            // for animation
    private volatile boolean running; // stop animation

    // Ball variables
    private static final int NUM_BALLS = 10;
    private ArrayList<Ball2> balls;

    public static void main(String[] args) {
        JFrame frame = new JFrame("Animation Demo");
        frame.setDefaultCloseOperation(
            JFrame.EXIT_ON_CLOSE);

        SimpleAnimation5 sa = new SimpleAnimation5();
        frame.add(sa);

        frame.setBounds(X_LOC, Y_LOC, WIDTH, HEIGHT);
        frame.setVisible(true);

        sa.startAnimation();
    }

    public SimpleAnimation5() {
        setBackground(Color.WHITE);
        balls = new ArrayList<Ball2>();
        BufferedImage im = loadImage("ball.gif");
        for (int i = 0; i < NUM_BALLS; i++) {
            Ball2 b = new Ball2(this, im);
            balls.add(b);
        }
    }

    private BufferedImage loadImage(String fileName) {
        BufferedImage im = null;
        try {
            im = ImageIO.read(getClass().getResource(fileName));
        } catch (IOException e) {
            System.out.println("Error loading " + fileName);
        }
        return im;
    }

    // Initialise and start the animation
    public void startAnimation() {
        if (runner == null || !running) {
            runner = new Thread(this);
            runner.start();
        }
    }

    // Called to stop execution
    public void stopAnimation() {
        running = false;
    }

    // Repeatedly update, render, sleep
    public void run() {
        running = true;
        while (running) {
            update();  // update position
            repaint(); // render
            try {
                Thread.sleep(DELAY); // pause
            } catch(InterruptedException ie) {
                running = false;
            }
        }
    }

    // Update the animation state
    public void update() {
        for (int i = 0; i < balls.size(); i++) {
            Ball2 b = balls.get(i);
            b.update();
        }
    }

    // Render the animation
    public void paintComponent(Graphics g) {
        super.paintComponent(g); // paint background
        for (int i = 0; i < balls.size(); i++) {
            Ball2 b = balls.get(i);
            b.draw(g);
        }
    }
}

14.2.8: Controlling the Animation

  • We can add controls to our animation with either GUI components or low-level keyboard and mouse event handing
  • We will show an example using keyboard and mouse event handling
  • The same principles apply when using components

Keyboard Control

  • We add the code to stop and exit the animation when the user presses the Escape key
  • To handle keyboard events, we use an inner class:
    private class KeyHandler extends KeyAdapter {
        public void keyPressed(KeyEvent e) {
            int keyCode = e.getKeyCode();
            if (keyCode == KeyEvent.VK_ESCAPE) {
                stopAnimation();
                System.out.println("Goodbye!");
                System.exit(0); // so enclosing JFrame exits
            }
        }
    }
    
  • To listen for keyboard events, we register a KeyHandler in the constructor of our animation class:
    addKeyListener(new KeyHandler());
    
  • One VERY IMPORTANT step is to request focus for the animation panel:
    sa.requestFocusInWindow(); // After visible
    
  • Without focus, the animation panel does not hear the keyboard

Mouse Control

  • We add code to stop and restart the animation with mouse clicks in the animation panel
  • To handle mouse events, we use an inner class:
    private class MouseHandler extends MouseAdapter {
        public void mouseClicked(MouseEvent e) {
            if (running) {
                stopAnimation();
            } else {
                startAnimation();
            }
        }
    }
    
  • We use the if statement and variable running to make a simple toggle
  • To listen for keyboard events, we register a KeyHandler in the constructor of our animation class:
    addMouseListener(new MouseHandler());
    
  • You can see the complete code in the following listing

Example Animation with Keyboard and Mouse Control

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.*;
import javax.imageio.ImageIO;
import javax.swing.*;

public class SimpleAnimation6 extends JPanel
        implements Runnable {
    public static final int X_LOC = 100, Y_LOC = 100,
                            WIDTH = 600, HEIGHT = 400;
    public static final int DELAY = 33; // 30 FPS

    private Thread runner;          // for animation
    private volatile boolean running; // stop animation

    // Ball variables
    private static final int NUM_BALLS = 10;
    private ArrayList<Ball2> balls;

    public static void main(String[] args) {
        JFrame frame = new JFrame("Animation Demo");
        frame.setDefaultCloseOperation(
            JFrame.EXIT_ON_CLOSE);

        SimpleAnimation6 sa = new SimpleAnimation6();
        frame.add(sa);

        frame.setBounds(X_LOC, Y_LOC, WIDTH, HEIGHT);
        frame.setVisible(true);

        sa.startAnimation();
        // Needed for keys to hear
        sa.requestFocusInWindow(); // After visible
    }

    public SimpleAnimation6() {
        addKeyListener(new KeyHandler());
        addMouseListener(new MouseHandler());
        setBackground(Color.WHITE);
        balls = new ArrayList<Ball2>();
        BufferedImage im = loadImage("ball.gif");
        for (int i = 0; i < NUM_BALLS; i++) {
            Ball2 b = new Ball2(this, im);
            balls.add(b);
        }
    }

    private BufferedImage loadImage(String fileName) {
        BufferedImage im = null;
        try {
            im = ImageIO.read(getClass().getResource(fileName));
        } catch (IOException e) {
            System.out.println("Error loading " + fileName);
        }
        return im;
    }

    // Initialise and start the animation
    public void startAnimation() {
        if (runner == null || !running) {
            runner = new Thread(this);
            runner.start();
        }
    }

    // Called to stop execution
    public void stopAnimation() {
        running = false;
    }

    // Repeatedly update, render, sleep
    public void run() {
        running = true;
        while (running) {
            update();  // update position
            repaint(); // render
            try {
                Thread.sleep(DELAY); // pause
            } catch(InterruptedException ie) {
                running = false;
            }
        }
    }

    // Update the animation state
    public void update() {
        for (int i = 0; i < balls.size(); i++) {
            Ball2 b = balls.get(i);
            b.update();
        }
    }

    // Render the animation
    public void paintComponent(Graphics g) {
        super.paintComponent(g); // paint background
        for (int i = 0; i < balls.size(); i++) {
            Ball2 b = balls.get(i);
            b.draw(g);
        }
    }

    private class KeyHandler extends KeyAdapter {
        public void keyPressed(KeyEvent e) {
            int keyCode = e.getKeyCode();
            if (keyCode == KeyEvent.VK_ESCAPE) {
                stopAnimation();
                System.out.println("Goodbye!");
                System.exit(0); // so enclosing JFrame exits
            }
        }
    }

    private class MouseHandler extends MouseAdapter {
        public void mouseClicked(MouseEvent e) {
            if (running) {
                stopAnimation();
            } else {
                startAnimation();
            }
        }
    }
}

14.2.9: Summary

  • In this section we looked at how to create computer animation
  • Animation is the illusion of motion created by displaying a series of images or shapes:

    example animation

  • To control the movement, we created an animation loop as shown in the following diagram:

    update,render,sleep

  • During the update portion of the loop, we calculated the position of our shape
  • During the render portion, we drew the shape
  • Then we waited a short while using Thread.sleep() to slow down the animation and prevent resource starvation
  • Computer animation draws the images or shapes at a specific place on your computer screen
  • As an example of animation, we drew a filled oval to simulate a bouncing ball:
    g.fillOval(x, y, DIAMETER, DIAMETER);
    
  • To create movement we used two variables:
    private float dx; // delta x in pixels per loop
    private float dy; // delta y in pixels per loop
    
  • During the update portion of the animation, we calcuated a new position using:
    x += dx;
    y += dy;
    
  • To better simulate a ball, we added code to test for "walls" (the edge of the drawing panel)
  • When the ball hit a wall, we reversed the dx- and dy-directions
  • If we add balls to the animation, the code in the animation loop becomes cluttered
  • To avoid the clutter, we created a Ball class to encapsulate the location, speed and behavior of a ball
  • We then looked at how to add several balls to the animation using a vector
  • In addition, we looked at how to animate images by:
    • Loading images from a file into a BufferedImage object
    • Rendering the image in the animation loop
  • Finally, we looked at how to control the image using keyboard and mouse controls

Check Yourself

  1. What is animation?
  2. What is meant by the term "animation loop"?
  3. What three operations are present in an animation loop?
  4. What code did we use to calculate position from one animation frame to the next?
  5. True or false? To reduce clutter, you can encapsulate the animated object into a class.
  6. What two programming constructs allow you to easily animate multiple objects?
  7. What object is used to store images loaded from a file?

Exercise 14.2

Take one minute to review the Check Yourself questions. We will review the questions as time permits.

14.3: Playing Sound Files

Learner Outcomes

At the end of the lesson the student will be able to:

  • Describe how sound is recorded in a digital format
  • Discuss the approaches you can use in Java to play sounds
  • Play sounds using the Java Sound API

14.3.1: Sound Basics

  • In this lesson we discuss how to play sound files in Java
  • Sound is a vibration through some medium transmitted as a wave
  • Faster vibrations create a higher sound frequency and you hear a higher pitch
  • The pressure of the sound wave creates an amplitude that makes sound louder or softer
  • This is shown in the following diagram:

    Sound waves

  • While sound begins as a series of waves, it can be converted to a digital format

Digitizing sound

  • Digital sound, or audio, is a series of discrete samples of the sound waves
  • The amount of samples stored per second is called the sample rate
  • CD audio, for instance, has a sample rate of 44.1 kHz
  • In general, higher sampling rates give a more accurate audio representation but also larger files sizes
  • The number of bits used to store the sample determines the number of variations in amplitude
  • If a sample is 16 bits, it has 65,536 possible amplitudes

14.3.2: The Java Sound API

  • Java has several different approaches to sound:
    • Applet play() method
    • AudioClip class
    • Java Sound API
    • Java Media Framework
  • A flexible approach is the Java Sound API, which is what we will focus on
  • The Java Sound API has two main parts:
  • We will start with sampled audio and discuss MIDI later

Sampled Audio

  • Java Sound can play sound formats with either 8- or 16-bit samples with sample rates from 8kHz to 48kHz
  • Also, it can play either mono or stereo sound
  • Additionally, you can install other format readers from third parties, such as an OGG decoder
  • Java Sound provides support for reading three sampled sound file formats: AIFF, AU, and WAV
  • All formats are very flexible, and it does not matter much which one you use
  • Below is an overview of the a typical audio architecture (image from the Java Tutorial):

    typical audio architecture

Getting Sounds

  • To play sampled sound, you will need some sound files
  • You can get free sound effects files from the Internet like:
  • However, make sure you verify the licensing
  • Also you can create your own sounds using sound programs on your computer
  • Some free sound editing programs you might try are:

More Information

14.3.3: Opening a Sound File

  • To load a sound file, you can use the AudioSystem class
  • The class has several static methods, of which we can use the getAudioInputStream() to open an audio stream
  • The getAudioInputStream() method is overloaded so you can open a sound stream from many different sources
  • For instance, you can open sound streams from files, URLs and other streams
  • The getAudioInputStream() methods return an AudioInputStream object, which we can use to read sound samples
  • An AudioInputStream also has a method getFormat() that returns an AudioFormat object
  • The AudioFormat class lets us get information about the format of the sounds such as:
    • Sample rate
    • Number of channels
    • Frame size
  • Frame size is the number of bytes required for every sample for every channel
  • For instance, 16-bit stereo sound has a frame size of 4 (2 bytes x 2 channels
  • The frame size is useful for calculating how many bytes it takes to store a sound in memory
  • For example, a 3-second sound with an audio format of 16-bit samples, stereo, 44.1kHz is:
    3 x 2 x 2 44,100 = 517KB
  • You could cut the size in half using mono sound instead of stereo

Example Code to Open a Sound File

  • Example code to open an AudioInputStream is shown below:
    File file = new File("sound.wav");
    AudioInputStream stream =
        AudioSystem.getAudioInputStream(file);
    
  • Note that the getAudioInputStream() method can throw a checked exception
  • I have not shown the required try-catch statement for handling the exception
  • Also, I have not included code for reading from a JAR file

More Information

14.3.4: Using a Line to Play a Sound Clip

  • After opening a sound stream we need to send it to the sound system
  • For this we send the sound stream to a Line
  • A Line is an interface to send or receive audio to or from the sound system
  • The Line interface has several subinterfaces, including the SourceDataLine
  • Here is the inheritance hierarchy for Line:

    Inheritance UML for Line

  • A SourceDataLine lets you write to the sound system
  • We create a Line by using the getLine() method of the AudioSystem class
  • We pass this method a Line.Info object, which specifies the type of Line you want to create
  • DataLine.Info is a subclass of Line.Info that we can use to create the Line

Clips

  • Another Line subinterface is a Clip
  • A Clip is convenient because it loads samples into memory and feeds them to the audio system automatically
  • The code to load a clip is shown below
  • Though a Clip is useful, it does have drawbacks
  • Java Sound limits you to a maximum of 32 open lines at one time
  • Since a Clip is a Line, this means you can open only a limited number of sounds
    • Even before we play any of them
  • Several clips can play at once, but each Clip can play only one sound at a time
  • For instance, if we want two or three explosions to play at one time, we need a separate Clip for each one
  • If we keep our sound requirements moderately simple, we can usually get by just using clips

Example Code to Play a Clip

  • Example code to load a Clip is shown below
  • Note that the methods can throw various checked exceptions
  • I have not shown the required try-catch statement for handling the exceptions
    // Get a Clip from the AudioSystem
    clip = AudioSystem.getClip(); // new in JDK 1.5
    // Load the samples from the stream
    clip.open(stream);
    // Begin playback of the sound clip
    clip.start();
    

14.3.5: Example Playing a Sound

  • Let us put the code snippets we saw above into a working program
  • We can use our previous animation to play the sounds
  • To give us a sound to play, we can use the following file:
  • The example code assumes the sound file is placed in a subdirectory named "sounds"

Updating the Animation Class

  • First we add a Clip to the animation class:
    private Clip clip;
    
  • Then in the constructor, we open the clip:
    try {
        File file = new File("sounds/blip.wav");
        AudioInputStream stream =
            AudioSystem.getAudioInputStream(file);
        clip = AudioSystem.getClip();
        clip.open(stream);
    } catch (IOException ex) {
        ex.printStackTrace();
    } catch (UnsupportedAudioFileException ex) {
        ex.printStackTrace();
    } catch (LineUnavailableException ex) {
        ex.printStackTrace();
    }
    
  • Then we add a playSound() method we can call to produce the sound:
    public void playSound() {
        clip.setFramePosition(0); // Allows replay
        clip.start();
    }
    
  • You can see the complete code listing below

Updating the Ball Class

  • In addition to updating the animation class, we need to update the ball class as well
  • We want to play a sound whenever the ball changes direction
  • First we need a reference to the animation object so we can call the play() method:
    private SimpleAnimation7 anim;
    
  • We can pass the reference in the constructor of the ball class:
    public Ball3(SimpleAnimation7 panel, BufferedImage image) {
        anim = panel;
        // more code here
    }
    
  • Whenever the ball changes direction, we call the playSound() method of the animation class:
    anim.playSound();
    
  • You can see the complete code listing below

Class SimpleAnimation7 with playSound() Method

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.*;
import javax.imageio.ImageIO;
import javax.sound.sampled.*;
import javax.swing.*;

public class SimpleAnimation7 extends JPanel
        implements Runnable {
    public static final int X_LOC = 100, Y_LOC = 100,
                            WIDTH = 600, HEIGHT = 400;
    public static final int DELAY = 33; // 30 FPS

    private Thread runner;          // for animation
    private volatile boolean running; // stop animation
    private Clip clip;

    // Ball variables
    private static final int NUM_BALLS = 2;
    private ArrayList<Ball3> balls;

    public static void main(String[] args) {
        JFrame frame = new JFrame("Animation Demo");
        frame.setDefaultCloseOperation(
            JFrame.EXIT_ON_CLOSE);

        SimpleAnimation7 sa = new SimpleAnimation7();
        frame.add(sa);

        frame.setBounds(X_LOC, Y_LOC, WIDTH, HEIGHT);
        frame.setVisible(true);

        sa.startAnimation();
        // Needed for keys to hear
        sa.requestFocusInWindow(); // After visible
    }

    public SimpleAnimation7() {
        addKeyListener(new KeyHandler());
        addMouseListener(new MouseHandler());
        setBackground(Color.WHITE);
        balls = new ArrayList<Ball3>();
        BufferedImage im = loadImage("images/ball.gif");
        for (int i = 0; i < NUM_BALLS; i++) {
            Ball3 b = new Ball3(this, im);
            balls.add(b);
        }
        // Add sounds
        try {
            File file = new File("sounds/blip.wav");
            AudioInputStream stream =
                AudioSystem.getAudioInputStream(file);
            clip = AudioSystem.getClip();
            clip.open(stream);
        } catch (IOException ex) {
            ex.printStackTrace();
        } catch (UnsupportedAudioFileException ex) {
            ex.printStackTrace();
        } catch (LineUnavailableException ex) {
            ex.printStackTrace();
        }
    }

    private BufferedImage loadImage(String fileName) {
        BufferedImage im = null;
        try {
            im = ImageIO.read(getClass().getResource(fileName));
        } catch (IOException e) {
            System.out.println("Error loading " + fileName);
        }
        return im;
    }

    // Initialise and start the animation
    public void startAnimation() {
        if (runner == null || !running) {
            runner = new Thread(this);
            runner.start();
        }
    }

    // Called to stop execution
    public void stopAnimation() {
        running = false;
    }

    // Repeatedly update, render, sleep
    public void run() {
        running = true;
        while (running) {
            update();  // update position
            repaint(); // render
            try {
                Thread.sleep(DELAY); // pause
            } catch(InterruptedException ie) {
                running = false;
            }
        }
    }

    // Update the animation state
    public void update() {
        for (int i = 0; i < balls.size(); i++) {
            Ball3 b = balls.get(i);
            b.update();
        }
    }

    // Render the animation
    public void paintComponent(Graphics g) {
        super.paintComponent(g); // paint background
        for (int i = 0; i < balls.size(); i++) {
            Ball3 b = balls.get(i);
            b.draw(g);
        }
    }

    // Play sound clip
    public void playSound() {
        clip.setFramePosition(0);
        clip.start();
    }

    private class KeyHandler extends KeyAdapter {
        public void keyPressed(KeyEvent e) {
            int keyCode = e.getKeyCode();
            if (keyCode == KeyEvent.VK_ESCAPE) {
                stopAnimation();
                System.out.println("Goodbye!");
                System.exit(0); // so enclosing JFrame exits
            }
        }
    }

    private class MouseHandler extends MouseAdapter {
        public void mouseClicked(MouseEvent e) {
            if (running) {
                stopAnimation();
            } else {
                startAnimation();
            }
        }
    }
}

Class Ball3 that Calls the playSound() Method

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import static java.lang.Math.*;
import java.awt.*;
import java.awt.image.BufferedImage;

public class Ball3 {
    private float x;  // x position in pixels
    private float y;  // y position in pixels
    private float dx; // delta x in pixels per loop
    private float dy; // delta y in pixels per loop
    private SimpleAnimation7 anim;
    private BufferedImage im;

    public Ball3(SimpleAnimation7 panel, BufferedImage image) {
        anim = panel;
        im = image;
        dx = (float) random() * 2 + 3;
        dy = (float) random() * 2 + 3;
    }

    // Update state
    public void update() {
        if (x < 0) {
            dx = abs(dx);
            anim.playSound();
        } else if (x + im.getWidth() > anim.getWidth()) {
            dx = -abs(dx);
            anim.playSound();
        }
        if (y < 0) {
            dy = abs(dy);
            anim.playSound();
        } else if (y + im.getHeight() > anim.getHeight()) {
            dy = -abs(dy);
            anim.playSound();
        }
        x += dx;
        y += dy;
    }

    // Render
    public void draw(Graphics g) {
        g.drawImage(im, round(x), round(y), null);
    }
}

14.3.6: Summary

  • Sound is a vibration through some medium as a wave
  • Faster vibrations create a higher sound frequency and you hear a higher pitch
  • The pressure of the sound wave creates an amplitude that makes sound louder or softer
  • While sound begins as a series of waves, it can be converted to a digital format

sound conversion

  • Digital audio is a series of discrete samples of the sound waves
  • The sample rate of the samples determines the number of discrete samples taken
  • The number of bits used to store the sample determines the number of variations in amplitude
  • For instance, a 16 bit sample provides 65,536 possible amplitudes
  • In this section we looked at producing sampled audio using the Java Sound API
  • To open a sound file, you use code like this:
    File file = new File("sound.wav");
    AudioInputStream stream =
        AudioSystem.getAudioInputStream(file);
    
  • To route the AudioInputStream to the sound system, you use a Line
  • Some of the subinterfaces of Line that we use are SourceDataLine and Clip
  • The code to use a line looks something like:
    // Get a Clip from the AudioSystem
    clip = AudioSystem.getClip();
    // Load the samples from the stream
    clip.open(stream);
    // Begin playback of the sound clip
    clip.start();
    
  • We put all the code to open and play a sound file in an example class

Check Yourself

  1. What is a sound?
  2. How does a sound change frequency?
  3. How does a sound change its loudness?
  4. How is an analog sound converted to a digital format?
  5. What code can you use to open a sound stream using the Java Sound API?
  6. Once you open a sound file, what do you use to send the sound stream to the sound system?

Exercise 14.3

Take one minute to review the Check Yourself questions. We will review the questions after you are ready.

14.4: Playing Music

Learner Outcomes

At the end of the lesson the student will be able to:

  • Discuss the usual types of music formats
  • Describe how MIDI music is played
  • Play MIDI music files using the Java Sound API

14.4.1: Music Files

  • Music plays an important role in many applications
  • There are many formats for music files, which we discuss in this section

CD Audio

  • One form of sound is Red Book Audio (standard CD format), which plays from a CD
  • This produces quality audio and is easy to implement
  • Unfortunately, CD audio takes up a lot of space
  • Each minute of music takes up about 10MB
  • Unless you plan to distribute your program on CD, this is not a good option

MP3 and Ogg Vorbis

  • Another option is compressed music
  • MP3 and Ogg Vorbis formats are much smaller that CD audio
  • Typically, they take about 1MB per minute of music
  • The drawback is that the processor must decode the sound before playing it
  • This decoding might be noticeable as slow or jerky animation
  • The effects depend on what else is going on in your program and the power of your processor
  • If processor time is not an issue, then you can get an MP3 or Ogg Vorbis Java decoder
  • Both are available from www.javazoom.net
  • MP3 is incredibly popular but has licensing issues: mp3licensing.com
  • Ogg Vorbis, on the other hand, is license free and may sound better: xiph.org
  • Also, if you use either MP3 or Ogg Vorbis, make sure you do not preload the sound files into main memory
  • While compressed sound files are relatively compact, uncompressed files can take up huge amounts of memory
  • Thus, you want to stream your music directly from disk

14.4.2: Introduction to MIDI

  • A better solution for many projects is to use MIDI music
    • MIDI is an acronym for musical instrument digital interface
  • MIDI is not sampled music but is more like a digital sheet music
  • A MIDI file gives instructions on which note to play on which instrument
  • A synthesizer creates music from the instructions and which is played by the sound system
  • Since MIDI files contain instructions instead of sampled music, they are a much smaller size
  • Typically, a MIDI file is measured in kilobytes rather than megabytes
  • However, because music is synthesized, some instruments may not sound realistic
  • The sound quality depends on the soundbanks in the synthesizer
  • On the other hand, a creative musician can usually mask the deficiencies of MIDI
  • For information on MIDI technology: MIDI Manufacturers Association

Getting MIDI Music

  • To play MIDI music, you will need some MIDI files
  • You can get free MIDI files from the Internet
  • However, just because the MIDI file is available does not mean that it is legal to use it in a commercially
  • Music is copyrighted and you need to respect the copyright
  • You can write your own MIDI music if you are musically inclined
  • Several programs let you record MIDI music on your own computer
  • Also, Cabrillo offers courses in MIDI such as MUS 57: Music and Computers

14.4.3: Processing a MIDI File

  • Let us look at how Java processes a MIDI file
  • In Java, a Sequence object holds the instructions from a MIDI file
  • Thus a Sequence object is like a song
  • To play a MIDI file, you first load a MIDI file into a Sequence object
  • Then the Sequence is played by a Sequencer which transmits the instructions to a Synthesizer
  • The Synthesizer contains the soundbanks which produce the sounds played by the audio system
  • This process is shown in simplified form in the following diagram from the book, Killer Game Programming in Java

    MIDI diagram

Soundbanks

  • The Java Sound API synthesizes MIDI music through a soundbank
  • A soundbank is a collection of instrument sounds
  • The JDK and JRE includes a soundbank in the directories:
    • C:\Program Files\Java\<version>\lib\audio
    • C:\Program Files\Java\<version>\jre\lib\audio
  • The exact directories depend upon the version of Java you have installed
  • The soundbank file is named: soundbank.gm
  • If Java cannot find a soundbank file, then it uses the hardware MIDI port
  • Since the quality of the hardware MIDI is an unknown, you might want to include a soundbank with your application
  • Several soundbanks of varying quality are available by following the link:

14.4.4: Playing MIDI Music

  • The Java Sound API provides MIDI in the javax.sound.midi package
  • To play MIDI music, you need a Sequence and a Sequencer
  • You load the MIDI data into a Sequence using code like:
    Sequence song = MidiSystem.getSequence(
        getClass().getResource(filename));
    
  • Then you create a Sequencer object:
    Sequencer sequencer = MidiSystem.getSequencer();
  • To open the sequence and play it, you use:
    sequencer.setSequence(song);
    sequencer.open();
    sequencer.start();
    

Responding When the Track Ends

  • By default, a sequence will play once and then stop
  • Usually, you want to loop the music in your application
  • To loop a Sequence, you need to know when the sequence is finished to you can start the Sequencer again
  • The Java Sound MIDI API has a MetaEventListener you can use to listen for end-of-track events
  • This listener interface has one method that must be written:
    public void meta(MetaMessage event)
  • You add the listener object implementing the interface to the Sequencer by using:
    sequencer.addMetaEventListener(listenerObject);
    
  • The meta() method gets called frequently but we only want to respond to an end-of-track event:
    public static final int END_OF_TRACK = 47;
    
    public void meta(MetaMessage event) {
        if (event.getType() == END_OF_TRACK) {
            // do something about the event
        }
    }
    

14.4.5: The MidiSong Class

  • Let us put the code snippets from above into a working program
  • The following class, named MidiSong, lets you load and play a MIDI file
  • In addition, it reports some of the available information about the MIDI file
  • We can use this class to experiment with different MIDI files
  • To give us sounds to play, we can use the following files:

Class MidiSong for Playing MIDI Files

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
import java.io.*;
import javax.sound.midi.*;

public class MidiSong implements MetaEventListener {
    // MIDI meta-event constant used to signal the
    // end of a track
    public static final int END_OF_TRACK = 47;

    private Sequencer sequencer;
    private Sequence song;
    private boolean loop;

    // Driver to play midi files
    public static void main(String[] args) {
        MidiSong midi =
            new MidiSong("sounds/bsg.midi");
    }

    public MidiSong(String filename) {
        try {
            song = MidiSystem.getSequence(
                getClass().getResource(filename));
            sequencer = MidiSystem.getSequencer();
            sequencer.setSequence(song);
            sequencer.open();
            sequencer.addMetaEventListener(this);
            sequencer.start();
        } catch (InvalidMidiDataException e) {
            System.out.println("Bad midi file: "
                + filename);
            System.exit(1);
        } catch (MidiUnavailableException e) {
            System.out.println("No sequencer available");
            System.exit(1);
        } catch (IOException e) {
            System.out.println("Could not read: "
                + filename);
            System.exit(1);
        }
        displayMidiInfo(filename);
    }

    private void displayMidiInfo(String filename) {
        System.out.println("Midi File: " + filename);
        System.out.println("  Timing resolution: "
            + song.getResolution());
        System.out.println("  Number of ticks: "
            + song.getTickLength());
        System.out.println("  Number of tracks: "
            + song.getTracks().length);
        System.out.println("  Number of patches: "
            + song.getPatchList().length);
        long lengthMillis =
            song.getMicrosecondLength() / 1000L;
        System.out.println("  Duration: "
            + (lengthMillis / 1000.0) + " secs");
    }

    // Called by the sound system when a meta event occurs
    public void meta(MetaMessage event) {
        if (event.getType() == END_OF_TRACK) {
            sleep(300); // let buffer clear?
            close();
            System.exit(0);
        }
    }

    // Close the sequencer.
    public void close() {
        if (sequencer.isOpen()) {
            sequencer.close();
        }
    }

    private void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch(InterruptedException e) {
            System.out.println("Sleep Interrupted");
        }
    }
}

14.4.6: Summary

  • In this section, we discussed the typical music formats
  • These formats include:
    • Red Book Audio (CD Audio)
    • Compressed Audio like MP3 and Ogg Vorbis
    • MIDI
  • Oftentimes the best choice is MIDI
  • The reason is that MIDI files are smaller than sampled music files
  • A MIDI file contains instructions on which note to play on which instrument
  • A synthesizer then creates music from the instructions and which is played by the sound system
  • The Java Sound API provides MIDI in the javax.sound.midi package
  • To play MIDI music, you need a Sequence and a Sequencer
  • You load the MIDI data into a Sequence using code like:
    Sequence song = MidiSystem.getSequence(
        getClass().getResource(filename));
    
  • Then you get a Sequencer object:
    Sequencer sequencer = MidiSystem.getSequencer();
    
  • To open the sequence and play it, you use:
    sequencer.setSequence(song);
    sequencer.open();
    sequencer.start();
    
  • To respond to end-of-track events, you need to write a MetaEventListener
  • We put all the code to open and play a MIDI file in the MidiSong class

Check Yourself

  1. What is Ogg Vorbis?
  2. What are the problems with using compressed music files like MP3's?
  3. What is MIDI?
  4. How are MIDI sounds produced?
  5. What are the advantages of producing music using MIDI?
  6. What are the disadvantages of producing music using MIDI
  7. How can you improve the quality of MIDI sound files?
  8. What code do you use to play MIDI files using Java?
  9. How do you test for the end of a MIDI track?

Exercise 14.4

Take one minute to review the Check Yourself questions. We will review the questions after you are ready.

Wrap Up

Due Next:
Course Project (5/26/10)
Work on your project!
Home | Blackboard | Schedule | Room Policies | Syllabus
Help | FAQ's | HowTo's | Links
Last Updated: May 19 2010 @12:28:28