Java Audio Metronome | Timing and Speed Problems

…just a quick addition.

For comparison I just tried recording one of the current metronome apps I have on my phone (Samsung Galaxy S4) called Mobile Metronome, which the top free metronome for Android (with over 1 million downloads) and after 10 minutes of recording it was 3,229 samples (or approximately 73ms) behind the beat. So mine is actually over 10x more accurate than the top freebie, not to shabby. :slight_smile: So maybe this is just something I should be expecting and just have to live with? Although, my mind is going all kinds of places like trying to figure out how to now easily calculate the drift and then create a drift “offset” that would nudge back it enough to keep it on beat…OCD and programming, not a good mix. lol

Hi philfrei,

I realize it’s a small delay and different software…but I’ve moved recorded music tracks around between different software and computers before with no shift in the timing. A friend and I used to bounce tracks back and forth between his Mac laptop and my PC desktop (he used Logic and I use Magix Samplitude) and there wasn’t any timing drift problems as long as my project’s tempo matched his project’s tempo (or vice versa). But maybe any difference between them would have been too small to notice with instrument tracks?..it’s very easy to tell exactly where the start of my metronome tone begins since everything surrounding it is all zero’s, but that wouldn’t be the case with instrument tracks.

I just added in a print statement for tickBufferSize and it does come back out to equal exactly 44100. The tempo I tested was 60bpm, which I did to avoid any weird cascading rounding errors as you mentioned just to see if it would stay “spot on”.

I love your idea of trying to play a sound file in Java and recording that. I’ll record 10 minutes of the metronome within my recording software Samplitude so that its easy to compare the difference between the Java playback version and the original. I’ll need to figure out how to load the wave file into my program, because I’d like to use the same SourceDataLine method I’m using for my metronome.

Thanks again for all your help! :slight_smile:

Alrighty, I got the WAV file reader working. BTW, I found some great source code on Stack Overflow from a google search that I was able to use to get it going…it was posted by an extremely helpful guy! :wink:


…what a small world! ;D

I recorded 10 min of Samplitude’s metronome at 60bpm as a mono wav file, played it through my Java application, re-recorded it via the laptop, and it too is drifting ahead of time by 268 samples or approximately 6ms after 10 minutes. So this method had a little less drift than my metronome.

EDIT: I should mention that I did check the initial recording of Samplitude’s metronome (just to be sure) by loading it back into Samplitude and it was perfect down to the sample at 10 min. No drift at all. This is the file I played via Java.

Here is the code:

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;

import java.io.*;
import javax.sound.sampled.*;

public class WavReader implements ActionListener {
	static WavReader wavReader;
	
	JFrame mainFrame;
	JPanel mainContent;
	JPanel center;
	JButton buttonPlay;
	
	int bufferSize = 2000;
	byte[] buffer;
	SourceDataLine line = null;
	
	boolean playing = false;
	
	File fileIn;	
	AudioInputStream audioInputStream;
	
	public WavReader(){
		fileIn = new File("SamplitudeMetronomeMono.wav");
	}
	
	public static void main (String[] args) {		
		wavReader = new WavReader();
		
		SwingUtilities.invokeLater(new Runnable() {
			public void run() {
				wavReader.guiCreateAndShow();
			}
		});
	}
	
	public void guiCreateAndShow() {
		guiFrameAndContentPanel();
		guiAddContent();
	}
	public void guiFrameAndContentPanel() {
		mainContent = new JPanel();
		mainContent.setLayout(new BorderLayout());
		mainContent.setPreferredSize(new Dimension(500,500));
		mainContent.setOpaque(true);
		
		mainFrame = new JFrame("Timing Check");				
		mainFrame.setContentPane(mainContent);				
		mainFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		mainFrame.pack();
		mainFrame.setVisible(true);
	}
	public void guiAddContent() {
		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;
			loadWaveFile();
			createPlayer();
			new Thread(play).start();
		}
		else {
			playing = false;
		}
	}
	
	public void loadWaveFile(){
		try {
			audioInputStream = AudioSystem.getAudioInputStream(fileIn);
		} catch (Exception e) { e.printStackTrace(); }
	}
	
	public void createPlayer(){
		AudioFormat af = audioInputStream.getFormat();
		try {
			line = AudioSystem.getSourceDataLine(af);
			line.open(af);
			line.start();
		}
		catch (Exception e) { e.printStackTrace(); }
	}
	
	public Runnable play = new Runnable() { public void run() {
		buffer = new byte[bufferSize];
		int b=0;
		
		try { 
			while (playing && (audioInputStream.read(buffer) != -1)) {
				line.write(buffer, 0, buffer.length);
			}
		} catch (Exception e){ e.printStackTrace(); }
		
		destroyPlayer();
	}};
	
	public void destroyPlayer(){
		line.drain();
		line.close();
	}
}

Interesting!

I think it is very likely that if you go back to some of those tracks you swapped, there might indeed be some drift. Ears have to be really good, and circumstances very clear to hear imprecisions when only a few millis are involved. I recall once trying to improvise against a friend’s track and having the feeling be slightly uncomfortable, unable to get a sense of being “in the pocket”. We took a closer look at the rhythms and found one of the drums offset by 4 millis. Straightening that out did a lot to settle my nerves. But I didn’t hear it as a mistake, but rather just experienced some tension that I couldn’t really explain. It could also have been an example of me just being nervous, and the fix working as a sort of placebo. But my experience was that the fix did feel more solid. (And yes, non-percussive sounds would be next to impossible to detect as being off at this level.)

I tested the idea of the int cast truncation, as well, and found that the “correct” number of frames was being generated, to the nearest frame. Later on, it occurred to me: what if a proper DAW is even more accurate than the sample rate? In order to handle a plethora of sample rates, maybe the DAWs work at a finer granularity than 44100 fps, and the metronomes naturally are tied to the highest level of precision available in that DAW. (My Cakewalk Sonar also handles 48000 sample rate, so it has to have something that is more precise than 44100 fps, but aren’t there 96000 tracks now, as well?)

If a DAW’s metronome is more accurate than the 44100 sample rate, then the fraction of the frame that is getting truncated in Java, but is handled in the DAW’s metronome could cause some drift that is in the ballpark for what you are getting. Maybe?

I’m not sure how the fractions would be handled in the DAW. Maybe something like a Leap Year, adding or taking away an extra frame every now and then? As far as Java is concerned, there is no way I know of to take nanosecond readings at the precise time the frame is run through the digital-to-analog-converter, and the lack of real-time guarantees means the point when the frame is processed in the while loop could vary considerably!

In any event, your demonstration that the metronome is as or more accurate than other commercial metronomes seems like a good reality check. I wonder how your metronome, when running in Android, will compare.

Last bit of musing: if the DAW’s metronome IS more accurate (to fractions of a sample), then a recording made from a DAW would most likely be a tiny bit longer than the calculated number of frames (using the truncation that Java does with the int cast). Perhaps that led to the drift in the plus, instead of minus direction, for that test?

I forgot about that post you dug up. I was contradicting the answer of one of my Java heros, Kay Horstmann! I was also doing a bit of venting. Maybe if you like that answer, and found it helpful, you’ll give it a +?

Sometimes I wish I had minored in mathematics and/or sound engineering. I have a friend that is an engineer and used to work for Pro Tools. I’ll run the idea of the metronome vs sample rate accuracy issue by him next chance I get.

Surely there must be a way to query/bruteforce the maximum supported samplerate of the hardware.

It would have been interesting to have done some really fine comparisons between our programs. Can’t really do so now because my friend no longer lives in the area. Also, in Samplitude when you zoom all the way in on a waveform it actually chops it up using vertical lines to show the value of every individual sample…I’m not sure if Logic does that.

Being more accurate than 44100 was my initial thinking as well, which is why my thinking was to use nanoseconds for timing…but then Java wouldn’t allow that to work. :-\ However, that seems like it would have helped more with the odd tempos like 61bpm or similar since there would have to be “rounding” involved. But with 60bpm, that is one beat every second so it seems like each beat should theoretically be in the same place regardless of the sample rate…44100 or 96000 frames is divided over the span of a second, but it seems like that very first frame should always be in the same place. It’s strange for sure! I’m very interested to hear what your friend who worked for Pro Tools has to say. Was he a programmer who worked on coding Pro Tools?

I too wonder how well this will work when I move over to Android…hopefully the difference is in the coding, not the platform. I’m gonna be bummed if it ends up getting worse when moved to mobile.

Maybe the best solution is just to provide an “offset” value in the settings? Just in case the accuracy does change when moved to mobile, then for those who want it “dead on accurate” they could calibrate it for their specific device and punch in the offset value. Now it would be way cool if I could figure out how to do that automatically! Such as automatically record 1 min of click internally, calculate the drift, and then adjust the offset all at the press of a button! Man that would be slick! ;D

I tried to give your post on StackOverflow a “+”, but it said I have to have 15 reputation points…I’m only at 11! Pffffft!

I minored in physics…which is essentially math + additional headaches. LOL

Riven,
I’m not sure that the sample rate is the problem for my tests at 60bpm. Even if the sample rate was ridiculously low at like 100Hz, the beat should still start on the very first frame at the start of each second. Higher sample rates would certainly help for beats that start in between, but it seems like that first frame should always start at the start of the second on the dot.

Internal clock issues maybe? Hmm…brain storm! I think tomorrow I’ll try recording Samplitude’s click straight out of Samplitude (no Java) and record it via the laptop in Reaper. Two different DAW software on two different computers. If this comes out to be slightly off time as well, then I think we can conclude that this little bit of timing drift is to be expected.

I’m fairly sure philfrei meant that you might be sending 44100 samples per second to hardware that outputs 48000 samples per second.

This means that every sample is ‘scaled in time’ by factor 1.0884353741496598639455782312925 (48000 /44100)

Some transformation has to happen in hardware, and it might not be optimal, leading to a drift of a few millis over a timespan of 10 minutes. A drift of 7ms over 10601000ms means it is 99.9988333% accurate, which seems reasonable for consumer grade audio hardware.

That’s why I suggest to use the native sampling rate of your audio hardware, as to get rid of this truncation/inaccuracy altogether (hopefully).

ISTR that since Windows Vista the entire Windows audio pipeline is now implemented purely in software. I may be wrong.

Cas :slight_smile:

Ok, I have some new test results! I also re-recorded 44.1kHz just to see if it was the same as before, and it actually had slightly less drift this time…

TimingCheck.java:
44.1kHz = 295 samples (approx. 6ms) ahead of the beat
48kHz = 1,232 samples (approx. 27ms) behind the beat
96kHz = 297 samples (approx. 6ms) ahead of the beat

NO Java - metronome track played in Samplitude:
44.1kHz (both devices) = 289 samples (approx. 6ms) ahead of the beat

For some reason 48kHz was off by quite a bit more than the others and it was the only one to drift behind the beat. So that’s interesting.

I think you were right on the money philfrei, the drift is simply because it’s two different applications running on two different devices. Even two different DAW’s on different computers were not in sync perfectly after 10 minutes and were off by approximately the same amount as the Java program.

Guess I should stop worrying about this now…eh? ;D

Was the metronome app ever finished?

I don’t recall what happened with Tekkerue’s project. He hasn’t shown up here in a long time, but maybe the thread will automatically ping him and we will get an answer.

Since then, I made a drone tool that has a solid metronome as part of the program. It works quite well–the timing arises from counting frames audio frames right before shipping them to the SourceDataLine. Theoretically, it’s accurate to 1/44100th of a second. I use it for practicing as well as intonation training and as a kind of pseudo Tanpura.

Can I download it on iOS?

I don’t think so. The source code uses Java 8 with Swing for the GUI. iOS doesn’t have a jvm afaik. That hasn’t changed, has it?