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
|