From 8d587fff55f51c8d32245b1c3c9b27e995e818fb Mon Sep 17 00:00:00 2001 From: Tyler A Date: Wed, 2 Oct 2013 06:03:45 -0700 Subject: [PATCH] Initial commit of Alter --- Alter.iml | 13 ++ src/META-INF/MANIFEST.MF | 3 + src/tarehart/alter/AlterForm.form | 124 +++++++++++++++ src/tarehart/alter/AlterForm.java | 147 ++++++++++++++++++ .../alter/AmplitudeUpdateListener.java | 7 + src/tarehart/alter/AudioSystemHelper.java | 34 ++++ src/tarehart/alter/KeyGrabber.java | 29 ++++ src/tarehart/alter/KeyPresser.java | 66 ++++++++ src/tarehart/alter/MicrophoneAnalyzer.java | 101 ++++++++++++ src/tarehart/alter/TalkingJudge.java | 56 +++++++ src/tarehart/alter/resources/alter.png | Bin 0 -> 8459 bytes 11 files changed, 580 insertions(+) create mode 100644 Alter.iml create mode 100644 src/META-INF/MANIFEST.MF create mode 100644 src/tarehart/alter/AlterForm.form create mode 100644 src/tarehart/alter/AlterForm.java create mode 100644 src/tarehart/alter/AmplitudeUpdateListener.java create mode 100644 src/tarehart/alter/AudioSystemHelper.java create mode 100644 src/tarehart/alter/KeyGrabber.java create mode 100644 src/tarehart/alter/KeyPresser.java create mode 100644 src/tarehart/alter/MicrophoneAnalyzer.java create mode 100644 src/tarehart/alter/TalkingJudge.java create mode 100644 src/tarehart/alter/resources/alter.png diff --git a/Alter.iml b/Alter.iml new file mode 100644 index 0000000..eb373e1 --- /dev/null +++ b/Alter.iml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/META-INF/MANIFEST.MF b/src/META-INF/MANIFEST.MF new file mode 100644 index 0000000..0a5f95a --- /dev/null +++ b/src/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Main-Class: tarehart.alter.AlterForm + diff --git a/src/tarehart/alter/AlterForm.form b/src/tarehart/alter/AlterForm.form new file mode 100644 index 0000000..ea2b771 --- /dev/null +++ b/src/tarehart/alter/AlterForm.form @@ -0,0 +1,124 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/tarehart/alter/AlterForm.java b/src/tarehart/alter/AlterForm.java new file mode 100644 index 0000000..af61500 --- /dev/null +++ b/src/tarehart/alter/AlterForm.java @@ -0,0 +1,147 @@ +package tarehart.alter; + +import javax.sound.sampled.LineUnavailableException; +import javax.sound.sampled.Mixer; +import javax.swing.*; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import java.awt.*; +import java.awt.event.*; +import java.net.URL; +import java.util.List; + + +public class AlterForm { + + public static final int MAX_VOLUME = 50; + public static final int GRACE_PERIOD = 1000; // 1000 milliseconds = 1 second + private TalkingJudge judge; + private KeyPresser presser; + private MicrophoneAnalyzer microphoneAnalyzer; + + public AlterForm() throws AWTException { + + microphoneAnalyzer = new MicrophoneAnalyzer(); + setupMicrophones(); + + presser = new KeyPresser(); + judge = new TalkingJudge(presser, GRACE_PERIOD); + presser.setKey(KeyEvent.VK_ALT); + spinner1.setValue(KeyEvent.VK_ALT); + + setupKeyBinder(); + + progressBar1.setMaximum(MAX_VOLUME); + slider1.setMaximum(MAX_VOLUME); + + slider1.setValue(3); + + microphoneAnalyzer.addListener(new AmplitudeUpdateListener() { + @Override + public void amplitudeUpdated(float newAmplitude) { + int level = (int) newAmplitude; + progressBar1.setValue(level); + if (level >= slider1.getValue()) { + judge.gainSound(); + statusLight.setBackground(Color.green); + } else { + judge.loseSound(); + } + + if (!judge.hearsTalking()) { + statusLight.setBackground(Color.darkGray); + } + } + }); + + + spinner1.addChangeListener(new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + presser.setKey((Integer) spinner1.getValue()); + } + }); + + } + + private void setupKeyBinder() { + button1.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + KeyGrabber.grabNextKey(spinner1); + } + }); + } + + private void setupMicrophones() { + + comboBox1.setRenderer(new ListCellRenderer() { + @Override + public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { + DefaultListCellRenderer renderer = new DefaultListCellRenderer(); + Mixer.Info mixer = (Mixer.Info) value; + return renderer.getListCellRendererComponent(list, mixer.getName(), index, isSelected, cellHasFocus); + } + }); + + List mixers = AudioSystemHelper.ListAudioInputDevices(); + for (Mixer.Info mixer: mixers) { + comboBox1.addItem(mixer); + } + + try { + microphoneAnalyzer.setMixer((Mixer.Info) comboBox1.getItemAt(0)); + } catch (LineUnavailableException e) { + e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates. + } + + comboBox1.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + if (e.getStateChange() == ItemEvent.SELECTED) { + Mixer.Info mixer = (Mixer.Info) e.getItem(); + try { + microphoneAnalyzer.setMixer(mixer); + } catch (LineUnavailableException e1) { + e1.printStackTrace(); + } + } + } + }); + } + + public static void main(String[] args) { + + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (Exception e) { + // Essen + } + JFrame frame = new JFrame("Alt'er"); + URL url = ClassLoader.getSystemResource("tarehart/alter/resources/alter.png"); + Image img = Toolkit.getDefaultToolkit().createImage(url); + frame.setIconImage(img); + + try { + AlterForm m = new AlterForm(); + frame.setContentPane(m.rootPanel); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.pack(); + frame.setLocationRelativeTo(null); + frame.setVisible(true); + } catch (AWTException e) { + e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates. + } + + + } + + private JProgressBar progressBar1; + private JSlider slider1; + private JPanel rootPanel; + private JButton button1; + private JSpinner spinner1; + private JPanel statusLight; + private JComboBox comboBox1; + private JTextPane thisAppWillTakeTextPane; +} diff --git a/src/tarehart/alter/AmplitudeUpdateListener.java b/src/tarehart/alter/AmplitudeUpdateListener.java new file mode 100644 index 0000000..67016e6 --- /dev/null +++ b/src/tarehart/alter/AmplitudeUpdateListener.java @@ -0,0 +1,7 @@ +package tarehart.alter; + +public interface AmplitudeUpdateListener { + + public void amplitudeUpdated(float newAmplitude); + +} diff --git a/src/tarehart/alter/AudioSystemHelper.java b/src/tarehart/alter/AudioSystemHelper.java new file mode 100644 index 0000000..0d62ff7 --- /dev/null +++ b/src/tarehart/alter/AudioSystemHelper.java @@ -0,0 +1,34 @@ +package tarehart.alter; + +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.Line; +import javax.sound.sampled.Mixer; +import javax.sound.sampled.TargetDataLine; +import java.util.ArrayList; +import java.util.List; + +/** + * Some code borrowed from this tutorial: + * http://www.technogumbo.com/tutorials/Java-Microphone-Selection-And-Level-Monitoring/Java-Microphone-Selection-And-Level-Monitoring.php + */ +public class AudioSystemHelper { + + public static final Line.Info targetDLInfo = new Line.Info(TargetDataLine.class); + + public static List ListAudioInputDevices() { + List returnList = new ArrayList(); + Mixer.Info[] mixerInfo; + + mixerInfo = AudioSystem.getMixerInfo(); + + for(int i = 0; i < mixerInfo.length; i++) { + Mixer currentMixer = AudioSystem.getMixer(mixerInfo[i]); + + if( currentMixer.isLineSupported(targetDLInfo) ) { + returnList.add( mixerInfo[i] ); + } + } + + return returnList; + } +} diff --git a/src/tarehart/alter/KeyGrabber.java b/src/tarehart/alter/KeyGrabber.java new file mode 100644 index 0000000..d100e10 --- /dev/null +++ b/src/tarehart/alter/KeyGrabber.java @@ -0,0 +1,29 @@ +package tarehart.alter; + +import javax.swing.*; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; + +public class KeyGrabber { + + public static void grabNextKey(final JSpinner listenMe) { + + listenMe.grabFocus(); + + listenMe.addKeyListener(new KeyListener() { + @Override + public void keyTyped(KeyEvent e) { } + + @Override + public void keyPressed(KeyEvent e) { + int keyCode = e.getKeyCode(); + listenMe.setValue(keyCode); + listenMe.removeKeyListener(this); + } + + @Override + public void keyReleased(KeyEvent e) { } + }); + } + +} diff --git a/src/tarehart/alter/KeyPresser.java b/src/tarehart/alter/KeyPresser.java new file mode 100644 index 0000000..86a41a5 --- /dev/null +++ b/src/tarehart/alter/KeyPresser.java @@ -0,0 +1,66 @@ +package tarehart.alter; + +import java.awt.*; + +public class KeyPresser { + + private Robot robot; + private int key; + private boolean isKeyDown; + + + public KeyPresser() throws AWTException { + this.robot = new Robot(); + } + + public void setKey(int key) { + boolean wasDown = false; + + if (isKeyDown) { + wasDown = true; + release(); + } + + this.key = key; + + if (wasDown) { + beginHold(); + } + } + + public int getKey() { + return key; + } + + public boolean beginHold() { + if (!isKeyDown) { + try { + robot.keyPress(key); + isKeyDown = true; + return true; + } catch (IllegalArgumentException e) { + e.printStackTrace(); + } + } + return false; + } + + public boolean release() { + + if (isKeyDown) { + + try { + robot.keyRelease(key); + isKeyDown = false; + return true; + } catch (IllegalArgumentException e) { + e.printStackTrace(); + } + } + return false; + } + + public boolean isPressing() { + return isKeyDown; + } +} diff --git a/src/tarehart/alter/MicrophoneAnalyzer.java b/src/tarehart/alter/MicrophoneAnalyzer.java new file mode 100644 index 0000000..68b33db --- /dev/null +++ b/src/tarehart/alter/MicrophoneAnalyzer.java @@ -0,0 +1,101 @@ +package tarehart.alter; + +import javax.sound.sampled.*; +import java.util.LinkedList; +import java.util.List; + +/** + * Some code borrowed from this tutorial: + * http://www.technogumbo.com/tutorials/Java-Microphone-Selection-And-Level-Monitoring/Java-Microphone-Selection-And-Level-Monitoring.php + */ +public class MicrophoneAnalyzer { + + private TargetDataLine microphone; + + private boolean stopCapture = false; + private boolean threadEnded = true; + + private List listeners; + + public MicrophoneAnalyzer() { + + listeners = new LinkedList(); + + } + + public void addListener(AmplitudeUpdateListener listener) { + listeners.add(listener); + } + + public void setMixer(Mixer.Info info) throws LineUnavailableException { + + killExistingThread(); + + Mixer mixer = AudioSystem.getMixer(info); + microphone = (TargetDataLine)mixer.getLine(AudioSystemHelper.targetDLInfo); + + AudioFormat format = new AudioFormat(8000.0f, 8, 1, true, true); + microphone.open(format); + microphone.start(); + + Thread captureThread = new CaptureThread(); + captureThread.start(); + } + + private void killExistingThread() { + stopCapture = true; + while (!threadEnded) { + try { + Thread.sleep(50); + } catch (InterruptedException e) { } + } + } + + + private float calculateRMSLevel(byte[] audioData) { + // audioData might be buffered data read from a data line + long lSum = 0; + for(int i = 0; i < audioData.length; i++) { + lSum = lSum + audioData[i]; + } + + double dAvg = lSum / audioData.length; + + double sumMeanSquare = 0d; + for(int j = 0; j < audioData.length; j++) { + sumMeanSquare = sumMeanSquare + Math.pow(audioData[j] - dAvg, 2d); + } + + double averageMeanSquare = sumMeanSquare / audioData.length; + return (float)(Math.pow(averageMeanSquare, 0.5) + 0.5); + } + + + class CaptureThread extends Thread{ + + //An arbitrary-size temporary holding buffer + byte tempBuffer[] = new byte[100]; + public void run(){ + + threadEnded = false; + stopCapture = false; + try{ + while(!stopCapture) { + int cnt = microphone.read(tempBuffer, 0, tempBuffer.length); + if(cnt > 0){ + float currentLevel = calculateRMSLevel(tempBuffer); + for (AmplitudeUpdateListener aul: listeners) { + aul.amplitudeUpdated(currentLevel); + } + } + } + + microphone.close(); + threadEnded = true; + } catch (Exception e) { + System.out.println(e); + System.exit(0); + } + } + } +} diff --git a/src/tarehart/alter/TalkingJudge.java b/src/tarehart/alter/TalkingJudge.java new file mode 100644 index 0000000..f4615c2 --- /dev/null +++ b/src/tarehart/alter/TalkingJudge.java @@ -0,0 +1,56 @@ +package tarehart.alter; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +public class TalkingJudge { + + private int gracePeriod; + private KeyPresser presser; + private Timer timer; + + public TalkingJudge(KeyPresser presser, int gracePeriod) throws AWTException { + this.gracePeriod = gracePeriod; + this.presser = presser; + + setupTimer(); + + } + + private void setupTimer() { + timer = new Timer(gracePeriod, new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + presser.release(); + } + }); + + timer.setRepeats(false); + } + + public void gainSound() { + if (timer.isRunning()) { + timer.stop(); + } else { + presser.beginHold(); + } + } + + public void loseSound() { + + if (!hearsTalking()) { + return; + } + + if (!timer.isRunning()) { + timer.restart(); + } + } + + public boolean hearsTalking() { + return presser.isPressing(); + } + +} diff --git a/src/tarehart/alter/resources/alter.png b/src/tarehart/alter/resources/alter.png new file mode 100644 index 0000000000000000000000000000000000000000..dea64fd0f74e50704279467074312689c96d56a9 GIT binary patch literal 8459 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 to7;~cb-VrigJ