annotate src/samer/audio/FileSource.java @ 8:5e3cbbf173aa tip

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