view src/samer/audio/FileSource.java @ 1:5df24c91468d

Oh my what a mess.
author samer
date Fri, 05 Apr 2019 16:26:00 +0100
parents bf79fb79ee13
children
line wrap: on
line source
/*
 *	FileSource.java
 *
 *	Copyright (c) 2000, Samer Abdallah, King's College London.
 *	All rights reserved.
 *
 *	This software is provided AS iS and WITHOUT ANY WARRANTY;
 *	without even the implied warranty of MERCHANTABILITY or
 *	FITNESS FOR A PARTICULAR PURPOSE.
 */

package samer.audio;

import  samer.core.*;
import  samer.core.types.*;
import  samer.core.util.*;
import  samer.tools.*;
import  javax.sound.sampled.*;
import  javax.swing.*;
import  java.io.*;
import  java.util.*;
import  java.nio.ByteBuffer;

/**
	An AudioSource that read from multiple audio files. Can read any
	format for which the appropriate JavaSound plug-in is installed on
	your system, eg WAV, AU, MP3, OGG. The implementation of 
	AudioSource.reader() returns a Task which gets samples from the audio 
	files, going through the play list one by one. If the loop flag
	is true, then samples are returned indefinitely by looping through the
	playlist; otherwise, an EOFException is thrown if an attempt is made
	to read beyond the end of the last file.
	
	FileSource is a Viewable called "playlist", and an Agent with commands
	next, prev, reopen, view, select, loop.
	
	Properties read from current environment:
	<dl>
		<dt>current<dd>Current file (String)
		<dt>loop<dd>Loop playlist? (Boolean) [true]
	</dl>
					
*/

public class FileSource extends Viewable implements AudioSource, Agent
{
	List<File>				list=null;
	ListIterator<File>	it=null;
	boolean			loop, buffered=true;
	int				channelsToMix;
	VFile				curFile;
	InputStream		in=null;
	AudioFormat		format=null, inFormat=null;
	byte[]  			byte_buf=null;
	int				chunk=0, bytesPerSample=0;

	/**
	 * Construct a FileSource initialised with current file and loop initialised
	 * from the current Environment. No exception is thrown if the current file
	 * cannot be opened, however, you must be sure to set it to a valid file
	 * (or to set a playlist) before trying to read any samples.
	 */
	public FileSource()
	{
		super("playlist");

		Shell.push(node);
		curFile=new VFile("current","",0);
		loop=Shell.getBoolean("loop",true);
		Shell.pop();

		setAgent(this);
		Shell.registerViewable(this);
	}

	// AudioSource interface methods
	public void dispose() {
		close();
		Shell.deregisterViewable(this);
		curFile.dispose();
	}

	public int getChannels() { return format.getChannels(); }
	public float getRate()   { return format.getFrameRate(); }

	/** Returns current file */
	public File getFile() { return curFile.getFile(); }
	public void setBuffering(boolean b) { buffered=b; }

	/** The actual format of the stream in. May not be same as target format. **/
	public AudioFormat getStreamFormat() { return inFormat; }

	/** The requested audio format. **/
	public AudioFormat getTargetFormat() { return format; }
	public void        setTargetFormat(AudioFormat f) { format=f; }

	/** If true then loop playlist, otherwise, an Exception will be thrown
	 * when the end of the playlist is reached. If there is no playlist, then
	 * looping applies to the current file only. */
	public void setLoop(boolean f) { loop=f; }

	/** Set playlist to all WAV files in given directory */
	public void setDirectory(File dir, String ext) {
		setPlaylist(Arrays.asList(dir.listFiles(getFileFilter(ext)))); 
	}

	public synchronized List<File> getPlaylist() { return list; }
	public synchronized void setPlaylist(List<File> l) { list=l; it=list.listIterator(); }
	private synchronized void setFile(File f) { curFile.setFile(f); }

	/** Go back to start of playlist. Next block of samples will be from head
	 *  of first file in playlist. Will throw an exception if there is no playlist */
	public synchronized void rewind() throws Exception {
		it=list.listIterator(); next();
	}

	/** Move to head of next file in playlist */
	public synchronized void next() throws Exception
	{
		boolean wasOpen=isOpen();

		if (wasOpen) close();
		if (!it.hasNext()) {
			if (!loop) throw new EOFException();
			it=list.listIterator();
			if (!it.hasNext()) throw new Exception("no files in playlist");
		}
		setFile(it.next());
		if (wasOpen) openCurrent();
	}

	/** Move to head of previous file in playlist */
	public synchronized void prev() throws Exception
	{
		boolean wasOpen=isOpen();
		if (wasOpen) close();
		if (!it.hasPrevious()) {
			if (!loop) throw new EOFException();
			it=list.listIterator(list.size());
			if (!it.hasPrevious()) throw new Exception("no files in playlist");
		}
		setFile(it.previous());
		if (wasOpen) openCurrent();
	}

	public boolean isOpen() { return in!=null; }

	/** Closes current input stream */
	public synchronized void close() {
		try { 
			if (in!=null) {
				Shell.trace("Closing audio stream...");
				in.close(); in=null; byte_buf=null; 
			}
		}
		catch (IOException ex) {}
	}

	/** Opens the playlist starting with the first file.  */
	public synchronized void open() throws Exception { rewind(); if (!isOpen()) openCurrent(); }

	/** Opens the current file. Next read will returns samples
	 *  from head of given file. If setFormat() was called previously, then
	 *  we will attempt to do format conversion. */
	private synchronized void openCurrent() throws Exception
	{
		File file=curFile.getFile();
		Shell.trace("\nFileSource:Opening audio file "+file);

		AudioInputStream		s=AudioSystem.getAudioInputStream(file);
		AudioFormat				fmt1, af=s.getFormat();

		Shell.trace("  format: "+af);

		// convert to target format if required
		if (format!=null) {
			if (!format.equals(af)) {
				Shell.trace("  converting to "+format);
				if (af.getChannels()>format.getChannels()) {
					int frameSize = af.getChannels()*format.getSampleSizeInBits()/8;
					Shell.trace("  channels mix down required.");
					fmt1 = new AudioFormat( format.getEncoding(), format.getSampleRate(), 
								format.getSampleSizeInBits(), 
								af.getChannels(), frameSize, format.getFrameRate(), format.isBigEndian());

					channelsToMix = af.getChannels();
				} else {
					channelsToMix = 0;
					fmt1=format;
				}
				Shell.trace("  converting via "+fmt1);
				s=convertFormat(s,af,fmt1);
				inFormat = fmt1;
			} else {
				Shell.trace("  no formation conversion required");
				channelsToMix = 0;
				inFormat = af;
			}
		} else {
			Shell.trace("  using stream native format");
			channelsToMix = 0;
			inFormat = af;
		}
		in=s;
		if (buffered) in = new BufferedInputStream(in,64*1024);

		// If we have a reader task, then update the byte buffer
		if (chunk>0) setByteBuffer();
	}

	/** Returns number of bytes available in current file */
	public int available() throws Exception { return in.available(); }

	/** Reopen current file, so that next read will be from head of file. */
	public synchronized void reopen() throws Exception { close(); openCurrent(); }

	private void setBlockSize(int s) { chunk=s; }
	private void setByteBuffer() {
		bytesPerSample = 2*(channelsToMix>0 ? channelsToMix : 1);
		byte_buf       = new byte[chunk*bytesPerSample];
	}

	public int readInto(ByteBuffer buf, int offset, int len) throws IOException {
		return in.read(buf.array(), offset, len);
	}

	/** Returns a Task which copies samples as doubles into the given
	 *  buffer between the given positions. */
	public Task reader(final double [] dbuf, final int off, final int len) {
		setBlockSize(len);
		if (in!=null) setByteBuffer();

		return new AnonymousTask() {
			public void run() throws Exception {
				synchronized (FileSource.this) {
					// loop until len samples copied into dbuf
					int rem=len, pos=off;
					while (rem>0) {
						int bytesRead = in.read(byte_buf, 0, rem*bytesPerSample);
						if (bytesRead > 0) { // append this chunk to output
							int count=bytesRead/bytesPerSample;
							if (channelsToMix>0) {
								Util.shortToDoubleMixDown(byte_buf,dbuf,pos,count,channelsToMix);
							} else {
								Util.shortToDouble(byte_buf,dbuf,pos,count);
							}
							pos+=count; rem-=count;
						} else if (it!=null) next(); // next file if there is one
						else if (!loop) throw new EOFException(); // not looping and no more files
						else reopen(); // back to first file

						// if (rem>0) Shell.trace("Read "+bytesRead+" bytes, need "+rem+" more samples.");
					}
				}
			}
		};
	}

	/** Returns a Task which copies samples as floats into the given
	 *  buffer between the given positions. */
	public Task reader(final float [] dbuf, final int off, final int len) {
		setBlockSize(len);
		if (in!=null) setByteBuffer();

		return new AnonymousTask() {
			public synchronized void run() throws Exception {
				synchronized (FileSource.this) {
					// loop until len samples copied into dbuf
					int rem=len, pos=off;
					while (rem>0) {
						int bytesRead = in.read(byte_buf, 0, rem*bytesPerSample);
						if (bytesRead > 0) {
							int count=bytesRead/bytesPerSample;
							if (channelsToMix>0) {
								Util.shortToFloatMixDown(byte_buf,dbuf,pos,count,channelsToMix);
							} else {
								Util.shortToFloat(byte_buf,dbuf,pos,count);
							}
							pos+=count; rem-=count;
						} else if (it!=null) next();
						else if (!loop) throw new EOFException();
						else reopen();
					}
				}
			}
		};
	}

	// Agent
	public void getCommands(Agent.Registry r) {
		r.add("prev").add("reopen").add("next").add("view")
		 .add("select").add("loop",loop);
	}

	public void execute(String cmd, Environment env) throws Exception {
		if (cmd.equals("next")) next();
		else if (cmd.equals("prev")) prev();
		else if (cmd.equals("reopen")) reopen();
		else if (cmd.equals("view")) {
			if (list!=null) {
				Shell.expose(
					new JScrollPane(new JList(list.toArray())),
					"playlist");
			}
		} else if (cmd.equals("select")) {
			JFileChooser dlg = new JFileChooser(".");

			dlg.setMultiSelectionEnabled(true);
			dlg.setFileSelectionMode(dlg.FILES_AND_DIRECTORIES);
			if (list!=null && !list.isEmpty()) {
				dlg.setCurrentDirectory(list.get(0));
				dlg.setSelectedFiles(list.toArray(new File[0]));
			}
			dlg.setDialogTitle("Select audio files");
			if (dlg.showDialog(null, "OK")==JFileChooser.APPROVE_OPTION) {
				File first=dlg.getSelectedFile();
				if (first.isDirectory())
					setDirectory(first,".wav");
				else
					setPlaylist(Arrays.asList(dlg.getSelectedFiles()));
			}
		} 	else if (cmd.equals("loop")) {
			loop=X._bool(env.datum(),!loop);
		}
	}

	// Viewable
	public Viewer getViewer() { return new FileSourceViewer(); }
	class FileSourceViewer extends DefaultViewer implements Agent {
		public FileSourceViewer() {
			super(FileSource.this);
			add(curFile);
			add(Shell.createButtonsFor(this));
		}

		public void getCommands(Agent.Registry r) {
			r.add("prev").add("reopen").add("next").add("view").add("select");
		}
		public void execute(String cmd, Environment env) throws Exception {
			FileSource.this.execute(cmd,env);
		}
	};

	private static java.io.FileFilter getFileFilter(final String ext) { 
		return new java.io.FileFilter() {
			public boolean accept(File file) {
				return file.getName().toLowerCase().endsWith(ext);
			}
		};
	}



	private static AudioInputStream convertFormat(AudioInputStream sin, AudioFormat fin, AudioFormat fout) throws Exception
	{
		Shell.trace("\nconvertFormat:");
		Shell.trace("  | source: "+fin.toString());
		Shell.trace("  | target: "+fout.toString());

		if (fin.equals(fout)) return sin;
		else if (fin.getEncoding()==AudioFormat.Encoding.PCM_SIGNED) {
			try { 
				Shell.trace("  | Trying direct (PCM) from "+fin.getEncoding().toString());
				return AudioSystem.getAudioInputStream(fout,sin); 
			}
			catch (IllegalArgumentException ex) { Shell.trace("Direct conversion failed"); }

			AudioFormat fint = new AudioFormat(  // PCM
				fout.getSampleRate(), fout.getSampleSizeInBits(),
				fin.getChannels(), true, fout.isBigEndian());
			Shell.trace("  | Trying PCM conversion via "+fint.toString());
			return AudioSystem.getAudioInputStream(fout,AudioSystem.getAudioInputStream(fint,sin));
		} else {
		// First, check for MP3 - if so, cannot convert number of channels
			if (fin.getChannels()==fout.getChannels() && fin.getSampleRate()==fout.getSampleRate()) {
				Shell.trace("  | Trying decoding from "+fin.getEncoding().toString());
				return AudioSystem.getAudioInputStream(fout,sin);
			} else {
				AudioFormat fint = new AudioFormat( 
					fin.getSampleRate(), fout.getSampleSizeInBits(),
					fin.getChannels(), true, fout.isBigEndian());
				Shell.trace("  | Trying recursive via "+fint.toString());
				return convertFormat(AudioSystem.getAudioInputStream(fint,sin),fint,fout);
			}
		}
	}
}