Search code examples
javamultithreadingaudiokeystroke

Why doesn't my Java application play the sound at each loop?


I'm trying to make a Java application that simulates someone typing on their keyboard. The keystroke sound is played in a loop (Java chose a keystroke sound among others randomly and plays it) at a variable interval (to simulate a real person typing).

It works fine in the beginning, but after around the 95th iteration, it stops playing the sound (while still looping) for less than 4 seconds then plays the sound again. And after the 160th iteration, it plays the sound almost every second (instead of every third to sixth of a second).
After a while, it stops playing the sound for a long time, then forever.

Here is the source for the AudioPlayer.java class:

package entity;

import java.io.File;
import java.io.IOException;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Clip;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.UnsupportedAudioFileException;

public class AudioPlayer implements Runnable {
    private String audioFilePath;

    public void setAudioFilePath(String audioFilePath) {
         this.audioFilePath = audioFilePath;
    }

    @Override
    public void run() {
        File audioFile = new File(audioFilePath);

        try {
            AudioInputStream audioStream = AudioSystem.getAudioInputStream(audioFile);
            AudioFormat format = audioStream.getFormat();
            DataLine.Info info = new DataLine.Info(Clip.class, format);
            Clip audioClip = (Clip) AudioSystem.getLine(info);
            audioClip.open(audioStream);
            audioClip.start();
            boolean playCompleted = false;
            while (!playCompleted) {
                try {
                    Thread.sleep(500);
                    playCompleted = true;
                }
                catch (InterruptedException ex) {
                    ex.printStackTrace();
                }
            }
            audioClip.close();
        } catch (UnsupportedAudioFileException ex) {
            System.out.println("The specified audio file is not supported.");
            ex.printStackTrace();
        } catch (LineUnavailableException ex) {
            System.out.println("Audio line for playing back is unavailable.");
            ex.printStackTrace();
        } catch (IOException ex) {
            System.out.println("Error playing the audio file.");
            ex.printStackTrace();
        }
    }
}

And here is the Main.java class to test the keystroke simulator:

package sandbox;

import java.util.Random;

import entity.AudioPlayer;

public class Main {
    public static void main(String[] args) {
        Random rnd = new Random();
        AudioPlayer audio;

        for(int i = 0; i < 10000; i++) {
            int delay = rnd.nextInt(200)+75;
            try {
                Thread.sleep(delay);
            }
            catch (InterruptedException ie) {}
            int index = rnd.nextInt(3)+1;
            audio = new AudioPlayer();
            audio.setAudioFilePath("resources/keystroke-0"+index+".wav");
            Thread thread = new Thread(audio);
            thread.start();
            System.out.println("iteration "+i);
        }
    }
}

I used multiple short (less than 200ms) wave files of different sounding keystrokes (3 in total) all in the resources directory.

EDIT

I read your answers and comments. And I'm thinking maybe I misundertood them because the suggested solutions don't work, or maybe I should have made myself clear on what I exactly wanted. Also, I need to note that I don't use threads often (and have no clue what a mutex is).

So I'll first explain what I exactly want the program to do. It should be able to simulate keystroke and so I used a Thread because it allows two keystroke sounds to overlap just like when a real person is typing. Basically the sound clips I am using are keystroke sounds and a keystroke sound is composed of two sounds: the sound of a key being pressed. the sound of a key being released.

If at some point the program allows two keystroke to overlap it will sound as if someone pressed one key then another and then released the first key. That's how really typing sounds like!

Now the issues I encountered using the proposed solutions are: When calling the run() method of the AudioPlayer directly,

public static void main(String[] args)
{
    // Definitions here

    while (running) {
        Date previous = new Date();
        Date delay = new Date(previous.getTime()+rnd.nextInt(300)+75);

        // Setting the audio here

        audio.run();
        Date now = new Date();

        if (now.before(delay)) {
            try { 
                Thread.sleep(delay.getTime()-now.getTime());
            } catch (InterruptedException e) {
            }
        }

        System.out.println("iteration: "+(++i));
    }
}

the sounds play sequentially (one after the other) and at a rate that depends on the sleep duration of the AudioPlayer (or depends on the delay if the delay in the main() method is higher than the sleep duration of the AudioPlayer), which is no good because it won't sound like the average typist (more like someone who is new to typing and still looking for every keys when typing).

When calling the join() method of the AudioPlayer's Thread,

public static void main(String[] args)
{
    //Variable definitions here

    while (running) {
        int delay = rnd.nextInt(200)+75;
        try
        {
            Thread.sleep(delay);
        }
        catch (InterruptedException ie)
        {

        }

        //Setting the AudioPlayer and creating its Thread here

        thread.start();
        try
        {
            thread.join();
        }
        catch(InterruptedException ie)
        {

        }
        System.out.println("iteration "+(++i));
    }
}

the sounds play sequentially as well and at a rate that depends on the sleep duration of the AudioPlayer (or depends on the delay if the delay in the main() method is higher than the sleep duration of the AudioPlayer) which, again, is no good for the same reason as before.

So, to answer one of the commenter's question. Yes! there are other concerns not expressed before which require the threads in the first place.

I found a workaround that "solves" my issue (but that I don't consider as a proper solution since I am, in a way, cheating): What I did is increase the sleep duration of the AudioPlayer to something that is unlikely to be reached before the program is stopped (24 hours) and from what I've seen it doesn't use much resources even after more than an hour.

You can check out what I want, what I get when running the suggested solutions and what I get using my workaround on this youtube videos (Unfortunately StackOverflow doesn't have video uploading feature. so I had to put it on youtube).

EDIT The sound effects can be downloaded here.


Solution

  • How about this single-threaded solution which is a cleaner version of your own, but re-using already opened clips from buffers? To me the typing sounds pretty natural even though there are no two sounds playing at the same time. You can adjust the typing speed by changing the corresponding static constants in the Application class.

    package de.scrum_master.stackoverflow.q61159885;
    
    import javax.sound.sampled.AudioFormat;
    import javax.sound.sampled.Clip;
    import javax.sound.sampled.DataLine.Info;
    import javax.sound.sampled.LineUnavailableException;
    import javax.sound.sampled.UnsupportedAudioFileException;
    import java.io.Closeable;
    import java.io.File;
    import java.io.IOException;
    import java.util.HashMap;
    import java.util.Map;
    
    import static javax.sound.sampled.AudioSystem.getAudioInputStream;
    import static javax.sound.sampled.AudioSystem.getLine;
    
    public class AudioPlayer implements Closeable {
      private final Map<String, Clip> bufferedClips = new HashMap<>();
    
      public void play(String audioFilePath) throws IOException, UnsupportedAudioFileException, LineUnavailableException {
        Clip clip = bufferedClips.get(audioFilePath);
        if (clip == null) {
          AudioFormat audioFormat = getAudioInputStream(new File(audioFilePath)).getFormat();
          Info lineInfo = new Info(Clip.class, audioFormat);
          clip = (Clip) getLine(lineInfo);
          bufferedClips.put(audioFilePath, clip);
          clip.open(getAudioInputStream(new File(audioFilePath)));
        }
        clip.setMicrosecondPosition(0);
        clip.start();
      }
    
      @Override
      public void close() {
        bufferedClips.values().forEach(Clip::close);
      }
    }
    
    package de.scrum_master.stackoverflow.q61159885;
    
    import javax.sound.sampled.LineUnavailableException;
    import javax.sound.sampled.UnsupportedAudioFileException;
    import java.io.IOException;
    import java.util.Random;
    
    public class Application {
      private static final Random RANDOM = new Random();
    
      private static final int ITERATIONS = 10000;
      private static final int MINIMUM_WAIT = 75;
      private static final int MAX_RANDOM_WAIT = 200;
    
      public static void main(String[] args) throws UnsupportedAudioFileException, IOException, LineUnavailableException {
        try (AudioPlayer audioPlayer = new AudioPlayer()) {
          for (int i = 0; i < ITERATIONS; i++) {
            sleep(MINIMUM_WAIT + RANDOM.nextInt(MAX_RANDOM_WAIT));
            audioPlayer.play(randomAudioFile());
          }
        }
      }
    
      private static void sleep(int delay) {
        try {
          Thread.sleep(delay);
        } catch (InterruptedException ignored) {}
      }
    
      private static String randomAudioFile() {
        return "resources/keystroke-0" + (RANDOM.nextInt(3) + 1) + ".wav";
      }
    }
    

    You might have noticed that the AudioPlayer is Closeable, i.e. you can use "try with resouces" in the calling application. That way it makes sure that at the end of the program all clips are closed automatically.

    The key to replaying the same clip is of course clip.setMicrosecondPosition(0) before you start.


    Update: If you want to simulate multiple persons, just modify the main class like this. BTW, I don't know anything about audio programming and whether there is a way to better deal with mixers and overlapping sounds. It is just a proof of concept in order to give you an idea. There is one thread per person, but each person types in a serial fashion, not two keys at the same time. But multiple persons can overlap because there is one AudioPlayer per person with its own set of buffered clips.

    package de.scrum_master.stackoverflow.q61159885;
    
    import java.util.Random;
    
    public class Application {
      private static final Random RANDOM = new Random();
    
      private static final int PERSONS = 2;
      private static final int ITERATIONS = 10000;
      private static final int MINIMUM_WAIT = 150;
      private static final int MAX_RANDOM_WAIT = 200;
    
      public static void main(String[] args) {
        for (int p = 0; p < PERSONS; p++)
          new Thread(() -> {
            try (AudioPlayer audioPlayer = new AudioPlayer()) {
              for (int i = 0; i < ITERATIONS; i++) {
                sleep(MINIMUM_WAIT + RANDOM.nextInt(MAX_RANDOM_WAIT));
                audioPlayer.play(randomAudioFile());
              }
            } catch (Exception ignored) {}
          }).start();
      }
    
      private static void sleep(int delay) {
        try {
          Thread.sleep(delay);
        } catch (InterruptedException ignored) {}
      }
    
      private static String randomAudioFile() {
        return "resources/keystroke-0" + (RANDOM.nextInt(3) + 1) + ".wav";
      }
    }