diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs index 5e625c6..65c71af 100644 --- a/.settings/org.eclipse.jdt.core.prefs +++ b/.settings/org.eclipse.jdt.core.prefs @@ -245,7 +245,7 @@ org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type=insert org.eclipse.jdt.core.formatter.insert_new_line_after_label=insert org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert org.eclipse.jdt.core.formatter.insert_new_line_after_type_annotation=do not insert -org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=insert org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=do not insert org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=do not insert @@ -439,7 +439,7 @@ org.eclipse.jdt.core.formatter.keep_code_block_on_one_line=one_line_if_empty org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=true org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false org.eclipse.jdt.core.formatter.keep_enum_constant_declaration_on_one_line=one_line_never -org.eclipse.jdt.core.formatter.keep_enum_declaration_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_enum_declaration_on_one_line=one_line_if_empty org.eclipse.jdt.core.formatter.keep_if_then_body_block_on_one_line=one_line_if_single_item org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=false org.eclipse.jdt.core.formatter.keep_lambda_body_block_on_one_line=one_line_always @@ -450,7 +450,7 @@ org.eclipse.jdt.core.formatter.keep_simple_for_body_on_same_line=false org.eclipse.jdt.core.formatter.keep_simple_getter_setter_on_one_line=true org.eclipse.jdt.core.formatter.keep_simple_while_body_on_same_line=false org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=true -org.eclipse.jdt.core.formatter.keep_type_declaration_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_type_declaration_on_one_line=one_line_if_empty org.eclipse.jdt.core.formatter.lineSplit=150 org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false diff --git a/src/main/java/envoy/client/data/audio/AudioPlayer.java b/src/main/java/envoy/client/data/audio/AudioPlayer.java new file mode 100644 index 0000000..61982db --- /dev/null +++ b/src/main/java/envoy/client/data/audio/AudioPlayer.java @@ -0,0 +1,64 @@ +package envoy.client.data.audio; + +import javax.sound.sampled.*; + +import envoy.exception.EnvoyException; + +/** + * Plays back audio from a byte array. + *
+ * 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 (final LineUnavailableException e) {
+ throw new EnvoyException("Cannot play back audio", e);
+ }
+ }
+}
diff --git a/src/main/java/envoy/client/data/audio/AudioRecorder.java b/src/main/java/envoy/client/data/audio/AudioRecorder.java
new file mode 100644
index 0000000..85dafae
--- /dev/null
+++ b/src/main/java/envoy/client/data/audio/AudioRecorder.java
@@ -0,0 +1,122 @@
+package envoy.client.data.audio;
+
+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 {
+
+ /**
+ * 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 TargetDataLine line;
+ private Path tempFile;
+
+ /**
+ * 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); }
+
+ /**
+ * @return {@code true} if the recorder is active
+ * @since Envoy Client v0.1-beta
+ */
+ public boolean isRecording() { return line != null && line.isActive(); }
+
+ /**
+ * Starts the audio recording.
+ *
+ * @throws EnvoyException if starting the recording failed
+ * @since Envoy Client v0.1-beta
+ */
+ 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
+ final var ais = new AudioInputStream(line);
+ AudioSystem.write(ais, AudioFileFormat.Type.WAVE, tempFile.toFile());
+ } catch (IOException | LineUnavailableException e) {
+ throw new EnvoyException("Cannot record voice", e);
+ }
+ }
+
+ /**
+ * 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();
+ line.close();
+ final byte[] data = Files.readAllBytes(tempFile);
+ Files.delete(tempFile);
+ return data;
+ } catch (final IOException e) {
+ throw new EnvoyException("Cannot save voice recording", e);
+ }
+ }
+
+ /**
+ * Cancels the active recording.
+ *
+ * @since Envoy Client v0.1-beta
+ */
+ public void cancel() {
+ line.stop();
+ line.close();
+ try {
+ Files.deleteIfExists(tempFile);
+ } catch (IOException e) {}
+ }
+}
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/net/Receiver.java b/src/main/java/envoy/client/net/Receiver.java
index c71e5ed..bfd231c 100644
--- a/src/main/java/envoy/client/net/Receiver.java
+++ b/src/main/java/envoy/client/net/Receiver.java
@@ -51,16 +51,25 @@ public class Receiver extends Thread {
@Override
public void run() {
- try {
- while (true) {
+ while (true) {
+ try {
// Read object length
final byte[] lenBytes = new byte[4];
in.read(lenBytes);
final int len = SerializationUtils.bytesToInt(lenBytes, 0);
+ logger.log(Level.FINEST, "Expecting object of length " + len + ".");
// Read object into byte array
- final byte[] objBytes = new byte[len];
- in.read(objBytes);
+ final byte[] objBytes = new byte[len];
+ final int bytesRead = in.read(objBytes);
+ logger.log(Level.FINEST, "Read " + bytesRead + " bytes.");
+
+ // Catch LV encoding errors
+ if (len != bytesRead) {
+ logger.log(Level.WARNING,
+ String.format("LV encoding violated: expected %d bytes, received %d bytes. Discarding object...", len, bytesRead));
+ continue;
+ }
try (ObjectInputStream oin = new ObjectInputStream(new ByteArrayInputStream(objBytes))) {
final Object obj = oin.readObject();
@@ -69,16 +78,16 @@ public class Receiver extends Thread {
// Get appropriate processor
@SuppressWarnings("rawtypes")
final Consumer processor = processors.get(obj.getClass());
- if (processor == null)
- logger.log(Level.WARNING, String.format(
- "The received object has the class %s for which no processor is defined.", obj.getClass()));
+ if (processor == null) logger.log(Level.WARNING,
+ String.format("The received object has the class %s for which no processor is defined.", obj.getClass()));
else processor.accept(obj);
}
+ } catch (final SocketException e) {
+ // Connection probably closed by client.
+ return;
+ } catch (final Exception e) {
+ logger.log(Level.SEVERE, "Error on receiver thread", e);
}
- } catch (final SocketException e) {
- // Connection probably closed by client.
- } catch (final Exception e) {
- logger.log(Level.SEVERE, "Error on receiver thread", e);
}
}
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..d82fd24
--- /dev/null
+++ b/src/main/java/envoy/client/ui/AudioControl.java
@@ -0,0 +1,49 @@
+package envoy.client.ui;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javafx.scene.control.Alert;
+import javafx.scene.control.Alert.AlertType;
+import javafx.scene.control.Button;
+import javafx.scene.layout.HBox;
+
+import envoy.client.data.audio.AudioPlayer;
+import envoy.exception.EnvoyException;
+import envoy.util.EnvoyLog;
+
+/**
+ * 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();
+
+ private static final Logger logger = EnvoyLog.getLogger(AudioControl.class);
+
+ /**
+ * 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) {
+ logger.log(Level.SEVERE, "Could not play back audio: ", ex);
+ new Alert(AlertType.ERROR, "Could not play back audio").showAndWait();
+ }
+ });
+ 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 5ed306a..26f15df 100644
--- a/src/main/java/envoy/client/ui/controller/ChatScene.java
+++ b/src/main/java/envoy/client/ui/controller/ChatScene.java
@@ -20,6 +20,7 @@ import javafx.scene.paint.Color;
import envoy.client.data.Chat;
import envoy.client.data.LocalDB;
import envoy.client.data.Settings;
+import envoy.client.data.audio.AudioRecorder;
import envoy.client.event.MessageCreationEvent;
import envoy.client.net.Client;
import envoy.client.net.WriteProxy;
@@ -30,10 +31,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;
/**
@@ -58,6 +61,9 @@ public final class ChatScene implements Restorable {
@FXML
private Button postButton;
+ @FXML
+ private Button voiceButton;
+
@FXML
private Button settingsButton;
@@ -78,9 +84,11 @@ public final class ChatScene implements Restorable {
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();
@@ -172,6 +180,8 @@ public final class ChatScene implements Restorable {
contactLabel.setText(localDB.getUser().getName());
MessageControl.setUser(localDB.getUser());
if (!client.isOnline()) updateInfoLabel("You are offline", "infoLabel-info");
+
+ recorder = new AudioRecorder();
}
@Override
@@ -205,11 +215,20 @@ public final class ChatScene implements Restorable {
logger.log(Level.WARNING, "Could not read current chat.", e);
}
+ // Discard the pending attachment
+ if (recorder.isRecording()) {
+ recorder.cancel();
+ recording = false;
+ voiceButton.setText("Record Voice Message");
+ }
+ pendingAttachment = null;
+
remainingChars.setVisible(true);
remainingChars
.setText(String.format("remaining chars: %d/%d", MAX_MESSAGE_LENGTH - messageTextArea.getText().length(), MAX_MESSAGE_LENGTH));
}
messageTextArea.setDisable(currentChat == null || postingPermanentlyDisabled);
+ voiceButton.setDisable(!recorder.isSupported());
}
/**
@@ -234,6 +253,26 @@ public final class ChatScene implements Restorable {
sceneContext.