{"id":"drawing-and-images-sound","slug":"sound","title":"Sound, and the end of section 14.2","text":"The last kind of multimedia in this module is sound: a click when a button is pressed, a chime when a message arrives, a cheer when a game is won.\n\n### Sound in JavaFX\n\nJavaFX plays short sounds with the class **AudioClip**, from the package `javafx.scene.media`. On your own computer, a window with a button that plays a chime looks like this:\n\n```java\nimport java.nio.file.Paths;\nimport javafx.application.Application;\nimport javafx.scene.Scene;\nimport javafx.scene.control.Button;\nimport javafx.scene.layout.VBox;\nimport javafx.scene.media.AudioClip;\nimport javafx.stage.Stage;\n\npublic class Main extends Application {\n\n    @Override\n    public void start(Stage stage) {\n        AudioClip chime = new AudioClip(Paths.get(\"chime.wav\").toUri().toString());\n        Button ring = new Button(\"Ring the chime\");\n        ring.setOnAction(event -> chime.play());\n\n        stage.setScene(new Scene(new VBox(ring)));\n        stage.setTitle(\"Door chime\");\n        stage.show();\n    }\n\n    public static void main(String[] args) {\n        launch(Main.class);\n    }\n}\n```\n\n- `new AudioClip(...)` takes the address of the sound file. `Paths.get(\"chime.wav\").toUri().toString()` turns the file's name into its full address, which starts with `file:`, such as `file:///home/ada/chime/chime.wav`.\n- The clip reads the whole sound when it's made, so `play()` starts at once. Calling `play()` again while it still plays starts the sound once more, over the first one.\n- `stop()` stops it, `setVolume(0.5)` makes it play at half the volume (from 0.0 for silence to 1.0, the default), and `setCycleCount(3)` makes each `play()` play it three times in a row.\n- For long sounds, such as music, JavaFX has `Media` and `MediaPlayer`, which play a file little by little instead of reading it all first.\n- The sound classes are a part of JavaFX of their own, `javafx.media`, which a project needs next to the `javafx.controls` of the windows.\n\n### Sound in Java Arena\n\nThis page can't play sound. Java Arena's practice JavaFX has no `javafx.scene.media`, and the Java engine that runs your programs inside the page can't play sound. So here, javac stops at the import:\n\n```java\nimport javafx.scene.media.AudioClip;\n\npublic class Main {\n    public static void main(String[] args) {\n        AudioClip chime = new AudioClip(\"file:chime.wav\");\n        chime.play();\n    }\n}\n```\n\n```javac\nMain.java:1: error: package javafx.scene.media does not exist\nimport javafx.scene.media.AudioClip;\n                         ^\nMain.java:5: error: cannot find symbol\n        AudioClip chime = new AudioClip(\"file:chime.wav\");\n        ^\n  symbol:   class AudioClip\n  location: class Main\nMain.java:5: error: cannot find symbol\n        AudioClip chime = new AudioClip(\"file:chime.wav\");\n                              ^\n  symbol:   class AudioClip\n  location: class Main\n3 errors\n```\n\nThis step is for reading. The MOOC's exercise with sound, a button that plays a sound file, needs your own computer with JavaFX installed (module 48 links to the MOOC's instructions for that).\n\n### Deciding what to play\n\nWhat a program **decides** about sound can still be written and checked here: which sound to play, and when. As in module 50, that decision goes in a plain class, and the window's handlers only carry it out. A quiz show's buzzer plays `right.wav` for a right answer and `wrong.wav` for a wrong one, except that the third wrong answer in a row plays `out.wav` and starts the count again. Here the handler prints the name of the clip where your computer would play it:\n\n```java\nimport javafx.application.Application;\nimport javafx.scene.Scene;\nimport javafx.scene.control.Button;\nimport javafx.scene.control.Label;\nimport javafx.scene.layout.HBox;\nimport javafx.scene.layout.VBox;\nimport javafx.stage.Stage;\n\npublic class Main extends Application {\n\n    @Override\n    public void start(Stage stage) {\n        Buzzer buzzer = new Buzzer();\n        Label last = new Label(\"No answers yet\");\n        Button right = new Button(\"Right\");\n        Button wrong = new Button(\"Wrong\");\n        right.setOnAction(event -> {\n            String clip = buzzer.clipFor(true);\n            System.out.println(\"play \" + clip);\n            last.setText(\"Last sound: \" + clip);\n        });\n        wrong.setOnAction(event -> {\n            String clip = buzzer.clipFor(false);\n            System.out.println(\"play \" + clip);\n            last.setText(\"Last sound: \" + clip);\n        });\n\n        stage.setScene(new Scene(new VBox(10, new HBox(5, right, wrong), last)));\n        stage.setTitle(\"Quiz buzzer\");\n        stage.show();\n    }\n\n    public static void main(String[] args) {\n        launch(Main.class);\n    }\n}\n// ==== Buzzer.java ====\npublic class Buzzer {\n    private int wrongInARow;\n\n    public String clipFor(boolean correct) {\n        if (correct) {\n            this.wrongInARow = 0;\n            return \"right.wav\";\n        }\n        this.wrongInARow++;\n        if (this.wrongInARow == 3) {\n            this.wrongInARow = 0;\n            return \"out.wav\";\n        }\n        return \"wrong.wav\";\n    }\n}\n```\n\n```window\n{\"events\":[\"click Button \\\"Wrong\\\"\",\"click Button \\\"Wrong\\\"\",\"click Button \\\"Right\\\"\",\"click Button \\\"Wrong\\\"\"],\"window\":{\"windows\":[{\"title\":\"Quiz buzzer\",\"scene\":{\"root\":{\"n\":1,\"type\":\"VBox\",\"spacing\":10,\"children\":[{\"n\":2,\"type\":\"HBox\",\"spacing\":5,\"children\":[{\"n\":3,\"type\":\"Button\",\"text\":\"Right\",\"onAction\":true},{\"n\":4,\"type\":\"Button\",\"text\":\"Wrong\",\"onAction\":true}]},{\"n\":5,\"type\":\"Label\",\"text\":\"Last sound: wrong.wav\"}]}}}],\"problems\":[]},\"outline\":\"Window \\\"Quiz buzzer\\\"\\n  VBox spacing 10\\n    HBox spacing 5\\n      Button \\\"Right\\\"\\n      Button \\\"Wrong\\\"\\n    Label \\\"Last sound: wrong.wav\\\"\"}\n```\n\n```output\nplay wrong.wav\nplay wrong.wav\nplay right.wav\nplay wrong.wav\n```\n\nOn your own computer, the handler would make an AudioClip for each of the three files in `start`, keep them in a map from the file's name to its clip, and call `clips.get(clip).play()` where this one prints. Buzzer stays the same, and every case of it can be tried by calling `clipFor`, with no sound at all.\n\n### Part 14 so far\n\nSection 14.1 (module 51) and section 14.2 (this module) were about showing more than text:\n\n- **Charts** (module 51): rows of data read from files into numbers, lists and maps; line charts with `NumberAxis`, `XYChart.Series` and `XYChart.Data`; several series from a map, and series from a formula; a `Slider` whose listener redraws a chart; bar charts with a `CategoryAxis`; and charts that are honest about their numbers.\n- **Drawing** (steps 1 and 2): a `Canvas` and its `GraphicsContext`, with fill and stroke colors and a line width; rectangles, ovals given by the rectangle around them, lines and polygons, where later drawing covers earlier; colors by name, by `Color.rgb` and by `Color.web`; and the mouse, with its pressed, dragged, released and clicked events and the point in `getX()` and `getY()`.\n- **Images** (steps 3 to 6): an `Image` read from a file (or an error image when it can't be), its `PixelReader`, a `WritableImage` written through its `PixelWriter`, and an `ImageView` that shows it; smaller copies that keep every second (or every f-th) pixel; copies placed into bigger images; tiles with `%`, mirrored tiles; and filters: the negative, two kinds of gray, a threshold and tints.\n- **Sound** (this step): `AudioClip`, for your own computer.\n\nSection 14.3 builds a larger program with a window: a game in the style of Asteroids, with shapes that move and the keyboard to steer, in the next module.\n","task":"The quiz buzzer from above gets seven answers. What does this program print?\n","kind":"predict","seed":"public class Main {\n    public static void main(String[] args) {\n        Buzzer buzzer = new Buzzer();\n        boolean[] answers = {false, false, true, false, false, false, false};\n        for (boolean answer : answers) {\n            System.out.println(buzzer.clipFor(answer));\n        }\n    }\n}\n// ==== Buzzer.java ====\npublic class Buzzer {\n    private int wrongInARow;\n\n    public String clipFor(boolean correct) {\n        if (correct) {\n            this.wrongInARow = 0;\n            return \"right.wav\";\n        }\n        this.wrongInARow++;\n        if (this.wrongInARow == 3) {\n            this.wrongInARow = 0;\n            return \"out.wav\";\n        }\n        return \"wrong.wav\";\n    }\n}\n","solution":"public class Main {\n    public static void main(String[] args) {\n        Buzzer buzzer = new Buzzer();\n        boolean[] answers = {false, false, true, false, false, false, false};\n        for (boolean answer : answers) {\n            System.out.println(buzzer.clipFor(answer));\n        }\n    }\n}\n// ==== Buzzer.java ====\npublic class Buzzer {\n    private int wrongInARow;\n\n    public String clipFor(boolean correct) {\n        if (correct) {\n            this.wrongInARow = 0;\n            return \"right.wav\";\n        }\n        this.wrongInARow++;\n        if (this.wrongInARow == 3) {\n            this.wrongInARow = 0;\n            return \"out.wav\";\n        }\n        return \"wrong.wav\";\n    }\n}\n","hints":["A right answer sets the count of wrong answers in a row back to 0. So after the first `true`, the count starts over.","Count the wrong answers after the `true`: the third one in a row plays `out.wav` and sets the count to 0, so the fourth is an ordinary wrong answer again."],"tests":[{"name":"Output","stdin":"","expect":"wrong.wav\nwrong.wav\nright.wav\nwrong.wav\nwrong.wav\nout.wav\nwrong.wav","hidden":false}],"require":[],"forbid":[],"lines":["wrong.wav","wrong.wav","right.wav","wrong.wav","wrong.wav","out.wav","wrong.wav"],"more":[{"task":"How light is a picture? Count the pixels of `pic.png` in three groups by their brightness, the average of their red, green and blue as whole numbers from 0 to 255 (rounded with `Math.round`, and divided by 3 with whole-number division): `dark` below 85, `middle` from 85 to 169, and `light` from 170 up.\n\nShow the counts in a bar chart (module 51), which is the scene's root: the x axis a `CategoryAxis` and the y axis a `NumberAxis`, both without labels and auto-ranging; the chart's title `Brightness of pic.png` and no legend; and one series named `Pixels` with the three data points `(\"dark\", count)`, `(\"middle\", count)` and `(\"light\", count)`, in that order. The window's title is `Brightness`. The first test gives the program the sunset `photo.png` as `pic.png`, and the window should look like the drawing below.\n","kind":"code","seed":"import javafx.application.Application;\nimport javafx.scene.Scene;\nimport javafx.scene.chart.BarChart;\nimport javafx.scene.chart.CategoryAxis;\nimport javafx.scene.chart.NumberAxis;\nimport javafx.scene.chart.XYChart;\nimport javafx.scene.image.Image;\nimport javafx.scene.image.PixelReader;\nimport javafx.scene.paint.Color;\nimport javafx.stage.Stage;\n\npublic class Main extends Application {\n\n    @Override\n    public void start(Stage stage) {\n        Image picture = new Image(\"file:pic.png\");\n        BarChart<String, Number> chart = new BarChart<>(new CategoryAxis(), new NumberAxis());\n\n        stage.setScene(new Scene(chart));\n        stage.setTitle(\"Brightness\");\n        stage.show();\n    }\n\n    public static void main(String[] args) {\n        launch(Main.class);\n    }\n}\n","solution":"import javafx.application.Application;\nimport javafx.scene.Scene;\nimport javafx.scene.chart.BarChart;\nimport javafx.scene.chart.CategoryAxis;\nimport javafx.scene.chart.NumberAxis;\nimport javafx.scene.chart.XYChart;\nimport javafx.scene.image.Image;\nimport javafx.scene.image.PixelReader;\nimport javafx.scene.paint.Color;\nimport javafx.stage.Stage;\n\npublic class Main extends Application {\n\n    @Override\n    public void start(Stage stage) {\n        Image picture = new Image(\"file:pic.png\");\n        PixelReader reader = picture.getPixelReader();\n        int dark = 0;\n        int middle = 0;\n        int light = 0;\n        for (int y = 0; y < (int) picture.getHeight(); y++) {\n            for (int x = 0; x < (int) picture.getWidth(); x++) {\n                Color color = reader.getColor(x, y);\n                int red = (int) Math.round(color.getRed() * 255);\n                int green = (int) Math.round(color.getGreen() * 255);\n                int blue = (int) Math.round(color.getBlue() * 255);\n                int brightness = (red + green + blue) / 3;\n                if (brightness < 85) {\n                    dark++;\n                } else if (brightness < 170) {\n                    middle++;\n                } else {\n                    light++;\n                }\n            }\n        }\n\n        BarChart<String, Number> chart = new BarChart<>(new CategoryAxis(), new NumberAxis());\n        chart.setTitle(\"Brightness of pic.png\");\n        chart.setLegendVisible(false);\n        XYChart.Series<String, Number> pixels = new XYChart.Series<>();\n        pixels.setName(\"Pixels\");\n        pixels.getData().add(new XYChart.Data<>(\"dark\", dark));\n        pixels.getData().add(new XYChart.Data<>(\"middle\", middle));\n        pixels.getData().add(new XYChart.Data<>(\"light\", light));\n        chart.getData().add(pixels);\n\n        stage.setScene(new Scene(chart));\n        stage.setTitle(\"Brightness\");\n        stage.show();\n    }\n\n    public static void main(String[] args) {\n        launch(Main.class);\n    }\n}\n","hints":["Three int counters, then the two loops over the pixels: for each, the brightness `(red + green + blue) / 3` from the whole-number amounts, and `if`, `else if` and `else` to pick the counter.","Then a `XYChart.Series<String, Number>` named `Pixels` with `new XYChart.Data<>(\"dark\", dark)` and the other two, added to `chart.getData()`. The chart also needs `setTitle(...)` and `setLegendVisible(false)`."],"tests":[{"name":"Test 1","stdin":"","files":{"pic.png":{"asset":"photo.png","url":"images/photo-5f4040f3fe.png","width":64,"height":48,"size":2077,"alt":"A soft sunset: a sky going from purple to orange, a glowing low sun, dark hills, and rippled water reflecting the sky."}},"expect":"","hidden":false,"events":[],"window":{"windows":[{"title":"Brightness","scene":{"root":{"n":1,"type":"BarChart","title":"Brightness of pic.png","legendVisible":false,"xAxis":{"type":"CategoryAxis","label":null,"autoRanging":true,"categories":["dark","middle","light"]},"yAxis":{"type":"NumberAxis","label":null,"autoRanging":true,"lowerBound":0,"upperBound":100,"tickUnit":5,"forceZeroInRange":true,"minorTickVisible":true},"series":[{"name":"Pixels","data":[["dark",638],["middle",2199],["light",235]]}]}}}],"problems":[]},"outline":"Window \"Brightness\"\n  BarChart \"Brightness of pic.png\" (no legend)\n    x axis CategoryAxis\n    y axis NumberAxis\n    series \"Pixels\": (\"dark\", 638) (\"middle\", 2199) (\"light\", 235)"},{"name":"Test 2","stdin":"","files":{"pic.png":{"asset":"landscape.png","url":"images/landscape-309f25443f.png","width":32,"height":24,"size":317,"alt":"A small landscape: a blue sky that gets lighter toward the horizon, a yellow sun, blue-green hills, a green slope, a small blue lake and a striped brown field."}},"expect":"","hidden":true,"events":[],"window":{"windows":[{"title":"Brightness","scene":{"root":{"n":1,"type":"BarChart","title":"Brightness of pic.png","legendVisible":false,"xAxis":{"type":"CategoryAxis","label":null,"autoRanging":true,"categories":["dark","middle","light"]},"yAxis":{"type":"NumberAxis","label":null,"autoRanging":true,"lowerBound":0,"upperBound":100,"tickUnit":5,"forceZeroInRange":true,"minorTickVisible":true},"series":[{"name":"Pixels","data":[["dark",0],["middle",599],["light",169]]}]}}}],"problems":[]},"outline":"Window \"Brightness\"\n  BarChart \"Brightness of pic.png\" (no legend)\n    x axis CategoryAxis\n    y axis NumberAxis\n    series \"Pixels\": (\"dark\", 0) (\"middle\", 599) (\"light\", 169)"},{"name":"Test 3","stdin":"","files":{"pic.png":{"asset":"blocks.png","url":"images/blocks-e2cb7ee008.png","width":48,"height":32,"size":201,"alt":"Colored squares of 8 by 8 pixels in 4 rows of 6: bright colors, light colors, dark colors, and grays from black to white."}},"expect":"","hidden":true,"events":[],"window":{"windows":[{"title":"Brightness","scene":{"root":{"n":1,"type":"BarChart","title":"Brightness of pic.png","legendVisible":false,"xAxis":{"type":"CategoryAxis","label":null,"autoRanging":true,"categories":["dark","middle","light"]},"yAxis":{"type":"NumberAxis","label":null,"autoRanging":true,"lowerBound":0,"upperBound":100,"tickUnit":5,"forceZeroInRange":true,"minorTickVisible":true},"series":[{"name":"Pixels","data":[["dark",512],["middle",512],["light",512]]}]}}}],"problems":[]},"outline":"Window \"Brightness\"\n  BarChart \"Brightness of pic.png\" (no legend)\n    x axis CategoryAxis\n    y axis NumberAxis\n    series \"Pixels\": (\"dark\", 512) (\"middle\", 512) (\"light\", 512)"}],"require":[],"forbid":[]},{"task":"A tiny pixel-art editor. Make a WritableImage of 8 x 8 pixels, all white (`Color.WHITE`) at first, and show it in an ImageView. A click on a pixel of the image view turns a white pixel black (`Color.BLACK`), and a black pixel white again. Under the image, a label counts the black pixels: `Black pixels: 0` at first, and `Black pixels: 3` after three pixels were turned black. The scene's root is a VBox with no spacing, holding the image view and then the label, and the window's title is `Pixel editor`. The window after the clicks below is drawn under them.\n","kind":"code","seed":"import javafx.application.Application;\nimport javafx.scene.Scene;\nimport javafx.scene.control.Label;\nimport javafx.scene.image.ImageView;\nimport javafx.scene.image.PixelWriter;\nimport javafx.scene.image.WritableImage;\nimport javafx.scene.layout.VBox;\nimport javafx.scene.paint.Color;\nimport javafx.stage.Stage;\n\npublic class Main extends Application {\n\n    @Override\n    public void start(Stage stage) {\n        WritableImage picture = new WritableImage(8, 8);\n        ImageView view = new ImageView(picture);\n        Label count = new Label(\"Black pixels: 0\");\n\n        stage.setScene(new Scene(new VBox(view, count)));\n        stage.setTitle(\"Pixel editor\");\n        stage.show();\n    }\n\n    public static void main(String[] args) {\n        launch(Main.class);\n    }\n}\n","solution":"import javafx.application.Application;\nimport javafx.scene.Scene;\nimport javafx.scene.control.Label;\nimport javafx.scene.image.ImageView;\nimport javafx.scene.image.PixelWriter;\nimport javafx.scene.image.WritableImage;\nimport javafx.scene.layout.VBox;\nimport javafx.scene.paint.Color;\nimport javafx.stage.Stage;\n\npublic class Main extends Application {\n    private int black;\n\n    @Override\n    public void start(Stage stage) {\n        WritableImage picture = new WritableImage(8, 8);\n        PixelWriter writer = picture.getPixelWriter();\n        for (int y = 0; y < 8; y++) {\n            for (int x = 0; x < 8; x++) {\n                writer.setColor(x, y, Color.WHITE);\n            }\n        }\n        ImageView view = new ImageView(picture);\n        Label count = new Label(\"Black pixels: 0\");\n        view.setOnMouseClicked(event -> {\n            int x = (int) event.getX();\n            int y = (int) event.getY();\n            if (picture.getPixelReader().getColor(x, y).equals(Color.WHITE)) {\n                writer.setColor(x, y, Color.BLACK);\n                this.black++;\n            } else {\n                writer.setColor(x, y, Color.WHITE);\n                this.black--;\n            }\n            count.setText(\"Black pixels: \" + this.black);\n        });\n\n        stage.setScene(new Scene(new VBox(view, count)));\n        stage.setTitle(\"Pixel editor\");\n        stage.show();\n    }\n\n    public static void main(String[] args) {\n        launch(Main.class);\n    }\n}\n","hints":["A new WritableImage is transparent, so paint all 64 pixels white first, with two loops. In the image view's `setOnMouseClicked`, `(int) event.getX()` and `(int) event.getY()` are the pixel's column and row.","`picture.getPixelReader().getColor(x, y).equals(Color.WHITE)` tells whether the pixel is white. Keep the count in an instance variable, add or take away one, and set the label's text."],"tests":[{"name":"Test 1","stdin":"","expect":"","hidden":false,"events":["click ImageView 1 at 1 1","click ImageView 1 at 6 1","click ImageView 1 at 3 5","click ImageView 1 at 4 6","click ImageView 1 at 6 1"],"window":{"windows":[{"title":"Pixel editor","scene":{"root":{"n":1,"type":"VBox","children":[{"n":2,"type":"ImageView","onMouseClicked":true,"image":{"width":8,"height":8,"crc":"715393b1","pixels":"////////////////////////////////////////////////AAAA/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wAAAP///////////////////////////////////////////wAAAP///////////////////////////////////////////////////////////w=="}},{"n":3,"type":"Label","text":"Black pixels: 3"}]}}}],"problems":[]},"outline":"Window \"Pixel editor\"\n  VBox\n    ImageView 8 x 8, pixels 715393b1\n    Label \"Black pixels: 3\""},{"name":"Test 2","stdin":"","expect":"","hidden":true,"events":["click ImageView 1 at 0 0","click ImageView 1 at 7 7","click ImageView 1 at 0 0","click ImageView 1 at 0 0","click ImageView 1 at 2 4"],"window":{"windows":[{"title":"Pixel editor","scene":{"root":{"n":1,"type":"VBox","children":[{"n":2,"type":"ImageView","onMouseClicked":true,"image":{"width":8,"height":8,"crc":"5eebf862","pixels":"AAAA/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wAAAP//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////AAAA/w=="}},{"n":3,"type":"Label","text":"Black pixels: 3"}]}}}],"problems":[]},"outline":"Window \"Pixel editor\"\n  VBox\n    ImageView 8 x 8, pixels 5eebf862\n    Label \"Black pixels: 3\""},{"name":"Test 3","stdin":"","expect":"","hidden":true,"events":[],"window":{"windows":[{"title":"Pixel editor","scene":{"root":{"n":1,"type":"VBox","children":[{"n":2,"type":"ImageView","onMouseClicked":true,"image":{"width":8,"height":8,"crc":"fea8a821","pixels":"/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////w=="}},{"n":3,"type":"Label","text":"Black pixels: 0"}]}}}],"problems":[]},"outline":"Window \"Pixel editor\"\n  VBox\n    ImageView 8 x 8, pixels fea8a821\n    Label \"Black pixels: 0\""}],"require":[],"forbid":[]}]}