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