In order to make SwingTimer
accurate, I like the logic and example suggested by @Tony Docherty
On CR. Here is the Link.
In order to highlight the given words, again and again, there is always a few microsecond delays. If I have words to highlight say: "hello how are" and the values for each word are (delays): 200,300,400 ms respectively, then the actual time taken by the timer is always more. Say instead of 200 ms, it takes 216 ms. Like this, if I have many words..in the end, the extra delay is noticeable.
I have to highlight each letter say: 'h''e''l''l''0' each should get 200/length(i.e 5) = 40 ms approx. Set the delay after each letter.
My logic is, take the current time say startTime
, just before starting the process. Also, calculate the totalDelay
which is totalDelay+=delay/.length().
Now check the condition: (startTime+totalDelay-System.currentTime
)
if this is -ve, that means the time consumption is more, so skip the letter. Check till there is a positive delay.This means I am adding the timings till now, and overcheck it with the difference in the time taken by the process when it got started.
This may result into skipping to highlight the letters.
But something is wrong. What, it’s difficult for me to make out. It's some problem with the looping thing maybe. I have seen it is entering the loop (to check whether the time is -ve ) just twice. But this should not be the case. And I am also not sure about setting up my next delay. Any ideas?
Here is an SSCCE:
import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.lang.reflect.InvocationTargetException;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextPane;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultStyledDocument;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;
public class Reminder {
private static final String TEXT = "arey chod chaad ke apnee saleem ki gali anarkali disco chalo";
private static final String[] WORDS = TEXT.split(" ");
private JFrame frame;
private Timer timer;
private StyledDocument doc;
private JTextPane textpane;
private int[] times = new int[100];
private long totalDelay=0,startTime=0;
private int stringIndex = 0;
private int index = 0;
public void startColoring() {
times[0]=100;times[9]=200;times[10]=200;times[11]=200;times[12]=200;
times[1]=400;times[2]=300;times[3]=900;times[4]=1000;times[5]=600;times[6]=200;times[7]=700;times[8]=700;
ActionListener actionListener = new ActionListener() {
@Override
public void actionPerformed(ActionEvent actionEvent)
{
doc.setCharacterAttributes(stringIndex, 1, textpane.getStyle("Red"), true);
stringIndex++;
try {
if (stringIndex >= doc.getLength() || doc.getText(stringIndex, 1).equals(" ")|| doc.getText(stringIndex, 1).equals("\n"))
{
index++;
}
if (index < WORDS.length) {
double delay = times[index];
totalDelay+=delay/WORDS[index].length();
/*Check if there is no -ve delay, and you are running according to the time*/
/*The problem is here I think. It's just entered this twice*/
while(totalDelay+startTime-System.currentTimeMillis()<0)
{
totalDelay+=delay/WORDS[index].length();
stringIndex++;
/*this may result into the end of current word, jump to next word.*/
if (stringIndex >= doc.getLength() || doc.getText(stringIndex, 1).equals(" ") || doc.getText(stringIndex, 1).equals("\n"))
{
index += 1;
totalDelay+=delay/WORDS[index].length();
}
}
timer.setDelay((int)(totalDelay+startTime-System.currentTimeMillis()));
}
else {
timer.stop();
System.err.println("Timer stopped");
}
} catch (BadLocationException e) {
e.printStackTrace();
}
}
};
startTime=System.currentTimeMillis();
timer = new Timer(times[index], actionListener);
timer.setInitialDelay(0);
timer.start();
}
public void initUI() {
frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JPanel panel = new JPanel();
doc = new DefaultStyledDocument();
textpane = new JTextPane(doc);
textpane.setText(TEXT);
javax.swing.text.Style style = textpane.addStyle("Red", null);
StyleConstants.setForeground(style, Color.RED);
panel.add(textpane);
frame.add(panel);
frame.pack();
frame.setVisible(true);
}
public static void main(String args[]) throws InterruptedException, InvocationTargetException {
SwingUtilities.invokeAndWait(new Runnable() {
@Override
public void run() {
Reminder reminder = new Reminder();
reminder.initUI();
reminder.startColoring();
}
});
}
}
UPDATE:
For better understanding:
The EG given by @Tony Docherty :
Lets take the word "Test" and say it needs to be highlighted for 1 second, therefore each letter is highlighted for 250ms. Doing things the way you originally, did meant that you set a timer for 250ms for each letter but if each cycle actually took 260ms and lets say the 'e' cycle took 400ms (maybe due to GC or something else using CPU cycles) by the end of the word you would have taken 180ms more than you should have. This error will continue to build for each word until the error is so large highlighting is no longer visually in sync.
The way I am trying, is rather than repeatedly saying this letter needs to be highlighted for x amount of time, calculate the time for each letter relative to the beginning of the sequence ie T = 250, e = 500, s = 750, t = 1000.
So to get the actual time delay you need to add the start time and subtract the current time. To run through the example using the timings I gave above:
StartTime Letter Offset CurrentTime Delay ActualTimeTaken
100000 T 250 100010 240 250
100000 e 500 100260 240 400
100000 s 750 100660 90 100
100000 t 1000 100760 240 250
So you should be able to see now that the timing for each letter is adjusted to take account of any overrun of time from the previous letter. Of course it is possible that a timing overrun is so great that you have to skip highlighting the next letter (or maybe more than 1) but at least I will remaining broadly in sync.
EDITED SSCCE
Update2
In first phase, I take the timings for each word. That is, when the user hits ESC key, the time is stored for a particular word (he does it as the song is played in background.) When the ESC key is pressed, the current word is highlighted and the time spent on the current word is stored in an array. I keep on storing the timings. When the user ends, now I would like to highlight the words as per the set timings. So here, the timing by the user is important. If the timings are fast, so is the highlighting of words or if slow, vice-versa.
New update: progress
The answers below have different logic, but to my surprise, they work more or less the same. A very very weird problem I have found out with all the logic (including mine) is that they seem to work perfectly for few lines, but after that they gain speed, that's also not slowly, but with a huge difference.
Also if you think I should think in a different way, your suggestions are highly appreciated.
Okay so I have been looking at the some code (the code I posted in your last question about Karaoke timer)
Using that code I put up some measuring system using
System.nanoTime()
viaSystem.out.println()
which will help us to see what is happening:The output on my PC is:
Thus my conclusion is the Swing Timer is actually running faster than we expect as the code in the
Timer
sactionPerformed
will not necessarily take as long as the letters expected highlighting time this of course causes an avalanche effect i.e the faster/slower the timer runs the greater/less the difference will become and timers next execution onrestart(..)
will take be at a different time i.e faster or slower.in the code do this:
Produces a more accurate result (maximum latency Ive had is 4ms faster per letter):
I think that to do something like this, you need a Swing Timer that ticks at a constant rate, say 15 msec, as long as it's fast enough to allow the time granularity you require, and then trip the desired behavior inside the timer when the elapsed time is that which you require.
while (true)
loop on the EDT. Let the "while loop" be the Swing Timer itself.The code I'm suggesting would look something like so:
For example, my SSCCE:
Have you considered java.util.Timer and scheduleAtFixedRate? You will need a little extra work to do stuff on the EDT, but it should fix the issue of accumulated delays.
ScheduledExecutorService
tends to be more accurate than Swing's Timer, and it offers the benefit of running more than one thread. In particular, if one tasks gets delayed, it does not affect the starting time of the next tasks (to some extent).Obviously if the tasks take too long on the EDT, this is going to be your limiting factor.
See below a proposed SSCCE based on yours - I have also slightly refactored the
startColoring
method and split it in several methods. I have also added some "logging" to get a feedback on the timing of the operations. Don't forget toshutdown
the executor when you are done or it might prevent your program from exiting.Each words starts colouring with a slight delay (between 5 and 20ms on my machine), but the delays are not cumulative. You could actually measure the scheduling overhead and adjust accordingly.