I have struggled with playing aac encoded audio files with Java a while now.
We had a group project at the end of our first semester and wanted to have a background music and few soundeffects in there.
At the end we used WAV files, as we couldn't get the AAC's to play.
This weekend I gave it another try and searched along again, and have got a working code searched together from different sites, but nowhere was a complete working solution.
For a more comfortable usage in future projects I made me a small library for aac playback.
As it was hard to find the working solution, I wanted to share it with you, in the hope some one having the same problem in the future will have it a bit easier.
The following code is a snippet from the lib I wrote. You can have a look onto the whole lib on GitLab at
Java AAC-Player.
This lib/solution uses the aac decoder JAAD.
The player I wrote around the JAAD library is available as Maven artifact:
<dependency>
<groupId>com.gitlab.9lukas5</groupId>
<artifactId>jaad</artifactId>
<!--
check latest version on https://search.maven.org
or try the latest tag you find in git
-->
</dependency>
public static void play(File[] files)
{
// local vars
byte[] b; // array for the actual audio Data during the playback
AudioTrack track; // track we are playing atm
AudioFormat af; // the track's format
SourceDataLine line; // the line we'll use the get our audio to the speaker's
Decoder dec; // decoder to get the audio bytes
Frame frame; //
SampleBuffer buf; //
int currentTrack; // index of current track from playlist
MP4Container cont; // container to open the current track with
Movie movie; // and get the content from the container
try
{
// for-next loop to play each titel from the playlist once
for (currentTrack = 0; currentTrack < files.length; currentTrack++)
{
cont = new MP4Container(new RandomAccessFile(files[currentTrack], "r")); // open titel with random access
movie = cont.getMovie(); // get content from container,
List<Track> content = movie.getTracks();
if (content.isEmpty()) // check if container HAS content
throw new Exception ("insert error message here"); // if so,
track = (AudioTrack) movie.getTracks().get(0); // grab first track and set the audioformat
af = new AudioFormat(track.getSampleRate(), track.getSampleSize(), track.getChannelCount(), true, true);
line = AudioSystem.getSourceDataLine(af); // get a DataLine from the AudioSystem
line.open(); // open and
line.start(); // start it
dec = new Decoder(track.getDecoderSpecificInfo());
buf = new SampleBuffer();
while(track.hasMoreFrames()) // while we have frames left
{
frame = track.readNextFrame(); // read next frame,
dec.decodeFrame(frame.getData(), buf); // decode it and put into the buffer
b = buf.getData(); // write the frame data from the buffer to our byte-array
line.write(b, 0, b.length); // and from there write the byte array into our open AudioSystem DataLine
while (paused) // check if we should pause
{
Thread.sleep(500); // if yes, stay half a second
if (Thread.interrupted()) // check if we should stop possibly
{
line.close(); // if yes, close line and
return; // exit thread
}
}
if (Thread.interrupted()) // if not in pause, still check on each frame if we should
{ // stop. If so
line.close(); // close line and
return; // exit thread
}
}
line.close(); // after titel is over, close line
if (loop) // if we should loop current titel, set currentTrack -1,
currentTrack--; // as on bottom of for-next it get's +1 and so the same titel get's played again
else if (repeat && (currentTrack == files.length -1)) // else check if we are at the end of the playlist
currentTrack = -1; // and should repeat the whole list. If so, set currentTrack -1, so it get's 0 on for-next bottom
}
}
catch (LineUnavailableException | IOException | InterruptedException e)
{
e.printStackTrace();
}
}
Related
I am currently trying to write a jukebox-like application in Java that is able to play any audio source possible, but encountered some difficulties when trying to play radio streams.
For playback I use JLayer from JavaZoom, that works fine as long as the target is a direct media file or a direct media stream (I can play PCM, MP3 and OGG just fine). However I encounter difficulties when trying to play radio streams which either contain pre-media data like a m3u/pls file (which I could fix by adding a detection beforehand), or data that is streamed on port 80 while a web-page exists at the same location and the media transmitted depends on the type of request. In the later case, whenever I try to stream the media, I instead get the HTML data.
Example link of a stream that is hidden behind a web-page: http://stream.t-n-media.de:8030
This is playable in VLC, but if you put it into a browser or my application you'll receive an HTML file.
Is there:
A ready-made, free solution that I could use in place of JLayer? Preferably open source so I can study it?
A tutorial that can help me to write a solution on my own?
Or can someone give me an example on how to properly detect/request a media stream?
Thanks in advance!
import java.io.*;
import java.net.*;
import javax.sound.sampled.*;
import javax.sound.midi.*;
/**
* This class plays sounds streaming from a URL: it does not have to preload
* the entire sound into memory before playing it. It is a command-line
* application with no gui. It includes code to convert ULAW and ALAW
* audio formats to PCM so they can be played. Use the -m command-line option
* before MIDI files.
*/
public class PlaySoundStream {
// Create a URL from the command-line argument and pass it to the
// right static method depending on the presence of the -m (MIDI) option.
public static void main(String[ ] args) throws Exception {
if (args[0].equals("-m")) streamMidiSequence(new URL(args[1]));
else streamSampledAudio(new URL(args[0]));
// Exit explicitly.
// This is needed because the audio system starts background threads.
System.exit(0);
}
/** Read sampled audio data from the specified URL and play it */
public static void streamSampledAudio(URL url)
throws IOException, UnsupportedAudioFileException,
LineUnavailableException
{
AudioInputStream ain = null; // We read audio data from here
SourceDataLine line = null; // And write it here.
try {
// Get an audio input stream from the URL
ain=AudioSystem.getAudioInputStream(url);
// Get information about the format of the stream
AudioFormat format = ain.getFormat( );
DataLine.Info info=new DataLine.Info(SourceDataLine.class,format);
// If the format is not supported directly (i.e. if it is not PCM
// encoded), then try to transcode it to PCM.
if (!AudioSystem.isLineSupported(info)) {
// This is the PCM format we want to transcode to.
// The parameters here are audio format details that you
// shouldn't need to understand for casual use.
AudioFormat pcm =
new AudioFormat(format.getSampleRate( ), 16,
format.getChannels( ), true, false);
// Get a wrapper stream around the input stream that does the
// transcoding for us.
ain = AudioSystem.getAudioInputStream(pcm, ain);
// Update the format and info variables for the transcoded data
format = ain.getFormat( );
info = new DataLine.Info(SourceDataLine.class, format);
}
// Open the line through which we'll play the streaming audio.
line = (SourceDataLine) AudioSystem.getLine(info);
line.open(format);
// Allocate a buffer for reading from the input stream and writing
// to the line. Make it large enough to hold 4k audio frames.
// Note that the SourceDataLine also has its own internal buffer.
int framesize = format.getFrameSize( );
byte[ ] buffer = new byte[4 * 1024 * framesize]; // the buffer
int numbytes = 0; // how many bytes
// We haven't started the line yet.
boolean started = false;
for(;;) { // We'll exit the loop when we reach the end of stream
// First, read some bytes from the input stream.
int bytesread=ain.read(buffer,numbytes,buffer.length-numbytes);
// If there were no more bytes to read, we're done.
if (bytesread == -1) break;
numbytes += bytesread;
// Now that we've got some audio data to write to the line,
// start the line, so it will play that data as we write it.
if (!started) {
line.start( );
started = true;
}
// We must write bytes to the line in an integer multiple of
// the framesize. So figure out how many bytes we'll write.
int bytestowrite = (numbytes/framesize)*framesize;
// Now write the bytes. The line will buffer them and play
// them. This call will block until all bytes are written.
line.write(buffer, 0, bytestowrite);
// If we didn't have an integer multiple of the frame size,
// then copy the remaining bytes to the start of the buffer.
int remaining = numbytes - bytestowrite;
if (remaining > 0)
System.arraycopy(buffer,bytestowrite,buffer,0,remaining);
numbytes = remaining;
}
// Now block until all buffered sound finishes playing.
line.drain( );
}
finally { // Always relinquish the resources we use
if (line != null) line.close( );
if (ain != null) ain.close( );
}
}
// A MIDI protocol constant that isn't defined by javax.sound.midi
public static final int END_OF_TRACK = 47;
/* MIDI or RMF data from the specified URL and play it */
public static void streamMidiSequence(URL url)
throws IOException, InvalidMidiDataException, MidiUnavailableException
{
Sequencer sequencer=null; // Converts a Sequence to MIDI events
Synthesizer synthesizer=null; // Plays notes in response to MIDI events
try {
// Create, open, and connect a Sequencer and Synthesizer
// They are closed in the finally block at the end of this method.
sequencer = MidiSystem.getSequencer( );
sequencer.open( );
synthesizer = MidiSystem.getSynthesizer( );
synthesizer.open( );
sequencer.getTransmitter( ).setReceiver(synthesizer.getReceiver( ));
// Specify the InputStream to stream the sequence from
sequencer.setSequence(url.openStream( ));
// This is an arbitrary object used with wait and notify to
// prevent the method from returning before the music finishes
final Object lock = new Object( );
// Register a listener to make the method exit when the stream is
// done. See Object.wait( ) and Object.notify( )
sequencer.addMetaEventListener(new MetaEventListener( ) {
public void meta(MetaMessage e) {
if (e.getType( ) == END_OF_TRACK) {
synchronized(lock) {
lock.notify( );
}
}
}
});
// Start playing the music
sequencer.start( );
// Now block until the listener above notifies us that we're done.
synchronized(lock) {
while(sequencer.isRunning( )) {
try { lock.wait( ); } catch(InterruptedException e) { }
}
}
}
finally {
// Always relinquish the sequencer, so others can use it.
if (sequencer != null) sequencer.close( );
if (synthesizer != null) synthesizer.close( );
}
}
}
I have used this piece of code in one of my projects that deal with Audio streaming and was working just fine.
Furthermore, you can see similar examples here:
Java Audio Example
Just reading the javadoc of AudioSystem give me an idea.
There is an other signature for getAudioInputStream: you can give it an InputStream instead of a URL.
So, try to manage to get the input stream by yourself and add the needed headers so that you get the stream instead the html content:
URLConnection uc = url.openConnection();
uc.setRequestProperty("<header name here>", "<header value here>");
InputStream in = uc.getInputStream();
ain=AudioSystem.getAudioInputStream(in);
Hope this help.
I know this answer comes late, but I had the same issue: I wanted to play MP3 and AAC audio and also wanted the user to insert PLS/M3U links. Here is what I did:
First I tried to parse the type by using the simple file name:
import de.webradio.enumerations.FileExtension;
import java.net.URL;
public class FileExtensionParser {
/**
*Parses a file extension
* #param filenameUrl the url
* #return the filename. if filename cannot be determined by file extension, Apache Tika parses by live detection
*/
public FileExtension parseFileExtension(URL filenameUrl) {
String filename = filenameUrl.toString();
if (filename.endsWith(".mp3")) {
return FileExtension.MP3;
} else if (filename.endsWith(".m3u") || filename.endsWith(".m3u8")) {
return FileExtension.M3U;
} else if (filename.endsWith(".aac")) {
return FileExtension.AAC;
} else if(filename.endsWith((".pls"))) {
return FileExtension.PLS;
}
URLTypeParser parser = new URLTypeParser();
return parser.parseByContentDetection(filenameUrl);
}
}
If that fails, I use Apache Tika to do a kind of live detection:
public class URLTypeParser {
/** This class uses Apache Tika to parse an URL using her content
*
* #param url the webstream url
* #return the detected file encoding: MP3, AAC or unsupported
*/
public FileExtension parseByContentDetection(URL url) {
try {
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
InputStream in = connection.getInputStream();
BodyContentHandler handler = new BodyContentHandler();
AudioParser parser = new AudioParser();
Metadata metadata = new Metadata();
parser.parse(in, handler, metadata);
return parseMediaType(metadata);
} catch (IOException e) {
e.printStackTrace();
} catch (TikaException e) {
e.printStackTrace();
} catch (SAXException e) {
e.printStackTrace();
}
return FileExtension.UNSUPPORTED_TYPE;
}
private FileExtension parseMediaType(Metadata metadata) {
String parsedMediaType = metadata.get("encoding");
if (parsedMediaType.equalsIgnoreCase("aac")) {
return FileExtension.AAC;
} else if (parsedMediaType.equalsIgnoreCase("mpeg1l3")) {
return FileExtension.MP3;
}
return FileExtension.UNSUPPORTED_TYPE;
}
}
This will also solve the HTML problem, since the method will return FileExtension.UNSUPPORTED for HTML content.
I combined this classes together with a factory pattern and it works fine. The live detection takes only about two seconds.
I don't think that this will help you anymore but since I struggled almost three weeks I wanted to provide a working answer. You can see the whole project at github: https://github.com/Seppl2202/webradio
I'm having some trouble with reading and playing certain audio clips on Android 2.0.1 (Motorola Droid A855). Below is the code segment that I use. It works fine for some files, but for other files it just doesn't exit the while loop. I have tried checking
InputStream.available()
method but with no luck. I even printed out the number of bytes it reads properly before getting stuck. It seems that it gets stuck in the loop at the last round of read (have less than < 512 bytes left), and doesn't exit the loop.
int sampleFreq = 44100;
int minBufferSize = AudioTrack.getMinBufferSize(sampleFreq, AudioFormat.CHANNEL_IN_STEREO, AudioFormat.ENCODING_PCM_16BIT);
int bufferSize = 512;
AudioTrack at = new AudioTrack(AudioManager.STREAM_MUSIC, sampleFreq, AudioFormat.CHANNEL_IN_STEREO, AudioFormat.ENCODING_PCM_16BIT, minBufferSize, AudioTrack.MODE_STREAM);
InputStream input;
try {
File fileID=new File(Environment.getExternalStorageDirectory(),resourceID);
input = new FileInputStream( fileID);
int filesize=(int)fileID.length();
int i=0,byteread=0;
byte[] s = new byte[bufferSize];
at.play();
while((i = input.read(s, 0, bufferSize))>-1){
at.write(s, 0, i);
//at.flush();
byteread+=i;
Log.i(TAG,"playing audio "+byteread+"\t"+filesize);
}
at.stop();
at.release();
input.close();
} catch (FileNotFoundException e) {
// TODO
e.printStackTrace();
} catch (IOException e) {
// TODO
e.printStackTrace();
}
Audio files are around 1-2MB in size and are in wav format. Following is an example of the logging-
> : playing audio 1057280 1058474
> : playing audio 1057792 1058474
> : playing audio 1058304 1058474
Any idea why this is happening as it runs perfectly for some of the audio files.
Make sure your call to write() always delivers a byte size which is an integral number of samples.
For your 16 bit stereo mode, that should be an integral multiple of 4 bytes.
Additionally, at least before the final write, for stutter-free operation you should really respect the minimum buffer size of the audio subsystem and deliver at least that much data in each call to the audio write method.
If your source data is a .wav file, make sure you actually skip the header and read samples only starting from a valid payload chunk.
I have created a media player in Java using JLayer, and it accepts mp3 files. I also have the lyrics to a specific song appear once the user plays that song, but now I want to somehow highlight or change the text color of the lyrics as they are heard in the song (like karaoke). I only need to do this for one song - and I'll have the lyrics already implemented in my program. I have already searched for how to do this but can't seem to find exactly what I'm looking for. Below I added the code to my class that plays the music file.
public class PlayMusic {
/**
* Global variables. FileInputStream obtains input bytes from a file system
* and reads streas of raw bytes. BufferedInputStream adds functionality
* to the fis, and creates an internal buffer array.
*/
private String filename;
private Player player;
private boolean canResume;
private boolean valid;
private int total;
private int stopped;
FileInputStream fis;
BufferedInputStream bis;
/**
* Constructor the takes in the path of the mp3 file to be played.
* #param filename - path of the mp3 file
*/
public PlayMusic(String filename) {
this.filename = filename;
this.canResume = false;
this.valid = false;
this.total = 0;
this.stopped = 0;
this.fis = null;
this.bis = null;
}
/**
* Function called to stop a song altogether as opposed to pausing.
*/
public void close() {
if (player != null)
player.close();
stopped = 0;
fis = null;
bis = null;
player = null;
canResume = false;
}
/**
* Function called to pause a song. Fis.available() is a method that returns
* the number of remaining bytes that can be read from the input stream.
*/
public void pause(){
try {
if (fis!=null)
stopped = fis.available();
if (player!= null)
player.close();
fis = null;
bis = null;
player = null;
if(valid)
canResume = true;
} catch (IOException e) {
}
}
/**
* Function called when we want to resume a song from where it left off
* after being paused.
*/
public void resume()
{
if(!canResume)
return;
if(play(total-stopped))
canResume = false;
}
/**
* Function called to play the song and keep track of where in the song the
* user presses stop in order for the resume button to work properly. Fis.skip
* skips over and discards pos bytes of data from fis.
* #param pos - The position of the song in which we want to resume play
* #return
*/
public boolean play(int pos) {
valid = true;
canResume = false;
try {
fis = new FileInputStream(filename);
total = fis.available();
if(pos> -1)
fis.skip(pos);
bis = new BufferedInputStream(fis);
player = new Player(bis);
}
catch (Exception e) {
System.out.println("Problem playing file " + filename);
System.out.println(e);
}
/**
* Run the play button in a new thread so the music plays in the background.
*/
new Thread() {
public void run() {
try { player.play(); }
catch (Exception e) { System.out.println(e); valid = false; }
}
}.start();
return valid;
}
}
You won't be able to detect what words are being sung in the song (with ease at least), but if you have the lyrics of the song in a file, and you have the sound file of the song, then to me it sounds like you could just add more info to that lyrics file to create a map of when the words of the lyrics are being sung in the song.
For example, if I was to do this with the song Jingle Bells, I may have a tab separated file that contains the lyrics, where one line is one word, with a begin and end time relative to the start of the song in milliseconds.
jingle 0 1000
bells 1001 1500
jingle 1501 2500
bells 2501 3000
... and so on
Edit for explaining how to code up keeping track of how long a song has been playing.
Two methods to create an instance variable called say, totalTimeSongHasBeenPlaying
I'm not sure how you have abstracted out playing your sound files, but say that you abstracted that out to a Sound class, then you could have three methods, sound.soundStarted, sound.soundStopped, sound.soundRestarted, then at the start of playing the sound you can call soundStarted which could grab a System.nanoTime or a System.currentTimeMillis and on soundStopped, you could grab it again and take the difference and add it to totalTimeSongHasBeenPlaying, and on soundRestart you could set totalTimeSongHasBeenPlaying to zero.
Do some math against frame position the sound you are currently playing is versus how many frames is in a second for that file. I don't know the exact libraries for JLayer, it's been a while since I used it, but that method should also tell you how far along in the file you are.
After that, the Sound class could also then have a method such as currentWordBeingSung(), which looks at totalTimeSongHasBeenPlaying, and uses the lookup table you created off the lyrics file during construction and returns the specific word uniquely (may be duplicates). Your gui when you create it, say your JLyricsViewer, can hold an instance to your Sound object and you can use a SwingTimer to repaint it every 50 ms or so, where in your paintComponent method it looks at currentWordBeingSung().
TargetDataLine is, for me so far, the easiest way to capture microphone input in Java. I want to encode the audio that I capture with a video of the screen [in a screen recorder software] so that the user can create a tutorial, slide case etc.
I use Xuggler to encode the video.
They do have a tutorial on encoding audio with video but they take their audio from a file. In my case, the audio is live.
To encode the video I use com.xuggle.mediaTool.IMediaWriter. The IMediaWriter object allows me to add a video stream and has an
encodeAudio(int streamIndex, short[] samples, long timeStamp, TimeUnit timeUnit)
I can use that if I can get the samples from target data line as short[]. It returns byte[]
So two questions are:
How can I encode the live audio with video?
How do I maintain the proper timing of the audio packets so that they are encoded at the proper time?
References:
1. DavaDoc for TargetDataLine: http://docs.oracle.com/javase/1.4.2/docs/api/javax/sound/sampled/TargetDataLine.html
2. Xuggler Documentation: http://build.xuggle.com/view/Stable/job/xuggler_jdk5_stable/javadoc/java/api/index.html
Update
My code for capturing video
public void run(){
final IRational FRAME_RATE = IRational.make(frameRate, 1);
final IMediaWriter writer = ToolFactory.makeWriter(completeFileName);
writer.addVideoStream(0, 0,FRAME_RATE, recordingArea.width, recordingArea.height);
long startTime = System.nanoTime();
while(keepCapturing==true){
image = bot.createScreenCapture(recordingArea);
PointerInfo pointerInfo = MouseInfo.getPointerInfo();
Point globalPosition = pointerInfo.getLocation();
int relativeX = globalPosition.x - recordingArea.x;
int relativeY = globalPosition.y - recordingArea.y;
BufferedImage bgr = convertToType(image,BufferedImage.TYPE_3BYTE_BGR);
if(cursor!=null){
bgr.getGraphics().drawImage(((ImageIcon)cursor).getImage(), relativeX,relativeY,null);
}
try{
writer.encodeVideo(0,bgr,System.nanoTime()-startTime,TimeUnit.NANOSECONDS);
}catch(Exception e){
writer.close();
JOptionPane.showMessageDialog(null,
"Recording will stop abruptly because" +
"an error has occured", "Error",JOptionPane.ERROR_MESSAGE,null);
}
try{
sleep(sleepTime);
}catch(InterruptedException e){
e.printStackTrace();
}
}
writer.close();
}
I answered most of that recently under this question: Xuggler encoding and muxing
Code sample:
writer.addVideoStream(videoStreamIndex, 0, videoCodec, width, height);
writer.addAudioStream(audioStreamIndex, 0, audioCodec, channelCount, sampleRate);
while (... have more data ...)
{
BufferedImage videoFrame = ...;
long videoFrameTime = ...; // this is the time to display this frame
writer.encodeVideo(videoStreamIndex, videoFrame, videoFrameTime, DEFAULT_TIME_UNIT);
short[] audioSamples = ...; // the size of this array should be number of samples * channelCount
long audioSamplesTime = ...; // this is the time to play back this bit of audio
writer.encodeAudio(audioStreamIndex, audioSamples, audioSamplesTime, DEFAULT_TIME_UNIT);
}
In the case of TargetDataLine, getMicrosecondPosition() will tell you the time you need for audioSamplesTime. This appears to start from the time the TargetDataLine was opened. You need to figure out how to get a video timestamp referenced to the same clock, which depends on the video device and/or how you capture video. The absolute values do not matter as long as they are both using the same clock. You could subtract the initial value (at start of stream) from both your video and your audio times so that the timestamps match, but that is only a somewhat approximate match (probably close enough in practice).
You need to call encodeVideo and encodeAudio in strictly increasing order of time; you may have to buffer some audio and some video to make sure you can do that. More details here.
This is a code that will attempt to record an audio sample : but i've a not constructed AudioFormat object ( that has been passed to DataLine.Info) because i don't know the sample rate.
EDIT
I have seen that just randomly placing sample rate of 8000 works . But is it fine ? Can i keep any value of sample rate ?
boolean lineIsStopped = false;
TargetDataLine line = null;
AudioFormat af; // object not constructed through out
DataLine.Info info = new DataLine.Info(TargetDataLine.class, af); // af not initialized
try {
line = (TargetDataLine)AudioSystem.getLine(info);
line.open( af );
} catch( LineUnavailableException ex ) {
// handle the error
}
// now we are ready for an input
// call start to start accepting data from mic
byte data[] = new byte[ line.getBufferSize() / 5 ];
line.start(); // this statement starts delivering data into the line buffer
// start retreiving data from the line buffer
int numBytesRead;
int offset = 0;
ByteArrayOutputStream out = new ByteArrayOutputStream();
while( ! lineIsStopped ) { // when the line is not stopped i.e is active
numBytesRead = line.read( data , offset , data.length );
// now save the data
try {
out.write(data); // writes data to this output stream !
} catch( Exception exc) {
System.out.println(exc);
}
}
In this how can i construct audio format object without getting any audio sample ?
After reading your comments, you are recording from the mic. In which case you want to set the audio format according to the quality you want from the mic. If you want telephone quality 8k hz would be fine. If you want tape quality 22khz, and if you want CD quality audio 44.1khz. Of course, if you transmitting that over the network then 8khz is probably going to be good enough.
It's always a good idea to have this be a setting if your application so the user can control what quality they want.