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: