package greenfoot.guifx.soundrecorder;

import bluej.BlueJTheme;
import bluej.Config;
import bluej.pkgmgr.Project;
import bluej.utility.javafx.JavaFXUtil;
import bluej.utility.javafx.ResizableCanvas;
import greenfoot.sound.MemoryAudioInputStream;
import greenfoot.sound.Sound;
import greenfoot.sound.SoundPlaybackListener;
import greenfoot.sound.SoundRecorder;
import greenfoot.sound.SoundStream;

import java.io.File;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicReference;

import javafx.application.Platform;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Button;
import javafx.scene.image.Image;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import threadchecker.OnThread;
import threadchecker.Tag;


| The GUI class for the sound recorder. | | @author neil | @author Amjad Altadmri | @OnThread(Tag.FXPlatform) public class SoundRecorderControls extends Stage{ private Player player = new Player(); private SoundRecorder recorder = new SoundRecorder(); private boolean selectionActive = false; private boolean selectionDrawing = false; private double selectionBegin; private double selectionEnd; private boolean playing = false; private long playbackPosition; private boolean recording = false; private AtomicReference<List<byte[]>> currentRecording; private SoundPanel soundPanel = new SoundPanel(); private SaveState saveState = new SaveState(this, recorder); private final String playLabel = Config.getString("soundRecorder.play"); private final String playSelectionLabel = Config.getString("soundRecorder.playSelection"); private final String stopPlayLabel = Config.getString("soundRecorder.stopPlay"); private final String recordLabel = Config.getString("soundRecorder.record"); private final String stopRecordLabel = Config.getString("soundRecorder.stopRecord"); private Button trim = new Button(Config.getString("soundRecorder.trim")); private Button playStop = new Button(playLabel); private Button recordStop = new Button(recordLabel); private final SimpleBooleanProperty showingProperty = new SimpleBooleanProperty(false);
| Creates a SoundRecorderDialog that will save the sounds | in the sounds directory of the given project. | public SoundRecorderControls(Project project) { this.setWidth(450); this.setHeight(400); this.setMinWidth(450); this.setMinHeight(400); setTitle(Config.getString("soundRecorder.title")); Image icon = BlueJTheme.getApplicationFxIcon("greenfoot", false); if (icon != null) { getIcons().add(icon); } setOnShown(e -> showingProperty.set(true)); setOnHidden(e -> showingProperty.set(false)); buildUI(); setProject(project); } private void buildUI() { BorderPane soundAndControls = new BorderPane(soundPanel, null, null, buildControlBox(), null); soundAndControls.setPadding(new Insets(12)); BorderPane.setMargin(soundPanel, new Insets(12,12,12,12)); soundAndControls.setBackground(new Background(new BackgroundFill(Color.LIGHTGRAY, new CornerRadii(5, 5, 5, 5, false), null))); VBox.setVgrow(soundAndControls, Priority.ALWAYS); Button closeButton = new Button(Config.getString("soundRecorder.close")); JavaFXUtil.addChangeListener(saveState.savedProperty(), newValue -> closeButton.setText(Config.getString(newValue ? "soundRecorder.close" : "soundRecorder.close.without.saving"))); closeButton.setOnAction(event -> close()); this.setOnCloseRequest(event -> stopRecording()); VBox contentPane = new VBox(20); contentPane.setAlignment(Pos.CENTER); contentPane.setPadding(new Insets(12)); contentPane.getChildren().addAll(soundAndControls, saveState.buildSaveBox(), closeButton); this.setScene(new Scene(contentPane)); }
| Change the project associated with this sound recorder. | public void setProject(Project project) { saveState.setProjectSoundDir(getSoundDir(project)); }
| Builds the controls: record/trim/play | private Pane buildControlBox() { recordStop.setFocusTraversable(false); recordStop.setOnAction(event -> { if (!recording) { currentRecording = recorder.startRecording(); recordStop.setText(stopRecordLabel); playStop.setDisable(true); recording = true; new Timer().scheduleAtFixedRate(new TimerTask() { List<byte[]> lastValue = null; public void run() { List<byte[]> curValue = currentRecording.get(); if (curValue != lastValue) Platform.runLater(soundPanel::paintComponent); if (lastValue != null && curValue == null) cancel(); lastValue = curValue; } }, 100, 200); } else { stopRecording(); } }); trim.setDisable(true); trim.setFocusTraversable(false); trim.setOnAction(event -> { recorder.trim(Math.min(selectionBegin, selectionEnd), Math.max(selectionBegin, selectionEnd)); saveState.changed(true); selectionActive = false; updateButtons(); soundPanel.paintComponent(); }); playStop.setDisable(true); playStop.setFocusTraversable(false); playStop.setOnAction(event -> player.act()); HBox controls = new HBox(10); controls.setAlignment(Pos.CENTER); controls.getChildren().addAll(recordStop, playStop, trim); return controls; }
| Gets the sounds directory for the given project. Project may be null. | | @return the sound directory as a file. Will return null if the project is null. | private static File getSoundDir(Project project) { if (project != null) { return new File(project.getProjectDir(), "sounds"); } else { return null; } }
| Updates trim and play buttons based on whether the selection is active | private void updateButtons() { trim.setDisable(!selectionActive); playStop.setText(selectionActive ? playSelectionLabel : playLabel); }
| A class that handles playing sound, controlled by a play/stop button (for which this is the ActionListener). | private class Player implements SoundPlaybackListener { private final Timer timer = new Timer(); private TimerTask repaintWhilePlaying; private SoundStream stream; @OnThread(Tag.FXPlatform) public void act() { if (playing) { if (stream != null) stream.stop(); } else { MemoryAudioInputStream memoryStream; final int start; if (selectionActive) { start = getSelectionStartOffset(); int len = getSelectionFinishOffset() - start; memoryStream = new MemoryAudioInputStream(recorder.getRawSound(), start, len, recorder.getFormat()); } else { start = 0; memoryStream = new MemoryAudioInputStream(recorder.getRawSound(), recorder.getFormat()); } stream = new SoundStream(memoryStream, this); playing = true; playbackPosition = start; stream.play(); playStop.setText(stopPlayLabel); recordStop.setDisable(true); repaintWhilePlaying = new TimerTask() { @Override public void run() { playbackPosition = start + stream.getLongFramePosition(); Platform.runLater(soundPanel::paintComponent); } }; timer.scheduleAtFixedRate(repaintWhilePlaying, 50, 100); } } @OnThread(Tag.Any) public void playbackPaused(Sound sound) { } @OnThread(Tag.Any) public void playbackStarted(Sound sound) { } @OnThread(Tag.Any) public void playbackStopped(Sound sound) { Platform.runLater(() -> { updateButtons(); recordStop.setDisable(false); repaintWhilePlaying.cancel(); playing = false; soundPanel.paintComponent(); }); } @OnThread(Tag.Any) public void soundClosed(Sound sound) { } }
| A panel for displaying the recorded sound. | @OnThread(Tag.FXPlatform) private class SoundPanel extends ResizableCanvas { private SoundPanel() { this.addEventHandler(MouseEvent.MOUSE_PRESSED, this::mousePressed); this.addEventHandler(MouseEvent.MOUSE_RELEASED, this::mouseReleased); this.addEventHandler(MouseEvent.MOUSE_DRAGGED, this::mouseDragged); onResize = this::paintComponent; } protected void paintComponent() { GraphicsContext g = getGraphicsContext2D(); double width = getWidth(); double height = getHeight(); double middle = height / 2; double halfHeight = height / 2; byte[] sound = recorder.getRawSound(); g.setFill(Color.BLACK); g.fillRect(0, 0, width, height); if (recording || (sound != null && sound.length > 0)) { if (selectionActive) { g.setFill(Color.GRAY); g.fillRect(Math.min(selectionBegin, selectionEnd) * width, 0, Math.abs(selectionBegin - selectionEnd) * width, height); } byte[][] rec = null; int recLength = 0; if (recording) { List<byte[]> recList = currentRecording.get(); if (recList != null) { rec = recList.toArray(new byte[0][]); for (byte[] chunk : rec) { int chunkLength = chunk == null ? 0 : chunk.length; recLength += chunkLength; } } } else { recLength = sound.length; } int curRecChunk = 0; int prevChunksLength = 0; for (int i = 0; i < width; i++) { float pos = (float) i / (float) width; float f = 0; if (rec != null) { int index = (int) (pos * (float) recLength); if (recLength == 0 || index >= recLength) { f = 0.0f; } else { while (index >= prevChunksLength + rec[curRecChunk].length) { prevChunksLength += rec[curRecChunk].length; curRecChunk += 1; } f = (float) rec[curRecChunk][index - prevChunksLength] / 128.0f; } } else if (sound != null) { int index = (int) (pos * (float) sound.length); f = (float) sound[index] / 128.0f; } int waveHeight = (int) (halfHeight * f * 0.9f); g.setStroke(inSelection(pos) ? Color.YELLOW : Color.LIME); g.strokeLine(i, middle - waveHeight, i, middle + waveHeight); } if (playing) { g.setStroke(Color.RED); float playPosRel = (float) playbackPosition / (float) recLength; int pos = (int) (playPosRel * (float) width); g.strokeLine(pos, 0, pos, height); } } }
| Works out whether the given number (0->1) is inside the current selection (if there is one) | | @param f value to test if it is in the selected range | @return true if the passed value is within the selected range | private boolean inSelection(float f) { return selectionActive && f >= Math.min(selectionBegin, selectionEnd) && f <= Math.max(selectionBegin, selectionEnd); } public void mousePressed(MouseEvent e) { if (recorder.getRawSound() != null) { selectionActive = false; selectionDrawing = true; selectionBegin = calculatePosition(e.getX()); selectionEnd = selectionBegin; } } public void mouseReleased(MouseEvent e) { if (selectionDrawing) { selectionDrawing = false; selectionEnd = calculatePosition(e.getX()); if (selectionBegin == selectionEnd) selectionActive = false; paintComponent(); } updateButtons(); } public void mouseDragged(MouseEvent e) { if (selectionDrawing) { selectionEnd = calculatePosition(e.getX()); selectionActive = true; paintComponent(); } } private double calculatePosition(double x) { double pos = x / getWidth(); pos = Math.max(0, pos); pos = Math.min(1, pos); return pos; } }
| Gets the start of the selection as an index into the raw sound array | | @return the index of the selection start | private int getSelectionStartOffset() { double start = Math.min(selectionBegin, selectionEnd); float length = recorder.getRawSound().length; return (int)(start * length); }
| Gets the finish of the selection as an index into the raw sound array | | @return the index of the selection end | private int getSelectionFinishOffset() { double finish = Math.max(selectionBegin, selectionEnd); float length = recorder.getRawSound().length; return (int)(finish * length); } private void stopRecording() { if (recording) { recorder.stopRecording(); playStop.setDisable(false); saveState.changed(true); soundPanel.paintComponent(); recordStop.setText(recordLabel); recording = false; } } public SimpleBooleanProperty getShowingProperty() { return showingProperty; } }
top, use, map, class SoundRecorderControls

.   SoundRecorderControls
.   buildUI
.   setProject
.   buildControlBox
.   run
.   getSoundDir
.   updateButtons

top, use, map, class SoundRecorderControls . Player

.   act
.   run
.   playbackPaused
.   playbackStarted
.   playbackStopped
.   soundClosed

top, use, map, class SoundRecorderControls . Player . SoundPanel

.   SoundPanel
.   paintComponent
.   inSelection
.   mousePressed
.   mouseReleased
.   mouseDragged
.   calculatePosition
.   getSelectionStartOffset
.   getSelectionFinishOffset
.   stopRecording
.   getShowingProperty




578 neLoCode + 19 LoComm