Hi all,
I’m starting to work on a music/metronome application in Java and I’m running into some problems with the timing and speed.
For testing purposes I’m trying to play two sine wave tones at the same time at regular intervals, but instead they play in sync for a few beats and then slightly out of sync for a few beats and then back in sync again for a few beats.
From researching good metronome programming, I found that Thread.sleep() is horrible for timing, so I completely avoided that and went with checking System.nanoTime() to determine when the sounds should play.
I’m using AudioSystem’s SourceDataLine for my audio player and I’m using a thread for each tone that constantly polls System.nanoTime() in order to determine when the sound should play. I create a new SourceDataLine and delete the previous one each time a sound plays, because the volume fluctuates if I leave the line open and keep playing sounds on the same line. I create the player before polling nanoTime() so that the player is already created and all it has to do is play the sound when it is time.
In theory this seemed like a good method for getting each sound to play on time, but it’s not working correctly.
At the moment this is just a simple test in Java, but my goal is to create my app on mobile devices (Android, iOS, Windows Phone, etc)...however my current method isn’t even keeping perfect time on a PC, so I’m worried that certain mobile devices with limited resources will have even more timing problems. I will also be adding more sounds to it to create more complex rhythms, so it needs to be able to handle multiple sounds going simultaneously without sounds lagging.
Another problem I’m having is that the max tempo is controlled by the length of the tone since the tones don’t overlap each other. I tried adding additional threads so that every tone that played would get its own thread...but that really screwed up the timing, so I took it out. I would like to have a way to overlap the previous sound to allow for much higher tempos.
I posted this question on StackOverflow where I got one reply and my response back explains why I went this direction instead of preloading a larger buffer (which is what they recommended). In short, I did try the buffer method first, but I want to also update a “beat counter” visual display and there was no way to know when the hardware was actually playing the sounds from the buffer. I mentioned that on StackOverflow and I also asked a couple more questions regarding the buffer method, but I haven’t received any more responses.
http://stackoverflow.com/questions/24110247/java-audio-metronome-timing-and-speed-problems
Any help getting these timing and speed issues straightened out would be greatly appreciated! Thanks.
Here is my code...
SoundTest.java
- import java.awt.*;
- import java.awt.event.*;
- import javax.swing.*;
- import javax.swing.event.*;
-
- import java.io.*;
- import javax.sound.sampled.*;
-
- public class SoundTest implements ActionListener {
- static SoundTest soundTest;
-
-
-
- boolean playSound1 = true;
- boolean playSound2 = true;
-
-
- JFrame mainFrame;
- JPanel mainContent;
- JPanel center;
- JButton buttonPlay;
-
- int sampleRate = 44100;
- long startTime;
- SourceDataLine line = null;
- int tickLength;
- boolean playing = false;
-
- SoundElement sound01;
- SoundElement sound02;
-
- public static void main (String[] args) {
- soundTest = new SoundTest();
-
- SwingUtilities.invokeLater(new Runnable() { public void run() {
- soundTest.gui_CreateAndShow();
- }});
- }
-
- public void gui_CreateAndShow() {
- gui_FrameAndContentPanel();
- gui_AddContent();
- }
-
- public void gui_FrameAndContentPanel() {
- mainContent = new JPanel();
- mainContent.setLayout(new BorderLayout());
- mainContent.setPreferredSize(new Dimension(500,500));
- mainContent.setOpaque(true);
-
- mainFrame = new JFrame("Sound Test");
- mainFrame.setContentPane(mainContent);
- mainFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
- mainFrame.pack();
- mainFrame.setVisible(true);
- }
-
- public void gui_AddContent() {
- JPanel center = new JPanel();
- center.setOpaque(true);
-
- buttonPlay = new JButton("PLAY / STOP");
- buttonPlay.setActionCommand("play");
- buttonPlay.addActionListener(this);
- buttonPlay.setPreferredSize(new Dimension(200, 50));
-
- center.add(buttonPlay);
- mainContent.add(center, BorderLayout.CENTER);
- }
-
- public void actionPerformed(ActionEvent e) {
- if (!playing) {
- playing = true;
-
- if (playSound1)
- sound01 = new SoundElement(this, "Sound1", 800, 1);
- if (playSound2)
- sound02 = new SoundElement(this, "Sound2", 1200, 1);
-
- startTime = System.nanoTime();
-
- if (playSound1)
- new Thread(sound01).start();
- if (playSound2)
- new Thread(sound02).start();
- }
- else {
- playing = false;
- }
- }
- }
SoundElement.java
- import java.io.*;
- import javax.sound.sampled.*;
-
- public class SoundElement implements Runnable {
- SoundTest soundTest;
-
-
-
-
- long nsDelay = 750000000;
-
-
- long before;
- long after;
- long diff;
-
- String name="";
- int clickLength = 4100;
- byte[] audioFile;
- double clickFrequency;
- double subdivision;
- SourceDataLine line = null;
- long audioFilePlay;
-
- public SoundElement(SoundTest soundTestIn, String nameIn, double clickFrequencyIn, double subdivisionIn){
- soundTest = soundTestIn;
- name = nameIn;
- clickFrequency = clickFrequencyIn;
- subdivision = subdivisionIn;
- generateAudioFile();
- }
-
- public void generateAudioFile(){
- audioFile = new byte[clickLength * 2];
- double temp;
- short maxSample;
-
- int p=0;
- for (int i = 0; i < audioFile.length;){
- temp = Math.sin(2 * Math.PI * p++ / (soundTest.sampleRate/clickFrequency));
- maxSample = (short) (temp * Short.MAX_VALUE);
- audioFile[i++] = (byte) (maxSample & 0x00ff);
- audioFile[i++] = (byte) ((maxSample & 0xff00) >>> 8);
- }
- }
-
- public void run() {
- createPlayer();
- audioFilePlay = soundTest.startTime + nsDelay;
-
- while (soundTest.playing){
- if (System.nanoTime() >= audioFilePlay){
- play();
- destroyPlayer();
- createPlayer();
- audioFilePlay += nsDelay;
- }
- }
- try { destroyPlayer(); } catch (Exception e) { }
- }
-
- public void createPlayer(){
- AudioFormat af = new AudioFormat(soundTest.sampleRate, 16, 1, true, false);
- try {
- line = AudioSystem.getSourceDataLine(af);
- line.open(af);
- line.start();
- }
- catch (Exception ex) { ex.printStackTrace(); }
- }
-
- public void play(){
- line.write(audioFile, 0, audioFile.length);
- }
-
- public void destroyPlayer(){
- line.drain();
- line.close();
- }
- }