Issues: Creating a very accurate Swing Timer

前端 未结 4 515
佛祖请我去吃肉
佛祖请我去吃肉 2020-12-31 12:27

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

相关标签:
4条回答
  • 2020-12-31 13:06

    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

    0 讨论(0)
  • 2020-12-31 13:13

    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
                + "]";
       }
    
    }
    
    0 讨论(0)
  • 2020-12-31 13:20

    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();
                }
            });
        }
    }
    
    0 讨论(0)
  • 2020-12-31 13:22

    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.

    0 讨论(0)
提交回复
热议问题