From 7d15285da56ae9ca4d365003199875a43d9fec34 Mon Sep 17 00:00:00 2001 From: kske Date: Fri, 3 Jul 2020 14:17:04 +0200 Subject: [PATCH 1/8] Add AudioRecorder --- .../java/envoy/client/data/AudioRecorder.java | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 src/main/java/envoy/client/data/AudioRecorder.java diff --git a/src/main/java/envoy/client/data/AudioRecorder.java b/src/main/java/envoy/client/data/AudioRecorder.java new file mode 100644 index 0000000..96b08a0 --- /dev/null +++ b/src/main/java/envoy/client/data/AudioRecorder.java @@ -0,0 +1,68 @@ +package envoy.client.data; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import javax.sound.sampled.*; + +import envoy.exception.EnvoyException; + +/** + * Records audio and exports it as a byte array. + *

+ * Project: envoy-client
+ * File: AudioRecorder.java
+ * Created: 02.07.2020
+ * + * @author Kai S. K. Engelbart + * @since Envoy Client v0.1-beta + */ +public final class AudioRecorder { + + private final AudioFormat format; + + private DataLine.Info info; + private TargetDataLine line; + private Path tempFile; + + public AudioRecorder() { this(new AudioFormat(16000, 16, 1, true, false)); } + + public AudioRecorder(AudioFormat format) { + this.format = format; + info = new DataLine.Info(TargetDataLine.class, format); + } + + public boolean isSupported() { return AudioSystem.isLineSupported(info); } + + public void start() throws EnvoyException { + try { + + // Open the line + line = (TargetDataLine) AudioSystem.getLine(info); + line.open(format); + line.start(); + + // Prepare temp file + tempFile = Files.createTempFile("recording", "wav"); + + // Start the recording + var ais = new AudioInputStream(line); + AudioSystem.write(ais, AudioFileFormat.Type.WAVE, tempFile.toFile()); + } catch (IOException | LineUnavailableException e) { + throw new EnvoyException("Cannot record voice", e); + } + } + + public byte[] finish() throws EnvoyException { + try { + line.stop(); + line.close(); + byte[] data = Files.readAllBytes(tempFile); + Files.delete(tempFile); + return data; + } catch (IOException e) { + throw new EnvoyException("Cannot save voice recording", e); + } + } +} From d9bf20b88eb02ff9154ac73e7c17b94f2850452b Mon Sep 17 00:00:00 2001 From: kske Date: Fri, 3 Jul 2020 23:32:22 +0200 Subject: [PATCH 2/8] Add Voice Recording to ChatScene The ChatScene layout has been adjusted to include a voice message button. When pressed, a recording starts. To finish the recording, press the button again. The recording will be saved as a pending attachment. The next message sent will include the attachment. When a pending attachment is present, a message can be sent without text. When the chat is switched, the pending attachment is discarded. This does not stop active recordings, however. The ChatScene layout handles large stages better now by ditching percentage-wise row and column scaling in favor of absolute values for all cells except the message list and text area. --- .../envoy/client/ui/controller/ChatScene.java | 63 ++++++-- src/main/resources/fxml/ChatScene.fxml | 149 ++++++------------ 2 files changed, 100 insertions(+), 112 deletions(-) diff --git a/src/main/java/envoy/client/ui/controller/ChatScene.java b/src/main/java/envoy/client/ui/controller/ChatScene.java index ed35cda..3ef3dd2 100644 --- a/src/main/java/envoy/client/ui/controller/ChatScene.java +++ b/src/main/java/envoy/client/ui/controller/ChatScene.java @@ -17,9 +17,7 @@ import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.paint.Color; -import envoy.client.data.Chat; -import envoy.client.data.LocalDB; -import envoy.client.data.Settings; +import envoy.client.data.*; import envoy.client.event.MessageCreationEvent; import envoy.client.net.Client; import envoy.client.net.WriteProxy; @@ -29,10 +27,12 @@ import envoy.client.ui.listcell.ContactListCellFactory; import envoy.client.ui.listcell.MessageControl; import envoy.client.ui.listcell.MessageListCellFactory; import envoy.data.*; +import envoy.data.Attachment.AttachmentType; import envoy.event.EventBus; import envoy.event.MessageStatusChange; import envoy.event.UserStatusChange; import envoy.event.contact.ContactOperation; +import envoy.exception.EnvoyException; import envoy.util.EnvoyLog; /** @@ -57,6 +57,9 @@ public final class ChatScene { @FXML private Button postButton; + @FXML + private Button voiceButton; + @FXML private Button settingsButton; @@ -77,9 +80,11 @@ public final class ChatScene { private WriteProxy writeProxy; private SceneContext sceneContext; - private boolean postingPermanentlyDisabled = false; - - private Chat currentChat; + private Chat currentChat; + private AudioRecorder recorder; + private boolean recording; + private Attachment pendingAttachment; + private boolean postingPermanentlyDisabled = false; private static final Settings settings = Settings.getInstance(); private static final EventBus eventBus = EventBus.getInstance(); @@ -171,6 +176,9 @@ public final class ChatScene { contactLabel.setText(localDB.getUser().getName()); MessageControl.setUser(localDB.getUser()); if (!client.isOnline()) updateInfoLabel("You are offline", "infoLabel-info"); + + recorder = new AudioRecorder(); + if (!recorder.isSupported()) voiceButton.setDisable(true); } /** @@ -201,6 +209,10 @@ public final class ChatScene { logger.log(Level.WARNING, "Could not read current chat.", e); } + // Discard the pending attachment + // TODO: stop running recording + pendingAttachment = null; + remainingChars.setVisible(true); remainingChars .setText(String.format("remaining chars: %d/%d", MAX_MESSAGE_LENGTH - messageTextArea.getText().length(), MAX_MESSAGE_LENGTH)); @@ -230,6 +242,25 @@ public final class ChatScene { sceneContext.getController().initializeData(sceneContext, localDB); } + @FXML + private void voiceButtonClicked() { + new Thread(() -> { + try { + if (!recording) { + recording = true; + Platform.runLater(() -> voiceButton.setText("Recording...")); + recorder.start(); + } else { + pendingAttachment = new Attachment(recorder.finish(), AttachmentType.VOICE); + recording = false; + Platform.runLater(() -> { voiceButton.setText("Record Voice Message"); checkPostConditions(false); }); + } + } catch (EnvoyException e) { + e.printStackTrace(); + } + }).start(); + } + /** * Checks the text length of the {@code messageTextArea}, adjusts the * {@code remainingChars} label and checks whether to send the message @@ -252,11 +283,15 @@ public final class ChatScene { */ @FXML private void checkPostConditions(KeyEvent e) { + checkPostConditions(settings.isEnterToSend() && e.getCode() == KeyCode.ENTER + || !settings.isEnterToSend() && e.getCode() == KeyCode.ENTER && e.isControlDown()); + } + + private void checkPostConditions(boolean sendKeyPressed) { if (!postingPermanentlyDisabled) { - if (!postButton.isDisabled() && (settings.isEnterToSend() && e.getCode() == KeyCode.ENTER - || !settings.isEnterToSend() && e.getCode() == KeyCode.ENTER && e.isControlDown())) + if (!postButton.isDisabled() && sendKeyPressed) postMessage(); - postButton.setDisable(messageTextArea.getText().isBlank() || currentChat == null); + postButton.setDisable((messageTextArea.getText().isBlank() && pendingAttachment == null) || currentChat == null); } else { final var noMoreMessaging = "Go online to send messages"; if (!infoLabel.getText().equals(noMoreMessaging)) @@ -311,12 +346,14 @@ public final class ChatScene { return; } final var text = messageTextArea.getText().strip(); - if (text.isBlank()) throw new IllegalArgumentException("A message without visible text can not be sent."); try { // Create and send message - final var message = new MessageBuilder(localDB.getUser().getID(), currentChat.getRecipient().getID(), localDB.getIDGenerator()) - .setText(text) - .build(); + var builder = new MessageBuilder(localDB.getUser().getID(), currentChat.getRecipient().getID(), localDB.getIDGenerator()).setText(text); + if (pendingAttachment != null) { + builder.setAttachment(pendingAttachment); + pendingAttachment = null; + } + var message = builder.build(); // Send message writeProxy.writeMessage(message); diff --git a/src/main/resources/fxml/ChatScene.fxml b/src/main/resources/fxml/ChatScene.fxml index 8ed1185..d68b351 100644 --- a/src/main/resources/fxml/ChatScene.fxml +++ b/src/main/resources/fxml/ChatScene.fxml @@ -2,6 +2,7 @@ + @@ -12,40 +13,21 @@ - + - - - + + - - - - - + + + + + + - + @@ -55,14 +37,12 @@ - + -

+ * Project: envoy-client
+ * File: AudioPlayer.java
+ * Created: 05.07.2020
+ * + * @author Kai S. K. Engelbart + * @since Envoy Client v0.1-beta + */ +public final class AudioPlayer { + + private final AudioFormat format; + private final DataLine.Info info; + + private Clip clip; + + /** + * Initializes the player with the default audio format. + * + * @since Envoy Client v0.1-beta + */ + public AudioPlayer() { this(AudioRecorder.DEFAULT_AUDIO_FORMAT); } + + /** + * Initializes the player with a given audio format. + * + * @param format the audio format to use + * @since Envoy Client v0.1-beta + */ + public AudioPlayer(AudioFormat format) { + this.format = format; + info = new DataLine.Info(Clip.class, format); + } + + /** + * @return {@code true} if audio play back is supported + * @since Envoy Client v0.1-beta + */ + public boolean isSupported() { return AudioSystem.isLineSupported(info); } + + /** + * Plays back an audio clip. + * + * @param data the data of the clip + * @throws EnvoyException if the play back failed + * @since Envoy Client v0.1-beta + */ + public void play(byte[] data) throws EnvoyException { + try { + clip = (Clip) AudioSystem.getLine(info); + clip.open(format, data, 0, data.length); + clip.start(); + } catch (LineUnavailableException e) { + throw new EnvoyException("Cannot play back audio", e); + } + } +} diff --git a/src/main/java/envoy/client/data/AudioRecorder.java b/src/main/java/envoy/client/data/audio/AudioRecorder.java similarity index 59% rename from src/main/java/envoy/client/data/AudioRecorder.java rename to src/main/java/envoy/client/data/audio/AudioRecorder.java index 96b08a0..b55da93 100644 --- a/src/main/java/envoy/client/data/AudioRecorder.java +++ b/src/main/java/envoy/client/data/audio/AudioRecorder.java @@ -1,4 +1,4 @@ -package envoy.client.data; +package envoy.client.data.audio; import java.io.IOException; import java.nio.file.Files; @@ -20,21 +20,49 @@ import envoy.exception.EnvoyException; */ public final class AudioRecorder { - private final AudioFormat format; + /** + * The default audio format used for recording and play back. + * + * @since Envoy Client v0.1-beta + */ + public static final AudioFormat DEFAULT_AUDIO_FORMAT = new AudioFormat(16000, 16, 1, true, false); + + private final AudioFormat format; + private final DataLine.Info info; - private DataLine.Info info; private TargetDataLine line; private Path tempFile; - public AudioRecorder() { this(new AudioFormat(16000, 16, 1, true, false)); } + /** + * Initializes the recorder with the default audio format. + * + * @since Envoy Client v0.1-beta + */ + public AudioRecorder() { this(DEFAULT_AUDIO_FORMAT); } + /** + * Initializes the recorder with a given audio format. + * + * @param format the audio format to use + * @since Envoy Client v0.1-beta + */ public AudioRecorder(AudioFormat format) { this.format = format; info = new DataLine.Info(TargetDataLine.class, format); } + /** + * @return {@code true} if audio recording is supported + * @since Envoy Client v0.1-beta + */ public boolean isSupported() { return AudioSystem.isLineSupported(info); } + /** + * Starts the audio recording. + * + * @throws EnvoyException if starting the recording failed + * @since Envoy Client v0.1-beta + */ public void start() throws EnvoyException { try { @@ -54,6 +82,13 @@ public final class AudioRecorder { } } + /** + * Stops the recording. + * + * @return the finished recording + * @throws EnvoyException if finishing the recording failed + * @since Envoy Client v0.1-beta + */ public byte[] finish() throws EnvoyException { try { line.stop(); diff --git a/src/main/java/envoy/client/data/audio/package-info.java b/src/main/java/envoy/client/data/audio/package-info.java new file mode 100644 index 0000000..3a172be --- /dev/null +++ b/src/main/java/envoy/client/data/audio/package-info.java @@ -0,0 +1,11 @@ +/** + * Contains classes related to recording and playing back audio clips. + *

+ * Project: envoy-client
+ * File: package-info.java
+ * Created: 05.07.2020
+ * + * @author Kai S. K. Engelbart + * @since Envoy Client v0.1-beta + */ +package envoy.client.data.audio; diff --git a/src/main/java/envoy/client/ui/AudioControl.java b/src/main/java/envoy/client/ui/AudioControl.java new file mode 100644 index 0000000..698e315 --- /dev/null +++ b/src/main/java/envoy/client/ui/AudioControl.java @@ -0,0 +1,40 @@ +package envoy.client.ui; + +import javafx.scene.control.Button; +import javafx.scene.layout.HBox; + +import envoy.client.data.audio.AudioPlayer; +import envoy.exception.EnvoyException; + +/** + * Enables the play back of audio clips through a button. + *

+ * Project: envoy-client
+ * File: AudioControl.java
+ * Created: 05.07.2020
+ * + * @author Kai S. K. Engelbart + * @since Envoy Client v0.1-beta + */ +public final class AudioControl extends HBox { + + private AudioPlayer player = new AudioPlayer(); + + /** + * Initializes the audio control. + * + * @param audioData the audio data to play. + * @since Envoy Client v0.1-beta + */ + public AudioControl(byte[] audioData) { + var button = new Button("Play"); + button.setOnAction(e -> { + try { + player.play(audioData); + } catch (EnvoyException ex) { + + } + }); + getChildren().add(button); + } +} diff --git a/src/main/java/envoy/client/ui/controller/ChatScene.java b/src/main/java/envoy/client/ui/controller/ChatScene.java index 9dfcc25..38a5b9a 100644 --- a/src/main/java/envoy/client/ui/controller/ChatScene.java +++ b/src/main/java/envoy/client/ui/controller/ChatScene.java @@ -18,6 +18,7 @@ import javafx.scene.input.KeyEvent; import javafx.scene.paint.Color; import envoy.client.data.*; +import envoy.client.data.audio.AudioRecorder; import envoy.client.event.MessageCreationEvent; import envoy.client.net.Client; import envoy.client.net.WriteProxy; diff --git a/src/main/java/envoy/client/ui/listcell/MessageControl.java b/src/main/java/envoy/client/ui/listcell/MessageControl.java index d68acdd..7017f7c 100644 --- a/src/main/java/envoy/client/ui/listcell/MessageControl.java +++ b/src/main/java/envoy/client/ui/listcell/MessageControl.java @@ -9,7 +9,9 @@ import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.VBox; +import envoy.client.ui.AudioControl; import envoy.client.ui.IconUtil; +import envoy.data.Attachment.AttachmentType; import envoy.data.Message; import envoy.data.Message.MessageStatus; import envoy.data.User; @@ -38,6 +40,11 @@ public class MessageControl extends VBox { public MessageControl(Message message) { // Creating the underlying VBox, the dateLabel and the textLabel super(new Label(dateFormat.format(message.getCreationDate()))); + + // Voice attachment + if (message.hasAttachment() && message.getAttachment().getType() == AttachmentType.VOICE) + getChildren().add(new AudioControl(message.getAttachment().getData())); + final var textLabel = new Label(message.getText()); textLabel.setWrapText(true); getChildren().add(textLabel); diff --git a/src/main/resources/fxml/ChatScene.fxml b/src/main/resources/fxml/ChatScene.fxml index d68b351..5638c7a 100644 --- a/src/main/resources/fxml/ChatScene.fxml +++ b/src/main/resources/fxml/ChatScene.fxml @@ -13,21 +13,35 @@ - + - - + + - - - - - - + + + + + + - + @@ -37,12 +51,14 @@ - + -