annotate src/samer/audio/FileSource.java @ 0:bf79fb79ee13

Initial Mercurial check in.
author samer
date Tue, 17 Jan 2012 17:50:20 +0000
parents
children 5df24c91468d
rev   line source
samer@0 1 /*
samer@0 2 * FileSource.java
samer@0 3 *
samer@0 4 * Copyright (c) 2000, Samer Abdallah, King's College London.
samer@0 5 * All rights reserved.
samer@0 6 *
samer@0 7 * This software is provided AS iS and WITHOUT ANY WARRANTY;
samer@0 8 * without even the implied warranty of MERCHANTABILITY or
samer@0 9 * FITNESS FOR A PARTICULAR PURPOSE.
samer@0 10 */
samer@0 11
samer@0 12 package samer.audio;
samer@0 13
samer@0 14 import samer.core.*;
samer@0 15 import samer.core.types.*;
samer@0 16 import samer.core.util.*;
samer@0 17 import samer.tools.*;
samer@0 18 import javax.sound.sampled.*;
samer@0 19 import javax.swing.*;
samer@0 20 import java.io.*;
samer@0 21 import java.util.*;
samer@0 22
samer@0 23 /**
samer@0 24 An AudioSource that read from multiple audio files. Can read any
samer@0 25 format for which the appropriate JavaSound plug-in is installed on
samer@0 26 your system, eg WAV, AU, MP3, OGG. The implementation of
samer@0 27 AudioSource.reader() returns a Task which gets samples from the audio
samer@0 28 files, going through the play list one by one. If the loop flag
samer@0 29 is true, then samples are returned indefinitely by looping through the
samer@0 30 playlist; otherwise, an EOFException is thrown if an attempt is made
samer@0 31 to read beyond the end of the last file.
samer@0 32
samer@0 33 FileSource is a Viewable called "playlist", and an Agent with commands
samer@0 34 next, prev, reopen, view, select, loop.
samer@0 35
samer@0 36 Properties read from current environment:
samer@0 37 <dl>
samer@0 38 <dt>current<dd>Current file (String)
samer@0 39 <dt>loop<dd>Loop playlist? (Boolean) [true]
samer@0 40 </dl>
samer@0 41
samer@0 42 */
samer@0 43
samer@0 44 public class FileSource extends Viewable implements AudioSource, Agent
samer@0 45 {
samer@0 46 List<File> list=null;
samer@0 47 ListIterator<File> it=null;
samer@0 48 boolean loop;
samer@0 49 int channelsToMix;
samer@0 50 VFile curFile;
samer@0 51 InputStream in=null;
samer@0 52 AudioFormat format=null;
samer@0 53 byte[] byte_buf=null;
samer@0 54 int chunk=0;
samer@0 55
samer@0 56 /**
samer@0 57 * Construct a FileSource initialised with current file and loop initialised
samer@0 58 * from the current Environment. No exception is thrown if the current file
samer@0 59 * cannot be opened, however, you must be sure to set it to a valid file
samer@0 60 * (or to set a playlist) before trying to read any samples.
samer@0 61 */
samer@0 62 public FileSource()
samer@0 63 {
samer@0 64 super("playlist");
samer@0 65
samer@0 66 Shell.push(node);
samer@0 67 curFile=new VFile("current","",0);
samer@0 68 loop=Shell.getBoolean("loop",true);
samer@0 69 Shell.pop();
samer@0 70
samer@0 71 setAgent(this);
samer@0 72 Shell.registerViewable(this);
samer@0 73 }
samer@0 74
samer@0 75 public void dispose() {
samer@0 76 close();
samer@0 77 Shell.deregisterViewable(this);
samer@0 78 curFile.dispose();
samer@0 79 }
samer@0 80
samer@0 81 /** Returns current file */
samer@0 82 public File getFile() { return curFile.getFile(); }
samer@0 83 public void setTargetFormat(AudioFormat f) { format=f; }
samer@0 84
samer@0 85 /** If true then loop playlist, otherwise, an Exception will be thrown
samer@0 86 * when the end of the playlist is reached. If there is no playlist, then
samer@0 87 * looping applies to the current file only. */
samer@0 88 public void setLoop(boolean f) { loop=f; }
samer@0 89
samer@0 90 /** Set playlist to all WAV files in given directory */
samer@0 91 public void setDirectory(File dir, String ext) {
samer@0 92 setPlaylist(Arrays.asList(dir.listFiles(getFileFilter(ext))));
samer@0 93 }
samer@0 94
samer@0 95 public synchronized List<File> getPlaylist() { return list; }
samer@0 96 public synchronized void setPlaylist(List<File> l) { list=l; it=list.listIterator(); }
samer@0 97 private synchronized void setFile(File f) { curFile.setFile(f); }
samer@0 98
samer@0 99 /** Go back to start of playlist. Next block of samples will be from head
samer@0 100 * of first file in playlist. Will throw an exception if there is no playlist */
samer@0 101 public synchronized void rewind() throws Exception {
samer@0 102 it=list.listIterator(); next();
samer@0 103 }
samer@0 104
samer@0 105 /** Move to head of next file in playlist */
samer@0 106 public synchronized void next() throws Exception
samer@0 107 {
samer@0 108 boolean wasOpen=isOpen();
samer@0 109
samer@0 110 if (wasOpen) close();
samer@0 111 if (!it.hasNext()) {
samer@0 112 if (!loop) throw new EOFException();
samer@0 113 it=list.listIterator();
samer@0 114 if (!it.hasNext()) throw new Exception("no files in playlist");
samer@0 115 }
samer@0 116 setFile(it.next());
samer@0 117 if (wasOpen) openCurrent();
samer@0 118 }
samer@0 119
samer@0 120 /** Move to head of previous file in playlist */
samer@0 121 public synchronized void prev() throws Exception
samer@0 122 {
samer@0 123 boolean wasOpen=isOpen();
samer@0 124 if (wasOpen) close();
samer@0 125 if (!it.hasPrevious()) {
samer@0 126 if (!loop) throw new EOFException();
samer@0 127 it=list.listIterator(list.size());
samer@0 128 if (!it.hasPrevious()) throw new Exception("no files in playlist");
samer@0 129 }
samer@0 130 setFile(it.previous());
samer@0 131 if (wasOpen) openCurrent();
samer@0 132 }
samer@0 133
samer@0 134 public boolean isOpen() { return in!=null; }
samer@0 135
samer@0 136 /** Closes current input stream */
samer@0 137 public synchronized void close() {
samer@0 138 try {
samer@0 139 if (in!=null) {
samer@0 140 Shell.trace("Closing audio stream...");
samer@0 141 in.close(); in=null; byte_buf=null;
samer@0 142 }
samer@0 143 }
samer@0 144 catch (IOException ex) {}
samer@0 145 }
samer@0 146
samer@0 147 /** Opens the playlist starting with the first file. */
samer@0 148 public synchronized void open() throws Exception { rewind(); if (!isOpen()) openCurrent(); }
samer@0 149
samer@0 150 /** Opens the current file. Next read will returns samples
samer@0 151 * from head of given file. If setFormat() was called previously, then
samer@0 152 * we will attempt to do format conversion. */
samer@0 153 private synchronized void openCurrent() throws Exception
samer@0 154 {
samer@0 155 File file=curFile.getFile();
samer@0 156 Shell.trace("\nOpening audio file: "+file);
samer@0 157
samer@0 158 AudioInputStream s=AudioSystem.getAudioInputStream(file);
samer@0 159 AudioFormat af=s.getFormat();
samer@0 160 long frames=file.length()/af.getFrameSize();
samer@0 161
samer@0 162 Shell.trace(" format: "+af);
samer@0 163 Shell.trace(" duration: "+(long)(frames/af.getFrameRate())+" s");
samer@0 164
samer@0 165 // convert to target format if required
samer@0 166 if (format!=null) {
samer@0 167 if (!format.equals(af)) {
samer@0 168 Shell.trace(" converting to "+format);
samer@0 169 if (af.getChannels()>format.getChannels()) {
samer@0 170 Shell.trace(" channels mix down required.");
samer@0 171 AudioFormat fmt1 = new AudioFormat( format.getEncoding(), format.getSampleRate(),
samer@0 172 format.getSampleSizeInBits(),
samer@0 173 af.getChannels(), format.getFrameSize(), format.getFrameRate(), format.isBigEndian());
samer@0 174
samer@0 175 channelsToMix = af.getChannels();
samer@0 176 s=convertFormat(s,af,fmt1);
samer@0 177 } else {
samer@0 178 channelsToMix = 0;
samer@0 179 s=convertFormat(s,af,format);
samer@0 180 }
samer@0 181 }
samer@0 182 }
samer@0 183
samer@0 184 // If we have a reader task, then update the byte buffer
samer@0 185 if (chunk>0) setByteBuffer();
samer@0 186
samer@0 187 // Shell.trace("stream format: "+s.getFormat());
samer@0 188 in = new BufferedInputStream(s,16*1024);
samer@0 189 }
samer@0 190
samer@0 191 /** Returns number of bytes available in current file */
samer@0 192 public int available() throws Exception { return in.available(); }
samer@0 193
samer@0 194 /** Reopen current file, so that next read will be from head of file. */
samer@0 195 public synchronized void reopen() throws Exception { close(); openCurrent(); }
samer@0 196
samer@0 197 private void setChunkSize(int s) { chunk=s; }
samer@0 198 private void setByteBuffer() {
samer@0 199 byte_buf = new byte[2*chunk*(channelsToMix>0 ? channelsToMix : 1)];
samer@0 200 }
samer@0 201
samer@0 202 /** Returns a Task which copies samples as doubles into the given
samer@0 203 * buffer between the given positions. */
samer@0 204 public Task reader(final double [] dbuf, final int off, final int len) {
samer@0 205 setChunkSize(len);
samer@0 206 if (in!=null) setByteBuffer();
samer@0 207
samer@0 208 return new AnonymousTask() {
samer@0 209 public void run() throws Exception {
samer@0 210 int n = 0;
samer@0 211
samer@0 212 synchronized (FileSource.this) {
samer@0 213 int blen = byte_buf.length;
samer@0 214 while (n < blen) {
samer@0 215 int count = in.read(byte_buf, n, blen - n);
samer@0 216 if (count > 0) n+=count;
samer@0 217 else if (it!=null) next();
samer@0 218 else if (!loop) throw new EOFException();
samer@0 219 else reopen();
samer@0 220 }
samer@0 221 }
samer@0 222 if (channelsToMix>0) Util.shortToDoubleMixDown(byte_buf,dbuf,off,len,channelsToMix);
samer@0 223 else Util.shortToDouble(byte_buf,dbuf,off,len);
samer@0 224 }
samer@0 225 };
samer@0 226 }
samer@0 227
samer@0 228 /** Returns a Task which copies samples as floats into the given
samer@0 229 * buffer between the given positions. */
samer@0 230 public Task reader(final float [] dbuf, final int off, final int len) {
samer@0 231 setChunkSize(len);
samer@0 232 if (in!=null) setByteBuffer();
samer@0 233
samer@0 234 return new AnonymousTask() {
samer@0 235 public synchronized void run() throws Exception {
samer@0 236 int n = 0;
samer@0 237
samer@0 238 synchronized (FileSource.this) {
samer@0 239 int blen = byte_buf.length;
samer@0 240 while (n < blen) {
samer@0 241 int count = in.read(byte_buf, n, blen - n);
samer@0 242 if (count > 0) n+=count;
samer@0 243 else if (it!=null) next();
samer@0 244 else if (!loop) throw new EOFException();
samer@0 245 else reopen();
samer@0 246 }
samer@0 247 }
samer@0 248 if (channelsToMix>0) Util.shortToFloatMixDown(byte_buf,dbuf,off,len,channelsToMix);
samer@0 249 else Util.shortToFloat(byte_buf,dbuf,off,len);
samer@0 250 }
samer@0 251 };
samer@0 252 }
samer@0 253
samer@0 254 // Agent
samer@0 255 public void getCommands(Agent.Registry r) {
samer@0 256 r.add("prev").add("reopen").add("next").add("view")
samer@0 257 .add("select").add("loop",loop);
samer@0 258 }
samer@0 259
samer@0 260 public void execute(String cmd, Environment env) throws Exception {
samer@0 261 if (cmd.equals("next")) next();
samer@0 262 else if (cmd.equals("prev")) prev();
samer@0 263 else if (cmd.equals("reopen")) reopen();
samer@0 264 else if (cmd.equals("view")) {
samer@0 265 if (list!=null) {
samer@0 266 Shell.expose(
samer@0 267 new JScrollPane(new JList(list.toArray())),
samer@0 268 "playlist");
samer@0 269 }
samer@0 270 } else if (cmd.equals("select")) {
samer@0 271 JFileChooser dlg = new JFileChooser(".");
samer@0 272
samer@0 273 dlg.setMultiSelectionEnabled(true);
samer@0 274 dlg.setFileSelectionMode(dlg.FILES_AND_DIRECTORIES);
samer@0 275 if (list!=null && !list.isEmpty()) {
samer@0 276 dlg.setCurrentDirectory(list.get(0));
samer@0 277 dlg.setSelectedFiles(list.toArray(new File[0]));
samer@0 278 }
samer@0 279 dlg.setDialogTitle("Select audio files");
samer@0 280 if (dlg.showDialog(null, "OK")==JFileChooser.APPROVE_OPTION) {
samer@0 281 File first=dlg.getSelectedFile();
samer@0 282 if (first.isDirectory())
samer@0 283 setDirectory(first,".wav");
samer@0 284 else
samer@0 285 setPlaylist(Arrays.asList(dlg.getSelectedFiles()));
samer@0 286 }
samer@0 287 } else if (cmd.equals("loop")) {
samer@0 288 loop=X._bool(env.datum(),!loop);
samer@0 289 }
samer@0 290 }
samer@0 291
samer@0 292 // Viewable
samer@0 293 public Viewer getViewer() { return new FileSourceViewer(); }
samer@0 294 class FileSourceViewer extends DefaultViewer implements Agent {
samer@0 295 public FileSourceViewer() {
samer@0 296 super(FileSource.this);
samer@0 297 add(curFile);
samer@0 298 add(Shell.createButtonsFor(this));
samer@0 299 }
samer@0 300
samer@0 301 public void getCommands(Agent.Registry r) {
samer@0 302 r.add("prev").add("reopen").add("next").add("view").add("select");
samer@0 303 }
samer@0 304 public void execute(String cmd, Environment env) throws Exception {
samer@0 305 FileSource.this.execute(cmd,env);
samer@0 306 }
samer@0 307 };
samer@0 308
samer@0 309 private static java.io.FileFilter getFileFilter(final String ext) {
samer@0 310 return new java.io.FileFilter() {
samer@0 311 public boolean accept(File file) {
samer@0 312 return file.getName().toLowerCase().endsWith(ext);
samer@0 313 }
samer@0 314 };
samer@0 315 }
samer@0 316
samer@0 317
samer@0 318
samer@0 319 private static AudioInputStream convertFormat(AudioInputStream sin, AudioFormat fin, AudioFormat fout) throws Exception
samer@0 320 {
samer@0 321 if (fin==fout) return sin;
samer@0 322 else if (fin.getEncoding()==AudioFormat.Encoding.PCM_SIGNED) {
samer@0 323 try { return AudioSystem.getAudioInputStream(fout,sin); }
samer@0 324 catch (IllegalArgumentException ex) { Shell.trace("Direct conversion failed"); }
samer@0 325
samer@0 326 AudioFormat fint = new AudioFormat( // PCM
samer@0 327 fout.getSampleRate(), fout.getSampleSizeInBits(),
samer@0 328 fin.getChannels(), true, fout.isBigEndian());
samer@0 329 Shell.trace("Trying PCM conversion via "+fint.toString());
samer@0 330 return AudioSystem.getAudioInputStream(fout,AudioSystem.getAudioInputStream(fint,sin));
samer@0 331 } else {
samer@0 332 // First, check for MP3 - if so, cannot convert number of channels
samer@0 333 if (fin.getChannels()==fout.getChannels() && fin.getSampleRate()==fout.getSampleRate()) {
samer@0 334 Shell.trace("Trying direct decoding from "+fin.getEncoding().toString());
samer@0 335 return AudioSystem.getAudioInputStream(fout,sin);
samer@0 336 } else {
samer@0 337 AudioFormat fint = new AudioFormat(
samer@0 338 fin.getSampleRate(), fout.getSampleSizeInBits(),
samer@0 339 fin.getChannels(), true, fout.isBigEndian());
samer@0 340 Shell.trace("Trying conversion via "+fint.toString());
samer@0 341 return convertFormat(AudioSystem.getAudioInputStream(fint,sin),fint,fout);
samer@0 342 }
samer@0 343 }
samer@0 344 }
samer@0 345 }
samer@0 346