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
|