diff --git a/src/main/java/net/oijon/algonquin/gui/GUI.java b/src/main/java/net/oijon/algonquin/gui/GUI.java index c2705a3..ffa9ec0 100644 --- a/src/main/java/net/oijon/algonquin/gui/GUI.java +++ b/src/main/java/net/oijon/algonquin/gui/GUI.java @@ -1,5 +1,17 @@ package net.oijon.algonquin.gui; +import java.io.BufferedInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; + +import javax.sound.sampled.AudioInputStream; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.Clip; +import javax.sound.sampled.LineUnavailableException; +import javax.sound.sampled.UnsupportedAudioFileException; + import javafx.application.Application; import javafx.collections.FXCollections; import javafx.collections.ObservableList; @@ -17,6 +29,8 @@ import javafx.scene.layout.VBox; import javafx.stage.Stage; import net.oijon.algonquin.tts.IPA; +import net.oijon.algonquin.tts.trm.TRM; +import net.oijon.algonquin.tts.trm.Tube; public class GUI extends Application { @@ -59,7 +73,54 @@ public void handle(ActionEvent event) { if (synthType.getValue().equals("Classic")) { console.setText(IPA.createAudio(IPA.getFileNames(insert.getText()))); } else if (synthType.getValue().equals("TRM (highly experimental)")) { - console.setText("Sorry, but TRM isn't quite ready yet. However, you can use classic instead!"); + + //String consoleResult = "Generated from a value of 1: " + Float.toString(tube.generate(1)) + "\n"; + //consoleResult += "Generated from a value of 2: " + Float.toString(tube.generate(2)) + "\n"; + //consoleResult += "Generated from a value of 1.5: " + Float.toString(tube.generate((float) 1.5)) + "\n"; + //consoleResult += "Generated from a value of 10: " + Float.toString(tube.generate(10)) + "\n"; + //consoleResult += "Generated from a value of 30: " + Float.toString(tube.generate(30)) + "\n"; + String message = "This is currently under development and is highly experimental!\n"; + message += "However, below you will find debug information to help more easially develop this!\n"; + message += "-----BEGIN TESTTUBE-----\n"; + message += TRM.createTestTube() + "\n"; + message += "-----END TESTTUBE-----\n"; + message += "-----BEGIN SINEWAVE-----\n"; + message += "Now, a .wav file will be created of a pure vocal output. This should be just a sine wave.\n"; + try { + message += TRM.createTestWave(); + } catch (IOException e) { + // TODO Auto-generated catch block + message += e.toString() + "\n"; + } + message += "Now it will play!\n"; + try { + Clip clip = AudioSystem.getClip(); + AudioInputStream ais = AudioSystem.getAudioInputStream( + new File(System.getProperty("user.home") + "/AlgonquinTTS/testwave.wav").getAbsoluteFile() + ); + clip.open(ais); + clip.start(); + long fileLength = clip.getMicrosecondLength(); + while(clip.getMicrosecondLength() != clip.getMicrosecondPosition()) + { + } + } + catch (UnsupportedAudioFileException e) { + message += "Unsupported audio format: '" + System.getProperty("user.home") + "/AlgonquinTTS/testwave.wav" + "' - " + e.toString() + "\n"; + } + catch (LineUnavailableException e) { + message += "Could not play '" + System.getProperty("user.home") + "/AlgonquinTTS/testwave.wav" + "' - " + e.toString() + "\n"; + } + catch (IOException e) { + message += "Could not play '" + System.getProperty("user.home") + "/AlgonquinTTS/testwave.wav" + "' - " + e.toString() + "\n"; + e.printStackTrace(); + } + message += "If all went to plan, and you did not see any exceptions, you just heard the beep that will form the voicebox's raw output!\n"; + message += "Of course, this won't be what the end product sounds like, the end product will have to go through several tubes.\n"; + message += "These tubes represent parts of the mouth and nasal cavity, which should be able to approximate the sound of speaking!\n"; + message += "-----END SINEWAVE-----\n"; + console.setText(message); + } else { console.setText("Unsupported synthesis type \'" + synthType.getValue() + "\'."); } diff --git a/src/main/java/net/oijon/algonquin/gui/GUILauncher.java b/src/main/java/net/oijon/algonquin/gui/GUILauncher.java index 52f7bcf..4a25564 100644 --- a/src/main/java/net/oijon/algonquin/gui/GUILauncher.java +++ b/src/main/java/net/oijon/algonquin/gui/GUILauncher.java @@ -2,7 +2,7 @@ import java.io.IOException; -import net.oijon.algonquin.tts.TRM; +import net.oijon.algonquin.tts.trm.TRM; public class GUILauncher { diff --git a/src/main/java/net/oijon/algonquin/tts/IPA.java b/src/main/java/net/oijon/algonquin/tts/IPA.java index ba37f49..7def3e8 100644 --- a/src/main/java/net/oijon/algonquin/tts/IPA.java +++ b/src/main/java/net/oijon/algonquin/tts/IPA.java @@ -130,6 +130,7 @@ public static String createAudio(String[] fileNames) { while(clip.getMicrosecondLength() != clip.getMicrosecondPosition()) { } + is.close(); } catch (UnsupportedAudioFileException e) { exception = "unsupported audio format: '" + fileNames[i] + "' - " + e.toString() + "\n"; diff --git a/src/main/java/net/oijon/algonquin/tts/trm/TRM.java b/src/main/java/net/oijon/algonquin/tts/trm/TRM.java new file mode 100644 index 0000000..1212657 --- /dev/null +++ b/src/main/java/net/oijon/algonquin/tts/trm/TRM.java @@ -0,0 +1,160 @@ +package net.oijon.algonquin.tts.trm; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.util.Arrays; + +import javax.sound.sampled.AudioFileFormat; +import javax.sound.sampled.AudioFormat; +import javax.sound.sampled.AudioInputStream; +import javax.sound.sampled.AudioSystem; + +// Highly experimental. +public class TRM { + + public static void makeTestSound() throws IOException { + final double sampleRate = 44100.0; + final double frequency = 440; + final double frequency2 = 90; + final double amplitude = 1.0; + final double seconds = 2.0; + final double twoPiF = 2 * Math.PI * frequency; + final double piF = Math.PI * frequency2; + + float[] buffer = new float[(int)(seconds * sampleRate)]; + + for (int sample = 0; sample < buffer.length; sample++) { + double time = sample / sampleRate; + buffer[sample] = (float)(amplitude * Math.cos(piF * time) * Math.sin(twoPiF * time)); + } + + final byte[] byteBuffer = new byte[buffer.length * 2]; + + int bufferIndex = 0; + for (int i = 0; i < byteBuffer.length; i++) { + final int x = (int)(buffer[bufferIndex++] * 32767.0); + + byteBuffer[i++] = (byte)x; + byteBuffer[i] = (byte)(x >>> 8); + } + + + if (new File(System.getProperty("user.home") + "/AlgonquinTTS").exists() == false) { + new File(System.getProperty("user.home") + "/AlgonquinTTS").mkdir(); + } + + File out = new File(System.getProperty("user.home") + "/AlgonquinTTS/out10.wav"); + + final boolean bigEndian = false; + final boolean signed = true; + + final int bits = 16; + final int channels = 1; + + AudioFormat format = new AudioFormat((float)sampleRate, bits, channels, signed, bigEndian); + ByteArrayInputStream bais = new ByteArrayInputStream(byteBuffer); + AudioInputStream audioInputStream = new AudioInputStream(bais, format, buffer.length); + AudioSystem.write(audioInputStream, AudioFileFormat.Type.WAVE, out); + audioInputStream.close(); + } + + public static String createTestTube() { + String exception = ""; + Tube testTube = new Tube(17); + Voicebox voiceBox = new Voicebox(5); + + exception += "\nVoicebox wave: "; + + for (int i = 0; i < testTube.getSampleRate()*2; i++) { + exception += voiceBox.frame() + ", "; + } + + exception += ("outwards elements: " + Arrays.toString(testTube.out()) + "\n"); + exception += "Full array: " + "\n"; + for (int i = 0; i < testTube.getSampleRate(); i++) { + exception += testTube.getDelayLine()[i][0] + ", "; + } + exception += "\n"; + + for (int i = 0; i < testTube.getSampleRate(); i++) { + exception += testTube.getDelayLine()[i][1] + ", "; + } + + return exception; + } + + public static String createTestWave() throws IOException { + String exception = ""; + + Voicebox voiceBox = new Voicebox(1); + + final double sampleRate = 44100.0; + final double seconds = 2.0; + + exception += "Sample Rate: " + sampleRate + "\n"; + exception += "Amplitude: 1\n"; + exception += "Seconds: " + seconds + "\n"; + + + float[] buffer = new float[(int)(seconds * sampleRate)]; + + for (int sample = 0; sample < buffer.length; sample++) { + buffer[sample] = voiceBox.frame(); + } + + final byte[] byteBuffer = new byte[buffer.length * 2]; + + int bufferIndex = 0; + for (int i = 0; i < byteBuffer.length; i++) { + final int x = (int)(buffer[bufferIndex++] * 32767.0); + + byteBuffer[i++] = (byte)x; + byteBuffer[i] = (byte)(x >>> 8); + } + + + if (new File(System.getProperty("user.home") + "/AlgonquinTTS").exists() == false) { + new File(System.getProperty("user.home") + "/AlgonquinTTS").mkdir(); + exception += "Created new directory in " + System.getProperty("user.home") + "/AlgonquinTTS" + "\n"; + } + + File out = new File(System.getProperty("user.home") + "/AlgonquinTTS/testwave.wav"); + exception += "Created new file in " + System.getProperty("user.home") + "/AlgonquinTTS/testwave.wav" + "\n"; + + final boolean bigEndian = false; + final boolean signed = true; + + exception += "Big Endian: " + bigEndian + "\n"; + exception += "Signed: " + signed + "\n"; + + final int bits = 16; + final int channels = 1; + + exception += "Bits: " + bits + "\n"; + exception += "Channels: " + channels + "\n"; + + AudioFormat format = new AudioFormat((float)sampleRate, bits, channels, signed, bigEndian); + ByteArrayInputStream bais = new ByteArrayInputStream(byteBuffer); + AudioInputStream audioInputStream = new AudioInputStream(bais, format, buffer.length); + AudioSystem.write(audioInputStream, AudioFileFormat.Type.WAVE, out); + audioInputStream.close(); + + return exception; + } + + public static String createSchwaTubeTest() { + String exception = ""; + + int sampleRate = 2018; // L = c/F, where L is the length (17cm), c is the speed of sound (cm/s), and F is the sampling frequency + float delayLine[][] = new float[sampleRate][2]; + for (int i = 0; i < 2; i++) { + for (int j = 0; j < sampleRate; j++) { + delayLine[j][i] = (delayLine[j-1][i]); + } + } + + return exception; + } + +} diff --git a/src/main/java/net/oijon/algonquin/tts/trm/Tube.java b/src/main/java/net/oijon/algonquin/tts/trm/Tube.java new file mode 100644 index 0000000..0894e7e --- /dev/null +++ b/src/main/java/net/oijon/algonquin/tts/trm/Tube.java @@ -0,0 +1,90 @@ +package net.oijon.algonquin.tts.trm; + +public class Tube { + + private int length; + private int sampleRate; + private final int speedOfSound = 34300; //in cm/s + private double loss = 0.97; //3% loss + private float[][] delayLine; + + public Tube (int length) { + this.length = length; + // L = c/F, where L is the length (17cm), c is the speed of sound (cm/s), and F is the sampling frequency + sampleRate = speedOfSound/this.length; + //Creates the delay array based off + delayLine = new float[sampleRate][2]; + } + + //create and manage a delay line + //returns false if there is still room before sound makes its way out + //returns true if its pushing the sound out to the next tube + //this is done by checking if the last part of the array for the sample rate is filled. + public boolean push(float pushAmount) { + for (int i = 0; i < sampleRate-1; i++) { + delayLine[i+1][0] = delayLine[i][0]; + } + delayLine[0][0] = pushAmount; + delayLine[sampleRate-1][1] = -1*delayLine[sampleRate-1][0]; + for (int i = sampleRate-1; i > 0; i--) { + delayLine[i-1][0] = delayLine[i][0]; + } + if (Float.isNaN(delayLine[sampleRate-1][0])) { + return false; + } else { + return true; + } + } + + //convert to 44.1 kHz by adding new data to follow the slope. + //i could very well be misunderstanding something about this in the paper by Manzara + //if so, this method will probably be replaced. + public float[][] standardize() { + float[][] standard = new float[44100][2]; + int multiple = (int)(44100/sampleRate); + for (int h = 0; h < 2; h++) { + for (int i = 0; i < sampleRate; i++) { + float slope = 1; + if (i+1 < sampleRate) { + slope = (delayLine[i+1][h]-delayLine[i][h])/((i+1)-i); + } else { + slope = (delayLine[sampleRate-1][h]-delayLine[i][h])/((sampleRate-1)-i); + } + for (int j = 0; j < multiple; j++) { + //could be optimized more + if (j == 0) { + standard[i*multiple][h] = delayLine[i][h]; + } else { + standard[j][h] = standard[j-1][h]*slope; + } + } + } + } + + return standard; + } + + public float[] out() { + float[] out = new float[2]; + out[0] = delayLine[sampleRate-1][0]; + out[1] = delayLine[sampleRate-1][1]; + return out; + } + + //debug only, should not be used in production + public float[][] getDelayLine() { + return delayLine; + } + public int getLength() { + return length; + } + public int getSampleRate() { + return sampleRate; + } + public double getLoss() { + return loss; + } + public void setLoss(double loss) { + this.loss = loss; + } +} diff --git a/src/main/java/net/oijon/algonquin/tts/trm/Voicebox.java b/src/main/java/net/oijon/algonquin/tts/trm/Voicebox.java new file mode 100644 index 0000000..02b5832 --- /dev/null +++ b/src/main/java/net/oijon/algonquin/tts/trm/Voicebox.java @@ -0,0 +1,26 @@ +package net.oijon.algonquin.tts.trm; + +public class Voicebox { + + private float intensity; + private int frameCount = 0; + + public Voicebox(float intensity) { + this.intensity = intensity; + } + public float frame() { + float amount = (float) ((Math.sin(frameCount*(float)(1F/8F)))*intensity); + frameCount++; + return amount; + } + public int getFrameCount() { + return frameCount; + } + public float getIntensity() { + return intensity; + } + public void setIntensity(float intensity) { + this.intensity = intensity; + } + +}