Determine when to close a sound-playing thread in Java - java

I am playing of a sound-file in Java, and is looking for a simple way to determine when the sound-file has finished playing so I can kill the thread. Is there a simple way to accomplish this?

Sorry this is a little late, but I just ran into an issue today that sounds suspiciously familiar to this one. In some game code, Im using javax.sound.sampled.Clip to play various sounds, I found that if I didn't explicitly call line.close() once it was finished, the count of native threads in the profiler would just sky-rocket until I got an OutOfMemory error.
// this just opens a line to play the sample
Clip clip = AudioSystem.getClip();
clip.open( audioFormat, sounddata, 0, sounddata.length);
clip.start();
// at this point, there is a native thread created 'behind the scenes'
// unless I added this, it never goes away:
clip.addLineListener( new LineListener() {
public void update(LineEvent evt) {
if (evt.getType() == LineEvent.Type.STOP) {
evt.getLine().close();
}
}
});
My presumption is that the clip creates a thread to meter out the sample bytes into the line, but the thread hangs around after that in case you want to re-use the clip again. My second presumption is that somewhere something in my code must have a reference to the clip, or vice-versa, but at any rate, the snippet above duct-taped the problem.
Hope this is useful to someone.

Comments
You play sound either synchronously or asynchronously.
In case you play it synchronously (blocking the calling thread), you know when the sound ends playing — your code gain control then.
In case you play it asynchronously (allowing a different thread to be created), the helper thread will terminate itself right after finishing playing.
P.S.
Please share your observations on memory leaking and reasons underneath the question.

Why do you want to kill the thread? It will go away on its own once the it terminates.
If you insist, set up a synchronized method you can call to set a "die now" flag; check that flag periodically.

I dont use clips, it takes more time to load in memory, depending the size of the file you are reading.
I preefer reading the bytes, and use this method I created:
public void play(File file) throws UnsupportedAudioFileException, IOException, LineUnavailableException, InterruptedException
{
AudioInputStream encoded = AudioSystem.getAudioInputStream(file);
AudioFormat encodedFormat = encoded.getFormat();
AudioFormat decodedFormat = this.getDecodedFormat(encodedFormat);
line = AudioSystem.getSourceDataLine(decodedFormat);
currentDecoded = AudioSystem.getAudioInputStream(decodedFormat, encoded);
line.open(decodedFormat);
line.start();
byte[] b = new byte[this.bufferSize];
int i = 0;
synchronized(lock){
while(true)
{
i = currentDecoded.read(b, 0, b.length);
if(i == -1)
break;
line.write(b, 0, i);
if(paused == true)
{
line.stop();
lock.wait();
line.start();
}
}
}
line.drain();
line.stop();
line.close();
currentDecoded.close();
encoded.close();
}
it uses this method:
protected AudioFormat getDecodedFormat(AudioFormat format)
{
AudioFormat decodedFormat = new AudioFormat(
AudioFormat.Encoding.PCM_SIGNED, // Encoding to use
format.getSampleRate(), // sample rate (same as base format)
16, // sample size in bits (thx to Javazoom)
format.getChannels(), // # of Channels
format.getChannels()*2, // Frame Size
format.getSampleRate(), // Frame Rate
false // Big Endian
);
return decodedFormat;
}

Related

Unwanted downsampling : Java Sound

I have been trying to manually read a wav file in Java and read an array of bytes then write to an audio buffer for playback. I am receiving playback but it is heavily distorted. Java sound supports 16 bit sample rates but not 24-bit.
I went in to Logic 9 and exported a 24-bit audio file in to 16-bit and then used with my program. Originally, the 24-bit samples would produces white noise. Now I can hear my sample but very distorted and sounds like it has been bit crushed.
Can anyone help me to get a clean signal?
I am very new to audio programming but I am currently working on a basic Digital Audio Workstation.
import javax.sound.sampled.*;
import javax.sound.sampled.DataLine.Info;
import javax.swing.filechooser.FileNameExtensionFilter;
import java.io.*;
public class AudioData {
private String filepath;
private String filepath1;
private File file;
private byte [] fileContent;
private Mixer mixer;
private Mixer.Info[] mixInfos;
private AudioInputStream input;
private ByteArrayOutputStream byteoutput;
public static void main (String [] args) {
AudioData audiodata = new AudioData();
}
public AudioData () {
filepath = "/Users/ivaannagen/Documents/Samples/Engineering Samples - Obscure Techno Vol 3 (WAV)/ES_OT3_Kit03_Gmin_130bpm/ES_OT3_Kit03_FX_Fast_Snare_Riser_Gmin_130bpm.wav";
filepath1 = "/Users/ivaannagen/Documents/Samples/dawsampletest.wav";
file = new File (filepath1);
readAudio();
}
public void readAudio () {
mixInfos = AudioSystem.getMixerInfo();
mixer = AudioSystem.getMixer(mixInfos[0]);
AudioFormat format = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, 44100, 16, 2, 4, 44100, false);
// set up an audio format.
try {
DataLine.Info info = new DataLine.Info(SourceDataLine.class, format); // creates data line with class type and audio format.
SourceDataLine source = (SourceDataLine) AudioSystem.getLine(info);
System.out.println("Size of data line buffer: " + source.getBufferSize());
fileContent = new byte [source.getBufferSize() / 50];
byteoutput = new ByteArrayOutputStream();
input = AudioSystem.getAudioInputStream(file);
int readBytes = 0;
while ((readBytes = input.read(fileContent, 0, fileContent.length)) != -1) {
byteoutput.write(fileContent, 0, readBytes);
}
System.out.println("Size of audio buffer: " + fileContent.length);
//byteoutput.write(0);
// byteoutput.write(0);
System.out.println("Size of audio buffer: " + byteoutput.size());
source.open(format, source.getBufferSize()); // line must be open to be recognised by the mixer.
Line[] lines = mixer.getSourceLines();
System.out.println("mixer lines: " + lines.length);
// for(byte bytes: fileContent) {
// System.out.println(bytes);
// }
Thread playback = new Thread () {
public void run () {
// System.out.println((byteoutput.size() +2) % 4);
source.start(); // play (buffer originally empty)
source.write(byteoutput.toByteArray(), 0, byteoutput.size()); // write input bytes to output buffer
} // end run (to do).
}; // end thread action
playback.start(); // start thread
}
catch (LineUnavailableException lue) {
System.out.println(lue.getMessage());
}
catch (FileNotFoundException fnfe) {
System.out.println(fnfe.getMessage());
}
catch(IOException ioe) {
System.out.println(ioe.getMessage());
}
catch(UnsupportedAudioFileException uafe) {
System.out.println(uafe.getMessage());
}
}
}
Whether or not you can load and play a 24-bit file is system dependent, afaik.
I use Audacity for conversions. You should be able import your file into Audacity and export it as 16-bit, stereo, little-endian, 44100 fps, and then load that export with Java's AudioInputStream.
What you hear when playing from Audacity or from Java should be pretty much identical (adjusting for volume). If not, the most likely reason probably pertains to a mistake or overlook in the code, which is very easy to do.
The use of a ByteOutputStream in your code is superfluous. Read from the AudioInputStream into a fixed-size byte array (size being the buffer length, I recommend trying 8 or 16 * 1024 bytes as a first try) and then use the SourceDataLine write method to ship that array.
Following is code that works on my system for loading a playing a "CD Quality" wav called "a3.wav" that I have that is in the same directory as the Java class. You should be able to swap in your own 44100, 16-bit, stereo, little-endian wav file.
I've commented out an attempt to load and play a 24-bit wav file called "spoken8000_24.wav". That attempt gave me an IllegalArgumentException: No line matching interface SourceDataLine supporting format PCM_SIGNED 8000.0 Hz, 24 bit, stereo, 6 bytes/frame, little-endian is supported.
I have to admit, I'm unclear if my system doesn't provide the needed line or if I might have coded the format incorrectly! My OS can certainly play the file. So I'm thinking there is a distinction between what an OS can do and what a "Mixer" on a given system provides to Java.
As a get-around, I just always convert everything to "CD Quality" format, as that seems to be the most widely supported.
public class TriggerSound_SDL extends JFrame
{
public TriggerSound_SDL()
{
JButton button = new JButton("Play Sound");
button.addActionListener(e -> new Thread(() -> playBuzzer()).start());
getContentPane().add(button);
}
private void playBuzzer()
{
try
{
URL url;
url = getClass().getResource("a3.wav");
// url = getClass().getResource("spoken8000_24.wav");
AudioInputStream ais = AudioSystem.getAudioInputStream(url);
System.out.println(ais.getFormat());
AudioFormat audioFmt;
// "CD Quality" 44100 fps, 16-bit, stereo, little endian
audioFmt = new AudioFormat(
AudioFormat.Encoding.PCM_SIGNED,
44100, 16, 2, 4, 44100, false);
// 8000 fps, 32-bit, stereo
// audioFmt = new AudioFormat(
// AudioFormat.Encoding.PCM_SIGNED,
// 8000, 24, 2, 6, 8000, false);
Info info = new DataLine.Info(SourceDataLine.class,
audioFmt);
SourceDataLine sdl = (SourceDataLine)AudioSystem.getLine(info);
int bufferSize = 16 * 1024;
byte[] buffer = new byte[bufferSize];
sdl.open(audioFmt, bufferSize);
sdl.start();
int numBytesRead = 0;
while((numBytesRead = ais.read(buffer)) != -1)
{
sdl.write(buffer, 0, numBytesRead);
}
}
catch (IOException | UnsupportedAudioFileException
| LineUnavailableException ex)
{
ex.printStackTrace();
}
}
private static void createAndShowGUI()
{
JFrame frame = new TriggerSound_SDL();
frame.setDefaultCloseOperation(DISPOSE_ON_CLOSE);
frame.pack();
frame.setVisible(true);
}
public static void main(String[] args)
{
SwingUtilities.invokeLater(() -> createAndShowGUI());
}
}
This code, with some small tweaks should let you at least test the different formats.
EDIT:
I'm seeing where your goal is to make a DAW!
In that case, you will want to convert the bytes to PCM data. Can I suggest you borrow some code from AudioCue? I basically wrote it to be a Clip-substitute, and part of that involved making the PCM data available for manipulation. Some techniques for mixing, playing back at different frequencies, multithreading can be found in it.
Thanks for all the advice guys. I will be getting rid of the ByteOutputStream and just use the AudioInputStream, I now understand what I was doing was unnecessary!! Thanks for the advice all! I have indeed tried using AudioCue but it is not low level enough for what I want to do!
One more thing guys. Previously, I created a multitrack media player which is using the Clip class. To play all the audio tracks together, I was looping through a list of Clips and playing them. However, this means that all tracks may be playing a tiny amount after each other due to the processing of the loop. Also, Clip class created a new thread per audio. I do not wants 100 threads running on 100 tracks, I want one thread for my audio output. I am still trying to work out how to start all tracks at the same time without a loop....(im guessing AudioCue have nailed the concurrent cues).
Does anyone know the best way to play multiple audio tracks in to one output? Do I need to route/bus all my audio tracks in to one output and somehow write all data from audio files in to one output buffer then play this output in a thread?
Thanks!!

How do I efficiently close a large number of clips as soon as they have finished playing?

I have a game where sound effects are played using this method:
AudioInputStream inputStream = AudioSystem.getAudioInputStream(
TheGame.class.getResource(url));
Clip clip = AudioSystem.getClip();
clip.open(inputStream);
clip.start();
However, not closing the clips obviously builds up memory, and the fixes I've tried cause the game to freeze up occasionally.
I tried :
creating a new LineListener for every single clip that closes the clip when it stops. freezes up game OFTEN.
Having one LineListener that listens to every clip that closes them when they stop. freezes often as well.
Same as #2, but dividing the clips among multiple LineListeners, I've tried 4, 6, and 10, and all freeze up the game at least once.
Never closing the clips, eventually the game will stop playing sounds and freeze up
setting the clips to null as soon as they're opened, same result as #4
I'm not sure why closing clips seems to take so much time that it freezes up the entire game, is there a more time-efficient way?
Here is the code for the LineListener's update method I'm using:
#Override
public void update(LineEvent event) {
if (event.getType() == LineEvent.Type.STOP){
Clip c = (Clip) event.getSource();
c.close();
c.removeLineListener(this);
}
}
As per my comment above, you should reuse the existing Clip object.
Map<String, Clip> clips = new HashMap<String, Clip>();
public synchronized void play(String url) {
Clip clip = clips.get(url);
if(clip == null) {
AudioInputStream inputStream = AudioSystem.getAudioInputStream(
TheGame.class.getResource(url));
Clip clip = AudioSystem.getClip();
clip.open(inputStream);
clips.put(url, clip);
}
if(clip.isRunning())
clip.stop();
clip.setFramePosition(0);
clip.start();
}
You will need to get a bit more creative if you need to overlap the same sound with itself. Note that I have placed this in a synchronized method to protect the Map from concurrent modification if you should happen to be calling this from multiple threads.

How to wait till an audio clip is loaded?

I'm quite a newbie in JAVA and I am trying to read a clip. Here is my code :
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
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.LineEvent;
import javax.sound.sampled.LineListener;
public class TestClipBis {
protected static AudioFormat audioFormat;
public static void main(String[] args) throws Exception {
// Specification of the sound to play
// No control. We assume that the sound can be played on audio system
//File soundFile = new File("chimes.wav");
File soundFile = new File("test.wav");
InputStream is = new FileInputStream(soundFile);
InputStream bufferedIn = new BufferedInputStream(is);
//AudioInputStream sound = AudioSystem.getAudioInputStream(soundFile);
AudioInputStream sound = AudioSystem.getAudioInputStream(bufferedIn);
audioFormat = sound.getFormat();
System.out.println(audioFormat);
// Loading the sound into the memory (a Clip)
DataLine.Info info = new DataLine.Info(Clip.class, sound.getFormat());
System.out.println(info);
//Clip clip = (Clip) AudioSystem.getClip();
Clip clip = (Clip) AudioSystem.getLine(info);
System.out.println("Sound frame lenght : "+sound.getFrameLength());
System.out.println("Clip FrameLength before opening : "+clip.getFrameLength());
System.out.println("Clip will open - "+info);
System.out.println("Info format : "+info.getLineClass());
// Check before this line that everything is in memory
// Yes, but how ?
clip.open(sound);
System.out.println("Clip is open");
System.out.println("Clip FrameLength after opening : "+clip.getFrameLength());
// Due to a bug in Java Sound,
// we explicitly out of the VM when the sounds stop
clip.addLineListener(new LineListener() {
public void update(LineEvent event) {
if (event.getType() == LineEvent.Type.STOP) {
System.out.println("Methode de sortie");
event.getLine().close();
System.exit(0);
}
}
});
// Playing the clip
clip.start();
System.out.println("IsActive : "+clip.isActive());
//clip.close();
}
}
My problem is how to be sure that the clip is loaded in memory before opening and playing it ? With the above code, when I open and play the sound file, I have few seconds of playing but never the same length, randomly, and never the full song.
Or should I use something else than a clip to play a song? But I want to "move" into the song and not only streaming it from the start to the end.
Edit:
Ok, I tried few things. First, I tried to see if the "ByteArrayOuptputStream" was written. I had a "println" in the loop and yes, all is written but it don't fix the problem.
Then, when I open the clip, I tried to add the parameters : audioformat, bytearray, startpoint, bufferlength. Nothing better.
Then, I noticed that when the sounds stop, the method to exit was used. So, I tried to "mute" that method (with comment signs). The result is different : it read the file but the sound is jerky. And when I check the CPU use, it's around 100%. Is it a first clue to guess what's the problem ?
I tried to make a loop that indicates the frameposition after the start : all the frames are read but the sound is still jerky.
I also tried the thead.sleep before and after the start method : nothing better.
So, here is the code I use. Many code parts are between "comment quotes" because they are try, unsuccessfull...
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.nio.ByteBuffer;
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.LineEvent;
import javax.sound.sampled.LineListener;
public class TestBufferedClip {
protected static AudioFormat audioFormat;
public static ByteBuffer buffer;
public static void main(String[] args) throws Exception {
// Specification of the sound to play
// No control. We assume that the sound can be played on audio system
//File soundFile = new File("chimes.wav");
File soundFile = new File("test.wav");
FileInputStream fis = new FileInputStream(soundFile);
ByteArrayOutputStream baos = new ByteArrayOutputStream((int)soundFile.length());
byte[] buf = new byte[1024];
int n = 0;
int loop = 0;
while ((n=fis.read(buf))>=0) {
baos.write(buf);
buf=new byte[1024];
System.out.println("Loop = "+loop);
loop+=1;
}
byte[] ba = baos.toByteArray();
System.out.println("ByteArray size : "+ba.length);
ByteArrayInputStream bais = new ByteArrayInputStream(ba);
//AudioInputStream sound = AudioSystem.getAudioInputStream(soundFile);
AudioInputStream sound = AudioSystem.getAudioInputStream(bais);
audioFormat = sound.getFormat();
System.out.println(audioFormat);
// Loading the sound into the memory (a Clip)
DataLine.Info info = new DataLine.Info(Clip.class, sound.getFormat());
System.out.println("Info :"+info);
//Clip clip = (Clip) AudioSystem.getClip();
Clip clip = (Clip) AudioSystem.getLine(info);
System.out.println("Sound frame lenght : "+sound.getFrameLength());
System.out.println("Info format : "+info.getLineClass());
// Check before this line that everything is in memory
// Yes, but how ?
clip.open(audioFormat, ba, 0, ba.length);
//clip.open(sound);
System.out.println("Clip is open");
System.out.println("Clip FrameLength after opening : "+clip.getFrameLength());
// Due to a bug in Java Sound,
// we explicitly out of the VM when the sounds stop
/*
clip.addLineListener(new LineListener() {
public void update(LineEvent event) {
if (event.getType() == LineEvent.Type.STOP) {
System.out.println("Methode de sortie");
event.getLine().close();
System.exit(0);
}
}
});
*/
// Playing the clip
System.out.println("Before thread sleep");
try {
Thread.sleep(31000);
} catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("After thread sleep");
clip.start();
System.out.println("IsActive : "+clip.isActive());
/*
while (clip.isActive()==true) {
System.out.println("Position = "+clip.getFramePosition());
}
*/
//clip.close();
}
}
#Phil Freihofner :
I thought about your solution to read and discards data until I reach my "start" point. You wrote "In order to start at a point within the audio file, using a SourceDataLine, you would have to read and discard data from the audio input line until you got to the desired starting spot". How do you do that ? When I use the "SourceDataLine" method, my start method is a loop with a line.write(bytes, 0, bytesreads); to point the sound on the speakers.
So, how do you just read and discard ? I didn't find any "read" method with the line.
javax.sound.sampled supports two objects for playing back audio. The Clip, as you are using, has to be loaded completely into memory before one can play it back. On the plus side, it is also easy to position the playback to start from within the Clip, either using microseconds or frame position.
I see no benefit from first loading the sound into a byte buffer. That is a redundant level of buffering for Clips. I'd only do it if you were trying to do DSP or something else that requires getting to the data, something beyond the Clip's built in ability to set a start point.
If you are able to preload the possible audio choices as Clips before they are selected, that might be the best solution, as long as you don't run out of RAM.
The other option for playback is a SourceDataLine. It reads and plays back the file on a per-buffer basis. Thus, it can start up quicker than an unloaded Clip (no need to load the entire file into memory first). However, once the Clip is preloaded, the Clip will play back without having to do repeated file loads.
In order to start at a point within the audio file, using a SourceDataLine, you would have to read and discard data from the audio input line until you got to the desired starting spot. You can do this by counting frames (the format will tell you the number of bytes per frame). This reading and discarding would disrupt timing a bit, but my experience has been that reading and discarding audio data is a couple of orders of magnitude faster than playback, since playback involves blocking queues to keep the output at the required frame rate.
Check the Java Sound Tutorials for more info, which includes links to the Clip and SourceDataLine APIs.
Here is an example of the loading of a Clip:
File soundFile = new File("test.wav");
AudioInputStream sound = AudioSystem.getAudioInputStream(soundFile);
DataLine.Info info = new DataLine.Info(Clip.class, sound.getFormat());
Clip clip = (Clip) AudioSystem.getLine(info);
clip.open(sound);
The data from test.wav should now be in RAM. All that other stuff you have using byte buffers and buffered lines is unnecessary.
When you are ready to play, use clip.setMicrosecondPosition(long milliseconds) to set your sound to start at the desired location, (not needed if you are starting from the beginning, unless you've already played the Clip, in which case the position will be where it was when you stopped). Then use clip.start() to commence playing.
IMPORTANT NOTE: playback will end prematurely if the program running it exits. One way to test this is to put a Thread.sleep(long milliseconds) command after the clip.start(), where the value of milliseconds is longer than the length of the clip. But that is not a real solution, just a diagnostic to prevent the program from closing the clip playback thread. You should be handling keeping the program running from the main threads, not the thread with the audio playback.
First read whole into a byte buffer. Do this by copying all content from file to ByteArrayOutputStream. This way you will have whole media content in memory. Now you can wrap array from ByteArrayOutputStream.toByteArray() into ByteArrayInputStream and provide that pure in-memory stream as audio input stream.

disable other sounds in java

I wrote a program in Java using the pi4j lib to make sound whenever a (physical) button is clicked. This program works, but it now plays all the sounds interchangeably. I want that when you click on 2,3,4 or more buttons you only hear one sound.
This is the code I hope you can help.
public class ButtonSoundsProject{
public static void main(String args[]) throws InterruptedException {
System.out.println("Toy has been started!");
// create gpio controller
final GpioController gpio = GpioFactory.getInstance();
// provision gpio pin #02 as an input pin with its internal pull down resistor enabled
GpioPinDigitalInput[] pins = {
gpio.provisionDigitalInputPin(RaspiPin.GPIO_00, PinPullResistance.PULL_DOWN),
gpio.provisionDigitalInputPin(RaspiPin.GPIO_01, PinPullResistance.PULL_DOWN),
gpio.provisionDigitalInputPin(RaspiPin.GPIO_02, PinPullResistance.PULL_DOWN),
gpio.provisionDigitalInputPin(RaspiPin.GPIO_03, PinPullResistance.PULL_DOWN),
gpio.provisionDigitalInputPin(RaspiPin.GPIO_04, PinPullResistance.PULL_DOWN),
gpio.provisionDigitalInputPin(RaspiPin.GPIO_05, PinPullResistance.PULL_DOWN),};
final ArrayList<String> soundList = new ArrayList<String>();
soundList.add("/home/pi/Sounds/Sound1.wav");
soundList.add("/home/pi/Sounds/Sound2.wav");
soundList.add("/home/pi/Sounds/Sound3.wav");
soundList.add("/home/pi/Sounds/Sound4.wav");
soundList.add("/home/pi/Sounds/Sound5.wav");
soundList.add("/home/pi/Sounds/Sound6.wav");
soundList.add("/home/pi/Sounds/Sound7.wav");
soundList.add("/home/pi/Sounds/Sound8.wav");
soundList.add("/home/pi/Sounds/Sound9.wav");
soundList.add("/home/pi/Sounds/Sound10.wav");
soundList.add("/home/pi/Sounds/Sound11.wav");
soundList.add("/home/pi/Sounds/Sound12.wav");
// create and register gpio pin listener
GpioPinListenerDigital listener = new GpioPinListenerDigital() {
#Override
public void handleGpioPinDigitalStateChangeEvent(GpioPinDigitalStateChangeEvent event) {
// display pin state on console
final int randomNum = 0 + (int) (Math.random() * 12);
System.out.println(randomNum);
System.out.println(" --> GPIO PIN STATE CHANGE: " + event.getPin() + " = " + event.getState());
InputStream in;
try {
System.out.println(soundList.get(randomNum).toString());
String filepath = soundList.get(randomNum).toString();
in = new FileInputStream(new File(filepath));
AudioStream as = new AudioStream(in);
AudioPlayer.player.start(as);
} catch (Exception ex) {
ex.printStackTrace();
}
}
};
gpio.addListener(listener, pins);
for (;;) {
Thread.sleep(500);
}
}
}
As stated in the comments, I can't give you advise regarding the AudioStream and AudioPlayer classes because I don't seem to have those in my JDK. Since my method is similar, I'll give you what I have, and you can hopefully take it from there.
Basically, the solution is to stop and/or "mute" that audio clip. This is how I accomplish it using the javax.sound package.:
private Clip currentAudioClip; // Keep a reference to the current clip being played
public void handleGpioPinDigitalStateChangeEvent(GpioPinDigitalStateChangeEvent event) {
// Call this every time regardless.
// If nothing is playing, this will do nothing.
stopAudio();
String filepath = soundList.get(randomNum)
URL soundFileUrl = new File(filePath).toURI().toURL();
AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(soundFileUrl);
Line.Info lineInfo = new Line.Info(Clip.class);
Line line = AudioSystem.getLine(lineInfo);
currentAudioClip = (Clip) line;
currentAudioClip.open(audioInputStream);
audioClip.start();
// Alternative if you want to loop continuously. Comment out the `.start` line to use this.
// audioClip.loop(Clip.LOOP_CONTINUOUSLY);
}
public void stopAudio(){
if(audioClip != null){
muteLine(); // A gotcha I discovered (see explanation below)
audioClip.stop();
// audioClip.loop(0); // if you chose to loop, use this instead of `.stop()`
audioClip.flush();
audioClip = null;
}
}
public void muteLine(){
BooleanControl muteControl = (BooleanControl) audioClip.getControl(BooleanControl.Type.MUTE);
if(muteControl != null){
muteControl.setValue(true); // True to mute the line, false to unmute
}
}
In short, every time a pin state change event is fired, the previous audio clip will be ceased, and a new one should play. You shouldn't get any sound overlapping with this.
Also note that this is a slight modification of my original code, so let me know if there are any issues
Note about the GOTCHA
I wrote a question over on the Raspberry PI Stackexchange about an odd problem I encountered. The problem was that I discovered my audio clip would not cease playing on command. It would continue playing for a seemingly arbitrary amount of time. The stranger thing is that I only observed this while testing the app on the raspberry; it worked perfectly fine on my local machine (and even on several other machines).
It is possible my issue is related to the "looping" of my clip; if that is the case, and you simply want the clip to play for its length and no further, you may not encounter that issue, and you can probably dispense with the "muting" code I included. However, if you do observe the same issue, at least you have a solution.
Hope this helps, let me know if you have any questions.

Android AudioRecord class - process live mic audio quickly, set up callback function

I want to record audio from the mic and access it for possible playback in near real-time. I am unsure of how to use the Android AudioRecord class to record some mic audio and quickly access it.
For the AudioRecord class, the official site says 'the app polls the AudioRecord object in time', and 'the size of the buffer being filled determines the time-length of the recording before over-running unread data'. Later it's suggested that a larger buffer should be used when polling less frequently. They never actually show an example in code.
One example I've seen in a book uses the AudioRecord class to continuously read a buffer freshly populated with live mic audio, and then the app writes this data to an SD file. The pseudo-code looks something like -
set up AudioRecord object with buffer size and recording format info
set up a file and an output stream
myAudioRecord.startRecording();
while(isRecording)
{
// myBuffer is being filled with fresh audio
read audio data into myBuffer
send contents of myBuffer to SD file
}
myAudioRecord.stop();
How this code synchronizes its reading with the rate of recording is unclear - is the boolean "isRecording" sequenced on and off properly elsewhere? It seems this code could either read too frequently or too infrequently, depending on how long the reading and writing takes.
The site doc also says the AudioRecord class has a nested class named OnRecordPositionUpdateListener which is defined as an interface. The information suggests that somehow, you specify the period you want for being notified of the progress of the recording, and the name of your event handler, and a call is automatically made to your event handler at the specified frequency. I think the structure, in pseudo-code would be something like -
set target of period update message = myListener
set period to be about every 250 ms
other code
myListener()
{
if(record button was recently tapped)
handle message that another 250 ms of fresh audio is available
ie, read it and send it somewhere
)
I need to find some specific code which allows me to capture and process mic audio with a delay of less than about 500 ms. Android offers another class called MediaRecorder, but it doesn't support streaming, and I may want to stream live mic audio over a Wi-Fi network in near real-time. Where can I find some specific examples?
After experimenting lots with the notifications and a bunch of other techniques I settled on this code:
private class AudioIn extends Thread {
private boolean stopped = false;
private AudioIn() {
start();
}
#Override
public void run() {
android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_URGENT_AUDIO);
AudioRecord recorder = null;
short[][] buffers = new short[256][160];
int ix = 0;
try { // ... initialise
int N = AudioRecord.getMinBufferSize(8000,AudioFormat.CHANNEL_IN_MONO,AudioFormat.ENCODING_PCM_16BIT);
recorder = new AudioRecord(AudioSource.MIC,
8000,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT,
N*10);
recorder.startRecording();
// ... loop
while(!stopped) {
short[] buffer = buffers[ix++ % buffers.length];
N = recorder.read(buffer,0,buffer.length);
//process is what you will do with the data...not defined here
process(buffer);
}
} catch(Throwable x) {
Log.w(TAG,"Error reading voice audio",x);
} finally {
close();
}
}
private void close() {
stopped = true;
}
}
So far it's working pretty robustly on the half a dozen Android phones I've tried it on.
I wonder if you could combine these answers in the following way...
Use setPositionNotificationPeriod(160) before the while loop. This should cause the callback to be called every time 160 frames are read. Instead of calling process(buffer) inside of the thread that's doing the read loop, call process(buffer) from the callback. Use a variable to keep track of the last read buffer so you process the right one. As it is now, you block on the read, then you're not reading while you're processing. I think it might be better to separate those two.
Here is the code you need to use the OnRecordPositionUpdateListener and Notification Period.
I noticed that in practice it does not send the notification consistently at the same exact time, I want, but it is close enough.
About detectAfterEvery:
The size of detectEvery needs to be large enough to hold just the amount of data you want. So for this example, we have a sample rate of 44100 Hz, that means we want 44100 samples per second. By setting the setPositionNotificationPeriod to be 44100, the code tells Android to callback after it has recorded 44100 samples, which is about every 1 second.
The complete code is here:
final int sampleRate = 44100;
int bufferSize =
AudioRecord.getMinBufferSize(sampleRate,
AudioFormat.CHANNEL_CONFIGURATION_MONO,
AudioFormat.ENCODING_PCM_16BIT);
//aim for 1 second
int detectAfterEvery = (int)((float)sampleRate * 1.0f);
if (detectAfterEvery > bufferSize)
{
Log.w(TAG, "Increasing buffer to hold enough samples " + detectAfterEvery + " was: " + bufferSize);
bufferSize = detectAfterEvery;
}
recorder =
new AudioRecord(AudioSource.MIC, sampleRate,
AudioFormat.CHANNEL_CONFIGURATION_MONO,
AudioFormat.ENCODING_PCM_16BIT, bufferSize);
recorder.setPositionNotificationPeriod(detectAfterEvery);
final short[] audioData = new short[bufferSize];
final int finalBufferSize = bufferSize;
OnRecordPositionUpdateListener positionUpdater = new OnRecordPositionUpdateListener()
{
#Override
public void onPeriodicNotification(AudioRecord recorder)
{
Date d = new Date();
//it should be every 1 second, but it is actually, "about every 1 second"
//like 1073, 919, 1001, 1185, 1204 milliseconds of time.
Log.d(TAG, "periodic notification " + d.toLocaleString() + " mili " + d.getTime());
recorder.read(audioData, 0, finalBufferSize);
//do something amazing with audio data
}
#Override
public void onMarkerReached(AudioRecord recorder)
{
Log.d(TAG, "marker reached");
}
};
recorder.setRecordPositionUpdateListener(positionUpdater);
Log.d(TAG, "start recording, bufferSize: " + bufferSize);
recorder.startRecording();
//remember to still have a read loop otherwise the listener won't trigger
while (continueRecording)
{
recorder.read(audioData, 0, bufferSize);
}
private int freq =8000;
private AudioRecord audioRecord = null;
private Thread Rthread = null;
private AudioManager audioManager=null;
private AudioTrack audioTrack=null;
byte[] buffer = new byte[freq];
//call this method at start button
protected void Start()
{
loopback();
}
protected void loopback() {
android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_URGENT_AUDIO);
final int bufferSize = AudioRecord.getMinBufferSize(freq,
AudioFormat.CHANNEL_CONFIGURATION_MONO,
AudioFormat.ENCODING_PCM_16BIT);
audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, freq,
AudioFormat.CHANNEL_CONFIGURATION_MONO,
MediaRecorder.AudioEncoder.AMR_NB, bufferSize);
audioTrack = new AudioTrack(AudioManager.ROUTE_HEADSET, freq,
AudioFormat.CHANNEL_CONFIGURATION_MONO,
MediaRecorder.AudioEncoder.AMR_NB, bufferSize,
AudioTrack.MODE_STREAM);
audioTrack.setPlaybackRate(freq);
final byte[] buffer = new byte[bufferSize];
audioRecord.startRecording();
Log.i(LOG_TAG, "Audio Recording started");
audioTrack.play();
Log.i(LOG_TAG, "Audio Playing started");
Rthread = new Thread(new Runnable() {
public void run() {
while (true) {
try {
audioRecord.read(buffer, 0, bufferSize);
audioTrack.write(buffer, 0, buffer.length);
} catch (Throwable t) {
Log.e("Error", "Read write failed");
t.printStackTrace();
}
}
}
});
Rthread.start();
}
It plays the recorded audio less than 100 ms delay.

Categories