samer@0: /*
samer@0: * FileSource.java
samer@0: *
samer@0: * Copyright (c) 2000, Samer Abdallah, King's College London.
samer@0: * All rights reserved.
samer@0: *
samer@0: * This software is provided AS iS and WITHOUT ANY WARRANTY;
samer@0: * without even the implied warranty of MERCHANTABILITY or
samer@0: * FITNESS FOR A PARTICULAR PURPOSE.
samer@0: */
samer@0:
samer@0: package samer.audio;
samer@0:
samer@0: import samer.core.*;
samer@0: import samer.core.types.*;
samer@0: import samer.core.util.*;
samer@0: import samer.tools.*;
samer@0: import javax.sound.sampled.*;
samer@0: import javax.swing.*;
samer@0: import java.io.*;
samer@0: import java.util.*;
samer@1: import java.nio.ByteBuffer;
samer@0:
samer@0: /**
samer@0: An AudioSource that read from multiple audio files. Can read any
samer@0: format for which the appropriate JavaSound plug-in is installed on
samer@0: your system, eg WAV, AU, MP3, OGG. The implementation of
samer@0: AudioSource.reader() returns a Task which gets samples from the audio
samer@0: files, going through the play list one by one. If the loop flag
samer@0: is true, then samples are returned indefinitely by looping through the
samer@0: playlist; otherwise, an EOFException is thrown if an attempt is made
samer@0: to read beyond the end of the last file.
samer@0:
samer@0: FileSource is a Viewable called "playlist", and an Agent with commands
samer@0: next, prev, reopen, view, select, loop.
samer@0:
samer@0: Properties read from current environment:
samer@0:
samer@0: - current
- Current file (String)
samer@0:
- loop
- Loop playlist? (Boolean) [true]
samer@0:
samer@0:
samer@0: */
samer@0:
samer@0: public class FileSource extends Viewable implements AudioSource, Agent
samer@0: {
samer@0: List list=null;
samer@0: ListIterator it=null;
samer@1: boolean loop, buffered=true;
samer@0: int channelsToMix;
samer@0: VFile curFile;
samer@0: InputStream in=null;
samer@1: AudioFormat format=null, inFormat=null;
samer@0: byte[] byte_buf=null;
samer@1: int chunk=0, bytesPerSample=0;
samer@0:
samer@0: /**
samer@0: * Construct a FileSource initialised with current file and loop initialised
samer@0: * from the current Environment. No exception is thrown if the current file
samer@0: * cannot be opened, however, you must be sure to set it to a valid file
samer@0: * (or to set a playlist) before trying to read any samples.
samer@0: */
samer@0: public FileSource()
samer@0: {
samer@0: super("playlist");
samer@0:
samer@0: Shell.push(node);
samer@0: curFile=new VFile("current","",0);
samer@0: loop=Shell.getBoolean("loop",true);
samer@0: Shell.pop();
samer@0:
samer@0: setAgent(this);
samer@0: Shell.registerViewable(this);
samer@0: }
samer@0:
samer@1: // AudioSource interface methods
samer@0: public void dispose() {
samer@0: close();
samer@0: Shell.deregisterViewable(this);
samer@0: curFile.dispose();
samer@0: }
samer@0:
samer@1: public int getChannels() { return format.getChannels(); }
samer@1: public float getRate() { return format.getFrameRate(); }
samer@1:
samer@0: /** Returns current file */
samer@0: public File getFile() { return curFile.getFile(); }
samer@1: public void setBuffering(boolean b) { buffered=b; }
samer@1:
samer@1: /** The actual format of the stream in. May not be same as target format. **/
samer@1: public AudioFormat getStreamFormat() { return inFormat; }
samer@1:
samer@1: /** The requested audio format. **/
samer@1: public AudioFormat getTargetFormat() { return format; }
samer@1: public void setTargetFormat(AudioFormat f) { format=f; }
samer@0:
samer@0: /** If true then loop playlist, otherwise, an Exception will be thrown
samer@0: * when the end of the playlist is reached. If there is no playlist, then
samer@0: * looping applies to the current file only. */
samer@0: public void setLoop(boolean f) { loop=f; }
samer@0:
samer@0: /** Set playlist to all WAV files in given directory */
samer@0: public void setDirectory(File dir, String ext) {
samer@0: setPlaylist(Arrays.asList(dir.listFiles(getFileFilter(ext))));
samer@0: }
samer@0:
samer@0: public synchronized List getPlaylist() { return list; }
samer@0: public synchronized void setPlaylist(List l) { list=l; it=list.listIterator(); }
samer@0: private synchronized void setFile(File f) { curFile.setFile(f); }
samer@0:
samer@0: /** Go back to start of playlist. Next block of samples will be from head
samer@0: * of first file in playlist. Will throw an exception if there is no playlist */
samer@0: public synchronized void rewind() throws Exception {
samer@0: it=list.listIterator(); next();
samer@0: }
samer@0:
samer@0: /** Move to head of next file in playlist */
samer@0: public synchronized void next() throws Exception
samer@0: {
samer@0: boolean wasOpen=isOpen();
samer@0:
samer@0: if (wasOpen) close();
samer@0: if (!it.hasNext()) {
samer@0: if (!loop) throw new EOFException();
samer@0: it=list.listIterator();
samer@0: if (!it.hasNext()) throw new Exception("no files in playlist");
samer@0: }
samer@0: setFile(it.next());
samer@0: if (wasOpen) openCurrent();
samer@0: }
samer@0:
samer@0: /** Move to head of previous file in playlist */
samer@0: public synchronized void prev() throws Exception
samer@0: {
samer@0: boolean wasOpen=isOpen();
samer@0: if (wasOpen) close();
samer@0: if (!it.hasPrevious()) {
samer@0: if (!loop) throw new EOFException();
samer@0: it=list.listIterator(list.size());
samer@0: if (!it.hasPrevious()) throw new Exception("no files in playlist");
samer@0: }
samer@0: setFile(it.previous());
samer@0: if (wasOpen) openCurrent();
samer@0: }
samer@0:
samer@0: public boolean isOpen() { return in!=null; }
samer@0:
samer@0: /** Closes current input stream */
samer@0: public synchronized void close() {
samer@0: try {
samer@0: if (in!=null) {
samer@0: Shell.trace("Closing audio stream...");
samer@0: in.close(); in=null; byte_buf=null;
samer@0: }
samer@0: }
samer@0: catch (IOException ex) {}
samer@0: }
samer@0:
samer@0: /** Opens the playlist starting with the first file. */
samer@0: public synchronized void open() throws Exception { rewind(); if (!isOpen()) openCurrent(); }
samer@0:
samer@0: /** Opens the current file. Next read will returns samples
samer@0: * from head of given file. If setFormat() was called previously, then
samer@0: * we will attempt to do format conversion. */
samer@0: private synchronized void openCurrent() throws Exception
samer@0: {
samer@0: File file=curFile.getFile();
samer@1: Shell.trace("\nFileSource:Opening audio file "+file);
samer@0:
samer@0: AudioInputStream s=AudioSystem.getAudioInputStream(file);
samer@1: AudioFormat fmt1, af=s.getFormat();
samer@0:
samer@0: Shell.trace(" format: "+af);
samer@0:
samer@0: // convert to target format if required
samer@0: if (format!=null) {
samer@0: if (!format.equals(af)) {
samer@0: Shell.trace(" converting to "+format);
samer@0: if (af.getChannels()>format.getChannels()) {
samer@1: int frameSize = af.getChannels()*format.getSampleSizeInBits()/8;
samer@0: Shell.trace(" channels mix down required.");
samer@1: fmt1 = new AudioFormat( format.getEncoding(), format.getSampleRate(),
samer@0: format.getSampleSizeInBits(),
samer@1: af.getChannels(), frameSize, format.getFrameRate(), format.isBigEndian());
samer@0:
samer@0: channelsToMix = af.getChannels();
samer@0: } else {
samer@0: channelsToMix = 0;
samer@1: fmt1=format;
samer@0: }
samer@1: Shell.trace(" converting via "+fmt1);
samer@1: s=convertFormat(s,af,fmt1);
samer@1: inFormat = fmt1;
samer@1: } else {
samer@1: Shell.trace(" no formation conversion required");
samer@1: channelsToMix = 0;
samer@1: inFormat = af;
samer@0: }
samer@1: } else {
samer@1: Shell.trace(" using stream native format");
samer@1: channelsToMix = 0;
samer@1: inFormat = af;
samer@0: }
samer@1: in=s;
samer@1: if (buffered) in = new BufferedInputStream(in,64*1024);
samer@0:
samer@0: // If we have a reader task, then update the byte buffer
samer@0: if (chunk>0) setByteBuffer();
samer@0: }
samer@0:
samer@0: /** Returns number of bytes available in current file */
samer@0: public int available() throws Exception { return in.available(); }
samer@0:
samer@0: /** Reopen current file, so that next read will be from head of file. */
samer@0: public synchronized void reopen() throws Exception { close(); openCurrent(); }
samer@0:
samer@1: private void setBlockSize(int s) { chunk=s; }
samer@0: private void setByteBuffer() {
samer@1: bytesPerSample = 2*(channelsToMix>0 ? channelsToMix : 1);
samer@1: byte_buf = new byte[chunk*bytesPerSample];
samer@1: }
samer@1:
samer@1: public int readInto(ByteBuffer buf, int offset, int len) throws IOException {
samer@1: return in.read(buf.array(), offset, len);
samer@0: }
samer@0:
samer@0: /** Returns a Task which copies samples as doubles into the given
samer@0: * buffer between the given positions. */
samer@0: public Task reader(final double [] dbuf, final int off, final int len) {
samer@1: setBlockSize(len);
samer@0: if (in!=null) setByteBuffer();
samer@0:
samer@0: return new AnonymousTask() {
samer@0: public void run() throws Exception {
samer@1: synchronized (FileSource.this) {
samer@1: // loop until len samples copied into dbuf
samer@1: int rem=len, pos=off;
samer@1: while (rem>0) {
samer@1: int bytesRead = in.read(byte_buf, 0, rem*bytesPerSample);
samer@1: if (bytesRead > 0) { // append this chunk to output
samer@1: int count=bytesRead/bytesPerSample;
samer@1: if (channelsToMix>0) {
samer@1: Util.shortToDoubleMixDown(byte_buf,dbuf,pos,count,channelsToMix);
samer@1: } else {
samer@1: Util.shortToDouble(byte_buf,dbuf,pos,count);
samer@1: }
samer@1: pos+=count; rem-=count;
samer@1: } else if (it!=null) next(); // next file if there is one
samer@1: else if (!loop) throw new EOFException(); // not looping and no more files
samer@1: else reopen(); // back to first file
samer@0:
samer@1: // if (rem>0) Shell.trace("Read "+bytesRead+" bytes, need "+rem+" more samples.");
samer@0: }
samer@0: }
samer@0: }
samer@0: };
samer@0: }
samer@0:
samer@0: /** Returns a Task which copies samples as floats into the given
samer@0: * buffer between the given positions. */
samer@0: public Task reader(final float [] dbuf, final int off, final int len) {
samer@1: setBlockSize(len);
samer@0: if (in!=null) setByteBuffer();
samer@0:
samer@0: return new AnonymousTask() {
samer@0: public synchronized void run() throws Exception {
samer@0: synchronized (FileSource.this) {
samer@1: // loop until len samples copied into dbuf
samer@1: int rem=len, pos=off;
samer@1: while (rem>0) {
samer@1: int bytesRead = in.read(byte_buf, 0, rem*bytesPerSample);
samer@1: if (bytesRead > 0) {
samer@1: int count=bytesRead/bytesPerSample;
samer@1: if (channelsToMix>0) {
samer@1: Util.shortToFloatMixDown(byte_buf,dbuf,pos,count,channelsToMix);
samer@1: } else {
samer@1: Util.shortToFloat(byte_buf,dbuf,pos,count);
samer@1: }
samer@1: pos+=count; rem-=count;
samer@1: } else if (it!=null) next();
samer@0: else if (!loop) throw new EOFException();
samer@0: else reopen();
samer@0: }
samer@0: }
samer@0: }
samer@0: };
samer@0: }
samer@0:
samer@0: // Agent
samer@0: public void getCommands(Agent.Registry r) {
samer@0: r.add("prev").add("reopen").add("next").add("view")
samer@0: .add("select").add("loop",loop);
samer@0: }
samer@0:
samer@0: public void execute(String cmd, Environment env) throws Exception {
samer@0: if (cmd.equals("next")) next();
samer@0: else if (cmd.equals("prev")) prev();
samer@0: else if (cmd.equals("reopen")) reopen();
samer@0: else if (cmd.equals("view")) {
samer@0: if (list!=null) {
samer@0: Shell.expose(
samer@0: new JScrollPane(new JList(list.toArray())),
samer@0: "playlist");
samer@0: }
samer@0: } else if (cmd.equals("select")) {
samer@0: JFileChooser dlg = new JFileChooser(".");
samer@0:
samer@0: dlg.setMultiSelectionEnabled(true);
samer@0: dlg.setFileSelectionMode(dlg.FILES_AND_DIRECTORIES);
samer@0: if (list!=null && !list.isEmpty()) {
samer@0: dlg.setCurrentDirectory(list.get(0));
samer@0: dlg.setSelectedFiles(list.toArray(new File[0]));
samer@0: }
samer@0: dlg.setDialogTitle("Select audio files");
samer@0: if (dlg.showDialog(null, "OK")==JFileChooser.APPROVE_OPTION) {
samer@0: File first=dlg.getSelectedFile();
samer@0: if (first.isDirectory())
samer@0: setDirectory(first,".wav");
samer@0: else
samer@0: setPlaylist(Arrays.asList(dlg.getSelectedFiles()));
samer@0: }
samer@0: } else if (cmd.equals("loop")) {
samer@0: loop=X._bool(env.datum(),!loop);
samer@0: }
samer@0: }
samer@0:
samer@0: // Viewable
samer@0: public Viewer getViewer() { return new FileSourceViewer(); }
samer@0: class FileSourceViewer extends DefaultViewer implements Agent {
samer@0: public FileSourceViewer() {
samer@0: super(FileSource.this);
samer@0: add(curFile);
samer@0: add(Shell.createButtonsFor(this));
samer@0: }
samer@0:
samer@0: public void getCommands(Agent.Registry r) {
samer@0: r.add("prev").add("reopen").add("next").add("view").add("select");
samer@0: }
samer@0: public void execute(String cmd, Environment env) throws Exception {
samer@0: FileSource.this.execute(cmd,env);
samer@0: }
samer@0: };
samer@0:
samer@0: private static java.io.FileFilter getFileFilter(final String ext) {
samer@0: return new java.io.FileFilter() {
samer@0: public boolean accept(File file) {
samer@0: return file.getName().toLowerCase().endsWith(ext);
samer@0: }
samer@0: };
samer@0: }
samer@0:
samer@0:
samer@0:
samer@0: private static AudioInputStream convertFormat(AudioInputStream sin, AudioFormat fin, AudioFormat fout) throws Exception
samer@0: {
samer@1: Shell.trace("\nconvertFormat:");
samer@1: Shell.trace(" | source: "+fin.toString());
samer@1: Shell.trace(" | target: "+fout.toString());
samer@1:
samer@1: if (fin.equals(fout)) return sin;
samer@0: else if (fin.getEncoding()==AudioFormat.Encoding.PCM_SIGNED) {
samer@1: try {
samer@1: Shell.trace(" | Trying direct (PCM) from "+fin.getEncoding().toString());
samer@1: return AudioSystem.getAudioInputStream(fout,sin);
samer@1: }
samer@0: catch (IllegalArgumentException ex) { Shell.trace("Direct conversion failed"); }
samer@0:
samer@0: AudioFormat fint = new AudioFormat( // PCM
samer@0: fout.getSampleRate(), fout.getSampleSizeInBits(),
samer@0: fin.getChannels(), true, fout.isBigEndian());
samer@1: Shell.trace(" | Trying PCM conversion via "+fint.toString());
samer@0: return AudioSystem.getAudioInputStream(fout,AudioSystem.getAudioInputStream(fint,sin));
samer@0: } else {
samer@0: // First, check for MP3 - if so, cannot convert number of channels
samer@0: if (fin.getChannels()==fout.getChannels() && fin.getSampleRate()==fout.getSampleRate()) {
samer@1: Shell.trace(" | Trying decoding from "+fin.getEncoding().toString());
samer@0: return AudioSystem.getAudioInputStream(fout,sin);
samer@0: } else {
samer@0: AudioFormat fint = new AudioFormat(
samer@0: fin.getSampleRate(), fout.getSampleSizeInBits(),
samer@0: fin.getChannels(), true, fout.isBigEndian());
samer@1: Shell.trace(" | Trying recursive via "+fint.toString());
samer@0: return convertFormat(AudioSystem.getAudioInputStream(fint,sin),fint,fout);
samer@0: }
samer@0: }
samer@0: }
samer@0: }
samer@0: