I have made a Java Swing application that detects and displays audio pitch and level from an input (e.g microphone). I would like feedback on the current structure of my project, I'm attempting to follow the modified MVC pattern found here, diagram of my project below:
I'm seeking feedback on the structure and any other criticisms. Some thoughts: I don't know if I need the PitchLabel class, it's quite small and doesn't do much, so could be needless. I start a thread for the audio dispatcher (from a public pitch detection API "TarsosDSP"), but I'm not sure if this is the best way to implement it. I only have one user interface on the App's JFrame, that being the record button, but should I add another I think I would need to change the model, to encapsulate the controllers.
App.java - GUI
import com.formdev.flatlaf.FlatLightLaf; import javax.swing.*; import javax.swing.border.EmptyBorder; import java.awt.*; public class App { public static void main(String[] args) { EventQueue.invokeLater(new Runnable() { @Override public void run() { FlatLightLaf.setup(); JFrame mainFrame = new JFrame("JavaTuner"); mainFrame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); mainFrame.setSize(1024, 600); JPanel panel = new JPanel(new BorderLayout()); panel.setBorder(new EmptyBorder(25, 50, 25, 50)); AudioBar bar = new AudioBar(); bar.setPreferredSize(new Dimension(20, 100)); panel.add(bar, BorderLayout.EAST); PitchLabel pitchLabel = new PitchLabel("Pitch: ", SwingConstants.CENTER); Action recordBtnAction = new RecordButtonAction(bar, pitchLabel); recordBtnAction.putValue(Action.NAME, "Start Recording"); JToggleButton recordBtn = new JToggleButton(recordBtnAction); panel.add(pitchLabel, BorderLayout.CENTER); mainFrame.setContentPane(panel); mainFrame.add(recordBtn, BorderLayout.WEST); mainFrame.setVisible(true); } }); } }
RecordButtonAction.java - Acts when the 'Start Recording' button is pressed
import javax.swing.*; import java.awt.event.ActionEvent; public class RecordButtonAction extends AbstractAction { private final AudioBar bar; private final PitchLabel label; private Mic m; public RecordButtonAction(AudioBar bar, PitchLabel label){ this.bar = bar; this.label = label; } @Override public void actionPerformed(ActionEvent e) { JToggleButton toggle = (JToggleButton)e.getSource(); if(toggle.isSelected()){ toggle.setText("Stop Recording"); System.out.println("Streaming Started"); m = new Mic(RecordButtonAction.this); } else{ toggle.setText("Start Recording"); System.out.println("Recording Ended"); m.stopRecording(); updateLabel("Stopped Recording"); } } public void updateLabel(String str){ label.setText(str); } public void updateAudioBar(float rms) { bar.setAmplitude(rms); } }
Mic.java - Responsible for recording audio from input
import be.tarsos.dsp.AudioDispatcher; import be.tarsos.dsp.AudioEvent; import be.tarsos.dsp.io.jvm.JVMAudioInputStream; import be.tarsos.dsp.pitch.*; import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioInputStream; import javax.sound.sampled.AudioSystem; import javax.sound.sampled.DataLine; import javax.sound.sampled.LineUnavailableException; import javax.sound.sampled.TargetDataLine; import java.util.*; public class Mic{ private final RecordButtonAction recordButtonAction; private static AudioDispatcher dispatcher; private TargetDataLine targetLine; private AudioInputStream inputStream; public Mic(RecordButtonAction recordBtnAction) { this.recordButtonAction = recordBtnAction; this.initDataLines(); this.startRecording(); } private void initDataLines() { AudioFormat format = new AudioFormat(44100.0F, 16, 2, true, false); try { DataLine.Info info = new DataLine.Info(TargetDataLine.class, format); targetLine = (TargetDataLine)AudioSystem.getLine(info); targetLine.open(); inputStream = new AudioInputStream(targetLine); } catch (LineUnavailableException e) { e.printStackTrace(); } } private void startRecording() { JVMAudioInputStream audioInputStream = new JVMAudioInputStream(inputStream); targetLine.start(); float sampleRate = 44100; int bufferSize = 6000; int overlap = 0; dispatcher = new AudioDispatcher(audioInputStream, bufferSize, overlap); dispatcher.addAudioProcessor(new PitchProcessor(PitchProcessor.PitchEstimationAlgorithm.YIN, sampleRate, bufferSize, new Pitch(recordButtonAction))); new Thread(dispatcher, "Mic Audio Dispatch Thread").start(); } public void stopRecording(){ dispatcher.stop(); targetLine.close(); targetLine.stop(); } }
Pitch.java - Responsible for pitch detection, implements interface from TarsosDSP public pitch detection API
import be.tarsos.dsp.AudioEvent; import be.tarsos.dsp.pitch.PitchDetectionHandler; import be.tarsos.dsp.pitch.PitchDetectionResult; import java.util.ArrayDeque; import java.util.Queue; public class Pitch implements PitchDetectionHandler { private final int RMS_STORE_SIZE = 10; // For testing purposes private final double MIC_AMBIENCE_RMS = 0.000711; private final RecordButtonAction recordBtnAction; private final Queue<Double> previousRMSUnits; private double avgPreviousRMSUnits = 0; public Pitch(RecordButtonAction recordBtnAction){ this.recordBtnAction = recordBtnAction; previousRMSUnits = new ArrayDeque<Double>(); } // handlePitch is called after the Mic.startRecording() method, and runs continuously // until Mic.stopRecording() is called @Override public void handlePitch(PitchDetectionResult pitchDetectionResult, AudioEvent audioEvent) { if(previousRMSUnits.size() == RMS_STORE_SIZE){ previousRMSUnits.remove(); previousRMSUnits.add(audioEvent.getRMS()); double sum = previousRMSUnits.stream().reduce(Double::sum).get(); avgPreviousRMSUnits = sum / RMS_STORE_SIZE; } else{ previousRMSUnits.add(audioEvent.getTimeStamp()); } recordBtnAction.updateAudioBar((float) (avgPreviousRMSUnits + MIC_AMBIENCE_RMS) * 5); if(pitchDetectionResult.getPitch() != -1){ double timeStamp = audioEvent.getTimeStamp(); float pitch = pitchDetectionResult.getPitch(); recordBtnAction.updateLabel((String.format("Pitch detected at %.2fs: %.2fHz\n", timeStamp, pitch*2))); } } }
AudioBar.java - Graphics component, a volume bar
import javax.swing.*; import java.awt.*; // Using implementation from Radiodef's stackoverflow answer // Link: https://stackoverflow.com/questions/26574326/how-to-calculate-the-level-amplitude-db-of-audio-signal-in-java public class AudioBar extends JComponent { private int meterWidth = 10; private float amp = 0f; public void setAmplitude(float amp) { this.amp = Math.abs(amp); repaint(); } public void setMeterWidth(int meterWidth) { this.meterWidth = meterWidth; } @Override protected void paintComponent(Graphics g) { int w = Math.min(meterWidth, getWidth()); int h = getHeight(); int x = getWidth() / 2 - w / 2; int y = 0; g.setColor(Color.LIGHT_GRAY); g.fillRect(x, y, w, h); g.setColor(Color.BLACK); g.drawRect(x, y, w - 1, h - 1); int a = Math.round(amp * (h - 2)); g.setColor(Color.GREEN); g.fillRect(x + 1, y + h - 1 - a, w - 2, a); } @Override public Dimension getMinimumSize() { Dimension min = super.getMinimumSize(); if (min.width < meterWidth) min.width = meterWidth; if (min.height < meterWidth) min.height = meterWidth; return min; } @Override public Dimension getPreferredSize() { Dimension pref = super.getPreferredSize(); pref.width = meterWidth; return pref; } @Override public void setPreferredSize(Dimension pref) { super.setPreferredSize(pref); setMeterWidth(pref.width); } }
PitchLabel.java - Graphics component, a label that displays the pitch of audio streaming
import javax.swing.*; import java.awt.*; public class PitchLabel extends JLabel { private final Font f = new Font("Segoe UI", Font.PLAIN, 22); public PitchLabel(String text, int horizontalAlignment) { super(text, horizontalAlignment); this.setFont(f); } @Override public void setText(String text) { super.setText(text); } }
Code in git repo here