10

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.

naugler
  • 1,060
  • 10
  • 31
joey rohan
  • 3,505
  • 5
  • 33
  • 70
  • The while loop called on the EDT looks suspicious to me. Let me re-read your requirements... – Hovercraft Full Of Eels Feb 23 '13 at 13:37
  • @HovercraftFullOfEels The while loop don't freez any thing,till now.I am trying to check, the extra delay taken by the swing timer, from the starting of the highlighting thing.Will make an update to make it more clear. – joey rohan Feb 23 '13 at 13:40
  • 1
    I still don't fully understand what your problem is: for once, I see that not all letters are red after stopping the timer - is that part of the problem? And: what exactly do you mean by accuracy, any hard numbers and if so, how did you calculate them or what hardware (?) event produces them? You probably won't get below the 15ms of the typical coarse hardware timer produces anyway, plus your action itself takes some time. – kleopatra Feb 25 '13 at 15:32
  • @kleopatra 1) Not all the letters are red: That is beacuse of synchronization.I may give up hhighlighting a few letters in between but not loose sync.When i see the time taken by the process is more, i skip that letter. 2)No, not the part of the problem, your logic may be different. 3)Accuracy- I want them to be highlighted in accordance with the background sound/music/song (Same as Sync. in Kataoke apps, As they are Sync. very well).Yes, true, there might be CPU delay, but some logic to overcome, if not 100, then 92% accurate. – joey rohan Feb 25 '13 at 15:43
  • Continued...I have tried a hell lot of things, also the answer by @Hovercraft Full Of Eels.And i don't have any more ideas, hope will get some good help. – joey rohan Feb 25 '13 at 15:45
  • still don't get it (silly me ;-) - what is the other end of "synch"? Were/how do you get the "beat" (or whatever) that tells you which letter should be (ideally) highlighted at any single moment? – kleopatra Feb 25 '13 at 15:57
  • forgot 2 thingies: a) what's wrong with @HovercraftFullOfEels approach? b) still no numbers on your required "accuracy" ... – kleopatra Feb 25 '13 at 16:12
  • @kleopatra no beat required.In first phase i take the timings for a particular word.Ok will ake another update.b)"still no numbers "may or may not be.The main motive is Sync.The wrong thing is, the method used by -HovercraftFullOfEels, is a bit faster.Not correctly Sync. – joey rohan Feb 25 '13 at 17:00
  • sync to the minute, micro/milli/nano/femto/second ...? – kleopatra Feb 25 '13 at 17:02
  • @kleopatra mili. as milisec is as accurate as anything.See my updated question.Ask for an SSCCE which includes both taking and display the highlited words if Reqd. – joey rohan Feb 25 '13 at 17:18
  • as already mentioned, the min (best case scenario, no other or only a few timers and a very short actionPerformed itself) resolution is around 15ms .. if that's not good enough you'll have to look for an alternative – kleopatra Feb 25 '13 at 17:26
  • @kleopatra After all this, i have noticed, this varies as per the set delay and the length of a word.Need not to be 15MS for every thing.For EG for a word delay with 5sec, the delay approx comes out to be 100 MS.Any suggestions for alternative? – joey rohan Feb 25 '13 at 17:30
  • +1 @kleopatra for the average system latency of 15ms for timer (as it most likely uses `sleep()` and than ofcourse the action... Also if you're measuring elapsed time by subtracting two timestamps, you should use `System.nanoTime()` to get those timestamps as it uses the "most precise available system timer". As I have suggested before use a `Thread` with while loop, `SwingUtilities.invokeXX` and `Thread.sleep(time-15)` to create your own more accurate timer. – David Kroukamp Feb 26 '13 at 07:36
  • @DavidKroukamp I did tried both.1)By using System.nanoTime(),Which gives almost 99% same result.2) By using own sleep delay method,which is more accurate than swing timer, but still needs modifications like swing timer. – joey rohan Feb 26 '13 at 11:24
  • @DavidKroukamp head scratching - I understand your words but not what they are trying to tell me ;-) All I did so far is to try pushing joey into _defining_ (and measuring!) exactly what he needs. The outcome of that will heavily impact the solution. – kleopatra Feb 26 '13 at 11:57
  • @joey rohan on first sight looks like as Swing.Timer is inaccurate, but next period starting when all events are done, sure doesn't preventing for exceptions from RepaintManager, – mKorbel Feb 26 '13 at 15:40
  • @mKorbel "doesn't preventing for exceptions from RepaintManager" because of multiple requests ? – joey rohan Feb 26 '13 at 16:08
  • @joey rohan not, multiple requests for cycle only multiple of exceptions, any of exception in Java, – mKorbel Feb 26 '13 at 16:15
  • @mKorbel then i think there is only one solution.."prevention". – joey rohan Feb 26 '13 at 17:29
  • @kleopatra no sorry the rest was inteneded for joey, I was just saying what i +1'd for to bring OPs attention to it. My bad for confusion :) – David Kroukamp Feb 26 '13 at 18:07
  • @joey rohan I can to see difference on 1.5 - 3 seconds per day (between swing.Timer and util.Timer), [I see this post as good example](http://stackoverflow.com/a/7049095/714968) – mKorbel Feb 27 '13 at 16:04

4 Answers4

7

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.

  • In other words, don't change the Timer's delay at all, but just change the required elapse times according to your need.
  • You should not have a while (true) loop on the EDT. Let the "while loop" be the Swing Timer itself.
  • To make your logic more fool proof, you need to check if elapsed time is >= needed time.
  • Again, don't set the Timer's delay. In other words, don't use it as a timer but rather as a poller. Have it beat every xx msec constantly polling the elapsed time, and then reacting if the elapsed time is >= to your need.

The code I'm suggesting would look something like so:

     public void actionPerformed(ActionEvent actionEvent) {

        if (index > WORDS.length || stringIndex >= doc.getLength()) {
           ((Timer)actionEvent.getSource()).stop();
        }

        currentElapsedTime = calcCurrentElapsedTime();
        if (currentElapsedTime >= elapsedTimeForNextChar) {
           setNextCharAttrib(stringIndex);
           stringIndex++;

           if (atNextWord(stringIndex)) {
              stringIndex++; // skip whitespace 
              deltaTimeForEachChar = calcNextCharDeltaForNextWord();
           } else {
              elapsedTimeForNextChar += deltaTimeForEachChar;
           }
        }

        // else -- we haven't reached the next time to change char attribute yet.
        // keep polling.
     }

For example, my SSCCE:

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.LinkedList;
import java.util.List;

import javax.swing.*;
import javax.swing.text.*;

public class Reminder3 {
   private static final String TEXT = "arey chod chaad ke apnee saleem ki gali anarkali disco chalo";
   private static final String[] WORDS = TEXT.split(" ");
   private static final int[] TIMES = { 100, 400, 300, 900, 1000, 600, 200,
         700, 700, 200, 200, 200, 200 };
   private static final int POLLING_TIME = 12;

   private StyledDocument doc;
   private JTextPane textpane;
   private JPanel mainPanel = new JPanel();
   private List<ReminderWord> reminderWordList = new LinkedList<ReminderWord>();
   private Timer timer;

   // private int stringIndex = 0;

   public Reminder3() {
      doc = new DefaultStyledDocument();
      textpane = new JTextPane(doc);
      textpane.setText(TEXT);
      javax.swing.text.Style style = textpane.addStyle("Red", null);
      StyleConstants.setForeground(style, Color.RED);

      JPanel textPanePanel = new JPanel();
      textPanePanel.add(new JScrollPane(textpane));

      JButton startBtn = new JButton(new AbstractAction("Start") {

         @Override
         public void actionPerformed(ActionEvent arg0) {
            goThroughWords();
         }
      });
      JPanel btnPanel = new JPanel();
      btnPanel.add(startBtn);

      mainPanel.setLayout(new BorderLayout());
      mainPanel.add(textPanePanel, BorderLayout.CENTER);
      mainPanel.add(btnPanel, BorderLayout.SOUTH);
   }

   public void goThroughWords() {
      if (timer != null && timer.isRunning()) {
         return;
      }
      doc = new DefaultStyledDocument();
      textpane.setDocument(doc);
      textpane.setText(TEXT);

      javax.swing.text.Style style = textpane.addStyle("Red", null);
      StyleConstants.setForeground(style, Color.RED);

      int wordStartTime = 0;
      for (int i = 0; i < WORDS.length; i++) {

         if (i > 0) {
            wordStartTime += TIMES[i - 1];
         }
         int startIndexPosition = 0; // set this later
         ReminderWord reminderWord = new ReminderWord(WORDS[i], TIMES[i],
               wordStartTime, startIndexPosition);
         reminderWordList.add(reminderWord);
      }

      int findWordIndex = 0;
      for (ReminderWord word : reminderWordList) {

         findWordIndex = TEXT.indexOf(word.getWord(), findWordIndex);
         word.setStartIndexPosition(findWordIndex);
         findWordIndex += word.getWord().length();
      }

      timer = new Timer(POLLING_TIME, new TimerListener());
      timer.start();
   }

   public JComponent getMainPanel() {
      return mainPanel;
   }


   private void setNextCharAttrib(int textIndex) {
      doc.setCharacterAttributes(textIndex, 1,
            textpane.getStyle("Red"), true);      
   }

   private class TimerListener implements ActionListener {
      private ReminderWord currentWord = null;
      private long startTime = System.currentTimeMillis();

      @Override
      public void actionPerformed(ActionEvent e) {
         if (reminderWordList == null) { 
            ((Timer) e.getSource()).stop();
            return;
         }

         if (reminderWordList.isEmpty() && currentWord.atEnd()) {
            ((Timer) e.getSource()).stop();
            return;
         }

         // if just starting, or if done with current word
         if (currentWord == null || currentWord.atEnd()) {
            currentWord = reminderWordList.remove(0); // get next word
         }

         long totalElapsedTime = System.currentTimeMillis() - startTime;
         if (totalElapsedTime > (currentWord.getStartElapsedTime() + currentWord
               .getIndex() * currentWord.getTimePerChar())) {
            setNextCharAttrib(currentWord.getStartIndexPosition() + currentWord.getIndex());

            currentWord.increment();
         }

      }
   }

   private static void createAndShowGui() {
      Reminder3 reminder = new Reminder3();

      JFrame frame = new JFrame("Reminder");
      frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      frame.getContentPane().add(reminder.getMainPanel());
      frame.pack();
      frame.setLocationByPlatform(true);
      frame.setVisible(true);
   }


   public static void main(String[] args) {
      SwingUtilities.invokeLater(new Runnable() {
         public void run() {
            createAndShowGui();
         }
      });
   }

}

class ReminderWord {
   private String word;
   private int totalTime;
   private int timePerChar;
   private int startTime;
   private int startIndexPosition;
   private int index = 0;

   public ReminderWord(String word, int totalTime, int startTime,
         int startIndexPosition) {
      this.word = word;
      this.totalTime = totalTime;
      this.startTime = startTime;
      timePerChar = totalTime / word.length();
      this.startIndexPosition = startIndexPosition;
   }

   public String getWord() {
      return word;
   }

   public int getTotalTime() {
      return totalTime;
   }

   public int getStartElapsedTime() {
      return startTime;
   }

   public int getTimePerChar() {
      return timePerChar;
   }

   public int getStartIndexPosition() {
      return startIndexPosition;
   }

   public int increment() {
      index++;
      return index;
   }

   public int getIndex() {
      return index;
   }

   public boolean atEnd() {
      return index > word.length();
   }

   public void setStartIndexPosition(int startIndexPosition) {
      this.startIndexPosition = startIndexPosition;
   }

   @Override
   public String toString() {
      return "ReminderWord [word=" + word + ", totalTime=" + totalTime
            + ", timePerChar=" + timePerChar + ", startTime=" + startTime
            + ", startIndexPosition=" + startIndexPosition + ", index=" + index
            + "]";
   }

}
Hovercraft Full Of Eels
  • 283,665
  • 25
  • 256
  • 373
  • I did that all time.Trust me, it din't worked as expected."_just change the required elapse times according to your need._" This thing varies for me.Even a perfect formula failed. – joey rohan Feb 23 '13 at 13:45
  • This is beacuse of setDelay() method.I need to set delay for next letter, (but it should be calculated.)That is why have to put inside actionPerformed. – joey rohan Feb 23 '13 at 13:51
  • @joeyrohan: that's just it. Don't change the Timer's delay. Let the timer act as a *poller*. See edit above. – Hovercraft Full Of Eels Feb 23 '13 at 13:53
  • @HovercraftFullOfEels whichever granularity he achieves and no matter how fast his actionPerformed will execute, this will lack precision after a while (I think he is trying to make a karoake thing, so he needs to be in perfect sync with music). As this already been suggested, the best way would be synch itself with the audio-player directly (this would take into account pauses/forward/backward, possible latencies of the computer playing the audio) – Guillaume Polet Feb 23 '13 at 13:54
  • I just cannot find any logic related.And what is the exact meaning of "_Don't change the Timer's delay._"If i don't check inside actionPerformed, where can i?Do i have any option? – joey rohan Feb 23 '13 at 13:57
  • @GuillaumePolet True, but its tough that way.Have to do it all over again, and here, i just have a little sync. problem.This delay thing is done on the advice of the top answerer on my last post. – joey rohan Feb 23 '13 at 14:00
  • This thing gives me a very less precision, are you not trying to say calculate the time elapsed for every word rather than from the beginning to end? – joey rohan Feb 23 '13 at 14:04
  • @joeyrohan: it depends all on the granularity of your timer. Let me see if I can better "grok" your code to show a working example. Again, this code polls, and should poll at a relatively fast frequency, say a 10 to 15 msecs timer delay. – Hovercraft Full Of Eels Feb 23 '13 at 14:05
  • @joeyrohan: still working on it. Making subclasses to show my idea. – Hovercraft Full Of Eels Feb 23 '13 at 15:02
  • +1 wonderful example.Will take some time to absorb, go through and test it.Thank you for your efforts and your valuable time.Will let you know the results. – joey rohan Feb 23 '13 at 16:01
  • @HovercraftFullOfEels sorry to say, but it goes a bit fast :/ – joey rohan Feb 24 '13 at 10:55
  • @HovercraftFullOfEels But cannot make out where.Not fast, its very fast..any ideas? – joey rohan Feb 24 '13 at 13:48
  • @joeyrohan: I must have a bug. I will have to get to this later though. – Hovercraft Full Of Eels Feb 24 '13 at 14:44
  • @HovercraftFullOfEels Sorry for this. Please revise again, once i add a Bounty :) – joey rohan Feb 24 '13 at 14:47
  • 2
    @joeyrohan yes I got some but I need to work a bit on it. The basic thing is that I would like to avoid relying on polling mechanisms. I will give it some thoughs during this week but if you have a good answer, don't hesitate to give him your bounty. ONe thing that bothers me is that you only give one array of delays, but I believe you should have 2 of them: one to indicate the begining of each word, and the other for the duration of a word. – Guillaume Polet Feb 25 '13 at 19:59
  • I can't disagree with anything that @GuillaumePolet is suggesting. – Hovercraft Full Of Eels Feb 25 '13 at 23:52
  • @GuillaumePolet i din't noticed, as the highlighting of one word leads to other.Will be very thankful if i can get any help from your side. – joey rohan Feb 26 '13 at 06:14
6

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() via System.out.println() which will help us to see what is happening:

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import javax.swing.AbstractAction;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JTextPane;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.text.Style;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;

public class KaraokeTest {

    private int[] timingsArray = {1000, 1000, 9000, 1000, 1000, 1000, 1000, 1000, 1000, 1000};//word/letters timings
    private String[] individualWordsToHighlight = {" \nHello\n", " world\n", " Hello", " world", " Hello", " world", " Hello", " world", " Hello", " world"};//each individual word/letters to highlight
    private int count = 0;
    private final JTextPane jtp = new JTextPane();
    private final JButton startButton = new JButton("Start");
    private final JFrame frame = new JFrame();
    //create Arrays of individual letters and their timings
    final ArrayList<String> chars = new ArrayList<>();
    final ArrayList<Long> charsTiming = new ArrayList<>();

    public KaraokeTest() {
        initComponents();
    }

    private void initComponents() {
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setResizable(false);

        for (String s : individualWordsToHighlight) {
            String tmp = jtp.getText();
            jtp.setText(tmp + s);
        }
        jtp.setEditable(false);

        startButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent ae) {
                startButton.setEnabled(false);
                count = 0;
                charsTiming.clear();
                chars.clear();

                for (String s : individualWordsToHighlight) {
                    for (int i = 0; i < s.length(); i++) {
                        chars.add(String.valueOf(s.charAt(i)));
                        //System.out.println(String.valueOf(s.charAt(i)));
                    }
                }

                //calculate each letters timings
                for (int x = 0; x < timingsArray.length; x++) {
                    for (int i = 0; i < individualWordsToHighlight[x].length(); i++) {
                        individualWordsToHighlight[x] = individualWordsToHighlight[x].replace("\n", " ").replace("\r", " ");//replace line breaks
                        charsTiming.add((long) (timingsArray[x] / individualWordsToHighlight[x].trim().length()));//dont count spaces
                        //System.out.println(timingsArray[x] / individualWordsToHighlight[x].length());
                    }
                }

                Timer t = new Timer(1, new AbstractAction() {
                    long startTime = 0;
                    long acum = 0;
                    long timeItTookTotal = 0;
                    long dif = 0, timeItTook = 0, timeToTake = 0;
                    int delay = 0;

                    @Override
                    public void actionPerformed(ActionEvent ae) {
                        if (count < charsTiming.size()) {

                            if (count == 0) {
                                startTime = System.nanoTime();
                                System.out.println("Started: " + startTime);
                            }

                            timeToTake = charsTiming.get(count);
                            acum += timeToTake;

                            //highlight the next word
                            highlightNextWord();

                            //System.out.println("Acum " + acum);
                            timeItTook = (acum - ((System.nanoTime() - startTime) / 1000000));
                            timeItTookTotal += timeItTook;
                            //System.out.println("Elapsed since start: " + (System.nanoTime() - startTime));
                            System.out.println("Time the char should take: " + timeToTake);
                            System.out.println("Time it took: " + timeItTook);
                            dif = (timeToTake - timeItTook);
                            System.out.println("Difference: " + dif);
                            //System.out.println("Difference2 " + (timeToTake - dif));

                            //calculate start of next letter to highlight less the difference it took between time it took and time it should actually take
                            delay = (int) (timeToTake - dif);

                            if (delay < 1) {
                                delay = 1;
                            }

                            //restart timer with new timings
                            ((Timer) ae.getSource()).setInitialDelay((int) timeToTake);//timer is usually faster thus the entire highlighting will be done too fast
                            //((Timer) ae.getSource()).setInitialDelay(delay);
                            ((Timer) ae.getSource()).restart();

                        } else {//we are at the end of the array
                            long timeStopped = System.nanoTime();
                            System.out.println("Stopped: " + timeStopped);
                            System.out.println("Time it should take in total: " + acum);
                            System.out.println("Time it took using accumulator of time taken for each letter: " + timeItTookTotal
                                    + "\nDifference: " + (acum - timeItTookTotal));
                            long timeItTookUsingNanoTime = ((timeStopped - startTime) / 1000000);
                            System.out.println("Time it took using difference (endTime-startTime): " + timeItTookUsingNanoTime
                                    + "\nDifference: " + (acum - timeItTookUsingNanoTime));
                            reset();
                            ((Timer) ae.getSource()).stop();//stop the timer
                        }
                        count++;//increment counter
                    }
                });
                t.setRepeats(false);
                t.start();
            }
        });

        frame.add(jtp, BorderLayout.CENTER);
        frame.add(startButton, BorderLayout.SOUTH);

        frame.pack();
        frame.setVisible(true);
    }

    private void reset() {
        startButton.setEnabled(true);
        jtp.setText("");
        for (String s : individualWordsToHighlight) {
            String tmp = jtp.getText();
            jtp.setText(tmp + s);
        }
        JOptionPane.showMessageDialog(frame, "Done");
    }

    private void highlightNextWord() {
        //we still have words to highlight
        int sp = 0;
        for (int i = 0; i < count + 1; i++) {//get count for number of letters in words (we add 1 because counter is only incrementd after this method is called)
            sp += 1;
        }

        while (chars.get(sp - 1).equals(" ")) {
            sp += 1;
            count++;
        }

        //highlight words
        Style style = jtp.addStyle("RED", null);
        StyleConstants.setForeground(style, Color.RED);
        ((StyledDocument) jtp.getDocument()).setCharacterAttributes(0, sp, style, true);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                new KaraokeTest();
            }
        });
    }
}

The output on my PC is:

Started: 10289712615974

Time the char should take: 166

Time it took: 165

Difference 1

...

Time the char should take: 166

Time it took: 155

Difference 11

...

Time the char should take: 166

Time it took: 5

Difference 161

Stopped: 10299835063084

Time it should take in total: 9960

Time it took using accumulator of time taken for each letter: 5542

Difference: 4418

Time it took using difference (endTime-startTime): 10122

Difference: -162

Thus my conclusion is the Swing Timer is actually running faster than we expect as the code in the Timers actionPerformed 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 on restart(..) will take be at a different time i.e faster or slower.

in the code do this:

//calculate start of next letter to highlight less the difference it took between time it took and time it should actually take
delay = (int) (timeToTake - dif);


//restart timer with new timings
//((Timer) ae.getSource()).setInitialDelay((int)timeToTake);//timer is usually faster thus the entire highlighting will be done too fast
((Timer) ae.getSource()).setInitialDelay(delay);
((Timer) ae.getSource()).restart();

Produces a more accurate result (maximum latency Ive had is 4ms faster per letter):

Started: 10813491256556

Time the char should take: 166

Time it took: 164

Difference 2

...

Time the char should take: 166

Time it took: 164

Difference 2

...

Time the char should take: 166

Time it took: 162

Difference 4

Stopped: 10823452105363

Time it should take in total: 9960

Time it took using accumulator of time taken for each letter: 9806

Difference: 154

Time it took using difference (endTime-startTime): 9960

Difference: 0

David Kroukamp
  • 36,155
  • 13
  • 81
  • 138
  • @joeyrohan see my latest answer (deleted my old as it had many edits and became community wiki).. Although it no more *highlighted* the spaces it was still using the spaces when calculating the time for each letter... fixed that now by `charsTiming.add((long) (timingsArray[x] / individualWordsToHighlight[x].trim().length()));` – David Kroukamp Feb 27 '13 at 17:38
  • @joeyrohan ahh I hate these edits as I add things I forget.. See my latest edit, previous code was not clearing the arrays of chars and timings when start button is clicked... thus it would work fine the first time, and the next it would take double (as arrays have words added again) and so on and so fourth... Lol this is the last edit I promise :P – David Kroukamp Feb 27 '13 at 17:45
  • Will debugggg and see.insted of .trim() have to use something else also to remove '\n'.This thing sucks. – joey rohan Feb 27 '13 at 17:48
  • @joeyrohan perhaps simply replace all \n with white space(s) ... And put that into the array which will than be used to separate into characters excluding white spaces via `trim()` – David Kroukamp Feb 27 '13 at 17:53
  • @joeyrohan See [this](http://stackoverflow.com/questions/2163045/how-to-remove-line-breaks-from-a-file-in-java) to remove \n and put space instead. – David Kroukamp Feb 27 '13 at 17:57
  • @joeyrohan *Ah you mean replace..hehe nice trick.Thanks.* yup and its pleasure see my edited post fixed to omit \n and white space when calculating letter timing and highlighting – David Kroukamp Feb 27 '13 at 18:01
  • Sorry 2 say:/ a bug. Take timings as `{1000, 1000, 9000, 1000, 1000, 1000, 1000, 1000, 1000, 1000}` see the difference – joey rohan Mar 01 '13 at 15:15
  • For so long i was thinking it's something wrong with my logic:P its cause of "\n." – joey rohan Mar 01 '13 at 15:16
  • @joeyrohan Lol I cant believe a \n would do that to my code :P Hope I helped a little though.... So is it working now that you found the problem? – David Kroukamp Mar 01 '13 at 15:19
  • 1
    yeah it was the - "" but not " " in your replace() !! GOD!! – joey rohan Mar 01 '13 at 15:21
  • +1 OMG good eye...I see now how stupid I was.. it should have been replaced with a white space (" ") not "" – David Kroukamp Mar 01 '13 at 15:22
  • @joeyrohan edited my answer to reflect correct changes you have found... so others who use the code will be copying fully corrected code – David Kroukamp Mar 01 '13 at 15:24
  • 1
    HeHe had 2 cups of coffee for that.Can start my debugging process now. – joey rohan Mar 01 '13 at 15:25
  • Ok if i have an array having timings say x.And i am reading a file which have n lines.Then what should i change?When i read a line from a file, i store it words by words in an array (`individualWordsToHighlight`).And so the timings.Creating a problem about the "\n", also the length of both arrays.: what you suggest?PS: please don't update your answer..I want to do myself..suggest some logic please. – joey rohan Mar 01 '13 at 17:54
  • I am coping the array in which i have stored the timings.And same for string array.But, in `jtp`, text is set, such that it contains " " and `\n`.But, we are highlighting according to words in the array not according to text on `jtp`.Trying to fix it.So, doing something inside `highlightNextWord`. But i think have to convert the text in text area to charArr then check it with `sp`. – joey rohan Mar 02 '13 at 13:10
  • @joeyrohan thanks for the bounty and glad my answer was of help. Highest Ive earned so far :D... Have you gots the code working after your last comment *ahh did at last*? What was /is the problem? – David Kroukamp Mar 03 '13 at 14:09
  • 1
    Also reading my answer again *Swing Timer is running faster than it should* is actually not true its running as expected what we forgot is that the timers `actionPerformed` will only take as long as the task it has to do thereafter it will schedule a new timer restart at nextExecutionTime=nextLetterTime + currentTime, we thus have to make up for the accumulated (or lost) time. – David Kroukamp Mar 03 '13 at 14:16
  • 1
    Now also i don't know what i was doing wrong.But my other complex way did worked LOL.On `jtp` is was setting text word by word, and not `individualWordsToHighlight` array(the way you did in your SSCCE), and timings i was taking from some other array.problem was: it was highlightng -1 word after every line.It was cause of "\n".So was increasing `SP` (was checking `"\n"` through a char array(my jtp.text was converted `toCharArray`))But dint worked.So tried nesting of Scanners (cause scanners dont tell end of a line) and then added " " extra to every last word.WORKED :) – joey rohan Mar 03 '13 at 14:26
  • delay = (int) (timeToTake - dif); -> this line seems redundant. Because delay is simply equivalent to timeItTook. You can use timeItTook instead of delay. – Taha Körkem Jun 27 '23 at 12:23
4

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.

  • 2
    1+ but is this guaranteed to be more accurate than the Swing Timer? Also, your suggestion would require polling, same as mine. – Hovercraft Full Of Eels Feb 23 '13 at 15:25
  • 1
    Firing times are not necessarily more accurate, but scheduleAtFixedRate will always schedule the next execution based on the starting time rather than the previous execution like the Swing Timer does, so delays will not add up. And I don't see a reason to use polling. – aditsu quit because SE is EVIL Feb 23 '13 at 15:29
  • @HovercraftFullOfEels yes, its more accurate, +1, indeed will require polling, as same as yours. – joey rohan Feb 23 '13 at 15:29
  • Nothing is 100% accurate.Do you guarantee scheduleAtFixedRate won't take any + few MS more? – joey rohan Feb 23 '13 at 15:31
  • There is no guarantee. But if, for example, you want to fire the task every 100ms, you will probably see firing times like: 110, 201, 350, 415, 500, 620, 716, 805, 912, rather than 105, 230, 330, 455, 560, 672, 790, 903, 1014. In the 2nd case (called "fixed delay", which happens with the swing Timer, or util Timer with the schedule method), each firing time is at least 100ms later than the previous firing time. – aditsu quit because SE is EVIL Feb 23 '13 at 15:42
  • The fix delay if for just a word.This delay will change for the next word..then next... – joey rohan Feb 23 '13 at 15:48
  • 1
    Yes, the fixed delay is actually a minimum delay. And the fixed rate is more like an average rate. But I think the fixed rate option is what you are looking for. – aditsu quit because SE is EVIL Feb 23 '13 at 16:14
  • Oh, if you mean you need to use different delays for different words, then just cancel the TimerTask and schedule a new one. – aditsu quit because SE is EVIL Feb 23 '13 at 16:17
  • Still, i'll be needing a mechanism to cut short the over delay...comes to same thing...+ updating GUI – joey rohan Feb 24 '13 at 12:29
4

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 to shutdown 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.

public class Reminder {

    private static final String TEXT = "arey chod chaad ke apnee saleem ki gali anarkali disco chalo\n" +
            "arey chod chaad ke apnee saleem ki gali anarkali disco chalo\n" +
            "arey chod chaad ke apnee saleem ki gali anarkali disco chalo\n" +
            "arey chod chaad ke apnee saleem ki gali anarkali disco chalo\n" +
            "arey chod chaad ke apnee saleem ki gali anarkali disco chalo\n" +
            "arey chod chaad ke apnee saleem ki gali anarkali disco chalo";
    private static final String[] WORDS = TEXT.split("\\s+");
    private JFrame frame;
    private StyledDocument doc;
    private JTextPane textpane;
    private static final int[] TIMES = {100, 400, 300, 900, 1000, 600, 200, 700, 700, 200, 200, 
                                        100, 400, 300, 900, 1000, 600, 200, 700, 700, 200, 200,
                                        100, 400, 300, 900, 1000, 600, 200, 700, 700, 200, 200,
                                        100, 400, 300, 900, 1000, 600, 200, 700, 700, 200, 200,
                                        100, 400, 300, 900, 1000, 600, 200, 700, 700, 200, 200,
                                        100, 400, 300, 900, 1000, 600, 200, 700, 700, 200, 200, 200};
    private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);
    private int currentLetterIndex;
    private long start; //for logging

    public void startColoring() {
        start = System.currentTimeMillis(); //for logging
        int startTime = TIMES[0];
        for (int i = 0; i < WORDS.length; i++) {
            scheduler.schedule(colorWord(i, TIMES[i + 1]), startTime, TimeUnit.MILLISECONDS);
            startTime += TIMES[i+1];
        }
        scheduler.schedule(new Runnable() {

            @Override
            public void run() {
                scheduler.shutdownNow();
            }
        }, startTime, TimeUnit.MILLISECONDS);
    }

    //Color the given word, one letter at a time, for the given duration
    private Runnable colorWord(final int wordIndex, final int duration) {
        final int durationPerLetter = duration / WORDS[wordIndex].length();
        final int wordStartIndex = currentLetterIndex;
        currentLetterIndex += WORDS[wordIndex].length() + 1;
        return new Runnable() {
            @Override
            public void run() {
                System.out.println((System.currentTimeMillis() - start) + " ms - Word: " + WORDS[wordIndex] + "  - duration = " + duration + "ms");
                for (int i = 0; i < WORDS[wordIndex].length(); i++) {
                    scheduler.schedule(colorLetter(wordStartIndex + i), i * durationPerLetter, TimeUnit.MILLISECONDS);
                }
            }
        };
    }

    //Color the letter on the EDT
    private Runnable colorLetter(final int letterIndex) {
        return new Runnable() {
            @Override
            public void run() {
                SwingUtilities.invokeLater(new Runnable() {
                    @Override
                    public void run() {
                        System.out.println("\t" + (System.currentTimeMillis() - start) + " ms - letter: " + TEXT.charAt(letterIndex));
                        doc.setCharacterAttributes(letterIndex, 1, textpane.getStyle("Red"), true);
                    }
                });
            }
        };
    }

    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.invokeLater(new Runnable() {
            @Override
            public void run() {
                Reminder reminder = new Reminder();
                reminder.initUI();
                reminder.startColoring();
            }
        });
    }
}
assylias
  • 321,522
  • 82
  • 660
  • 783
  • Will take some time to debug.Thank you for your answer.Hope it works. – joey rohan Feb 25 '13 at 17:45
  • To my surprise,it works fine as any thing for the first four lines.After that, the speed increases drastically.My many lines are separated by '\n' so in your for loop which schedules next letter, i added: `for (int i = 0; i < WORDS[wordIndex].length(); i++) { if(WORDS[wordIndex].equals("\n"))continue;` but still, it counts `\n` as an extra letter after every end of line.Thats not a problem,by doing this, it should get a bit slow..but it runs fast after some time.Ideas? – joey rohan Feb 26 '13 at 12:40
  • @joeyrohan Note: I have only changed the text, the splitting to also split newlines and the time array - the rest is the same. – assylias Feb 26 '13 at 12:51
  • It works a lot better now.I have tried to run it around 20 times, and after some time,(not 4 lines) it becomes a bit fast.Say around 3 words ahead. – joey rohan Feb 26 '13 at 13:11
  • @joeyrohan Have you added the delay between lines to your `TIMES` array? – assylias Feb 26 '13 at 13:27
  • Hey if i am not wrong, is your logic mising a word?I mean its highlighting for i-1 word insted of i !! please check!! – joey rohan Feb 26 '13 at 13:34
  • @joeyrohan I have assumed that the first entry in your array is the delay before starting - if it is not then yes, everything will be offset. – assylias Feb 26 '13 at 13:44
  • For example: `arey` starts after 100ms and lasts 400ms, `chod` starts at 500ms and lasts 300ms etc. In the end there should be no noticeable delay if the `TIMES` array is accurate. – assylias Feb 26 '13 at 13:45
  • Then its a huge problem.If give 800 ms to chod, but this delay will be for the previous word..ok I will try to set some dummy first value, and see what happens – joey rohan Feb 26 '13 at 13:50
  • @joeyrohan Just change the counter in the loop of add a `0` at the beginning of the array. – assylias Feb 26 '13 at 13:55
  • That problem is over, but still, the delay is unnoticeable for few lines.But as the no. of lines are increased, so is the delay.Little less than answer by @Hovercraft Full Of Eels .But much more easier +1 – joey rohan Feb 26 '13 at 14:16
  • @joeyrohan Can you give a sample text so I can test it? – assylias Feb 26 '13 at 15:59
  • The text dosn't matter.Its a long procedure.First take the timings for individual words through a key hit.(Can provide you the class) Then store the timings(note: the song is played in background).Once its over, now comes your logic.Display any lyrics, and insted of your array, take the array in which you have the timings.Same,play the song in background.Now you can exactly know where you are going fast or slow.Can provide you the time setter class, if you need. – joey rohan Feb 26 '13 at 16:05
  • @joeyrohan When you say there is delay, is that based on the difference between actual time and expected time as printed by the sample code I gave? Or do you simply notice a delay vs. music (in which case it could also be due to the times being inaccurate in the first place)? – assylias Feb 26 '13 at 16:28
  • "_which case it could also be due to the times being inaccurate in the first place_" True.But that delay will pass on.Till now never happened, but assume the delay is such that (between song played and the highlighting process) it delays a word(more than enough).Than that delay should carry on .EG highlighting actual nth word may lead highlighting n-1 word(according to the song) but here, the case is, it grows more than n+5 or n+7...so on.But, its not the case.The song at starting is with the accordance with the words highlighted. – joey rohan Feb 26 '13 at 17:26