/* An actor that writes audio samples to a sound file.
@Copyright (c) 2000-2007 The Regents of the University of California.
All rights reserved.
Permission is hereby granted, without written agreement and without
license or royalty fees, to use, copy, modify, and distribute this
software and its documentation for any purpose, provided that the
above copyright notice and the following two paragraphs appear in all
copies of this software.
IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
ENHANCEMENTS, OR MODIFICATIONS.
PT_COPYRIGHT_VERSION 2
COPYRIGHTENDKEY
*/
package ptolemy.actor.lib.javasound;
import java.io.IOException;
import ptolemy.actor.lib.Sink;
import ptolemy.data.DoubleToken;
import ptolemy.data.IntToken;
import ptolemy.data.Token;
import ptolemy.data.expr.Parameter;
import ptolemy.data.type.BaseType;
import ptolemy.kernel.CompositeEntity;
import ptolemy.kernel.util.Attribute;
import ptolemy.kernel.util.IllegalActionException;
import ptolemy.kernel.util.NameDuplicationException;
import ptolemy.kernel.util.StringAttribute;
import ptolemy.media.javasound.SoundWriter;
///////////////////////////////////////////////////////////
//// AudioWriter
/**
This actor reads audio samples from the input port and writes
the samples to the specified sound file. Any existing file
with the same name will be silently overwritten. The input port
is of type DoubleToken. Each DoubleToken read from the input
port represents one sample of the audio data and should be in
the range [-1.0, 1.0]. Any samples that are outside of this range
will be hard-clipped to fall within this range before they are
written to the sound file. Single channel (mono) and two
channel (stereo) formats are supported. For single channel
audio, tokens are written to channel 0 of the output port. For
stereo , tokens are written to channel 0 (left) and channel 1
(right) of the output port.
<p>
The following parameters should be set to specify the format
of the file to write. In all cases, an exception is thrown if
an illegal parameter value is used. Note that if a parameter is
changed while audio writing is active, all data written so far will
be saved, and the sound file will be closed. Subsequent audio
samples will then be written to a new sound file with the new
parameter values.
<p>
<ul>
<li><i>pathName</i> should be set to the name of the output
file. Any existing file with the same name will be silently
overwritten. Relative filenames are supported. The default value is
"outfile.wav". The audio format to use is determined by the file
extension. E.g., "outfile.wav" will create a WAV format file.
The supported file formats are AU, WAV, and, AIFF. For example,
to write samples to a Sun AU format file with the name "test.au"
in the directory "c:\tmp", this parameter should be set to the
value c:\tmp\test.au. To write samples to a file with name "test.au"
in the current directory, this parameter should be set to the value
test.au.
<li><i>channels</i> should be set to desired number of audio
channels. Allowable values are 1 (for mono) and 2 (for stereo).
The default value is 1.
<li><i>sampleRate</i> should be set to desired sample rate,
in Hz. The DoubleTokens read in by this actor will be
interpreted as having this sample rate. Allowable values are
8000, 11025, 22050, 44100, and 48000. The default value is 8000.
<li><i>bitsPerSample</i> should be set to desired bit
resolution. Allowable values are 8 and 16. The default value is 16.
</ul>
<p>
There are security issues involved with accessing files and
audio resources in applets. By default, applets are not
allowed to write files. The .java.policy file may be modified
to grant applets more privileges.
<p>
Note: Requires Java 2 v1.3.0 or later.
@author Brian K. Vogel
@version $Id: AudioWriter.java,v 1.34 2007/12/06 18:27:42 cxh Exp $
@since Ptolemy II 1.0
@Pt.ProposedRating Yellow (vogel)
@Pt.AcceptedRating Yellow (chf)
@see AudioReader
@see AudioCapture
@see AudioPlayer
*/
public class AudioWriter extends Sink {
/** Construct an actor with the given container and name.
* @param container The container.
* @param name The name of this actor.
* @exception IllegalActionException If the actor cannot be contained
* by the proposed container.
* @exception NameDuplicationException If the container already has an
* actor with this name.
*/
public AudioWriter(CompositeEntity container, String name)
throws IllegalActionException, NameDuplicationException {
super(container, name);
input.setTypeEquals(BaseType.DOUBLE);
pathName = new StringAttribute(this, "pathName");
pathName.setExpression("outfile.wav");
sampleRate = new Parameter(this, "sampleRate", new IntToken(8000));
sampleRate.setTypeEquals(BaseType.INT);
bitsPerSample = new Parameter(this, "bitsPerSample", new IntToken(16));
bitsPerSample.setTypeEquals(BaseType.INT);
channels = new Parameter(this, "channels", new IntToken(1));
channels.setTypeEquals(BaseType.INT);
attributeChanged(channels);
_curElement = 0;
}
///////////////////////////////////////////////////////////////////
//// ports and parameters ////
/** The name of the file to write to. The default
* value of this parameter is "test.wav", which creates a file called
* test.wav in the current directory and writes samples to this file.
* <p>
* The audio format to use is determined by the file extension.
* E.g., "outfile.wav" will create a WAV format file.
* The supported file formats are AU, WAV, and, AIFF.
* <p>
* An exception will be occur if the path references an
* unsupported sound file.
*/
public StringAttribute pathName;
/** The desired sample rate to use, in Hz. Valid values
* include: 8000, 11025, 22050, 44100, and 48000.
* The default value of the sample rate is an IntToken equal
* to 8000.
* <p>
* An exception will be thrown if an illegal value is used.
*/
public Parameter sampleRate;
/** The desired number of bits per sample. The default value is
* an IntToken equal to 16. Supported values are 8 and 16.
* An exception will be thrown if an illegal value is
* used.
*/
public Parameter bitsPerSample;
/** The number of audio channels to use. Supported values are
* 1 (single channel) and 2 (stereo).
* The default value is an IntToken equal to 1.
* An exception will be thrown if an illegal value is used.
*/
public Parameter channels;
///////////////////////////////////////////////////////////////////
//// public methods ////
/** Handle change requests for all parameters. An exception is
* thrown if the requested change is not allowed.
* @param attribute The attribute that changed.
* @exception IllegalActionException If the change is not
* allowed.
*/
public void attributeChanged(Attribute attribute)
throws IllegalActionException {
if (attribute == channels) {
_channels = ((IntToken) channels.getToken()).intValue();
if (_channels < 1) {
throw new IllegalActionException(this,
"Attempt to set channels parameter to an illegal "
+ "value of: " + _channels
+ " . The value must be a "
+ "positive integer.");
}
// Check if we need to reallocate.
if ((_inArray == null) || (_channels != _inArray.length)) {
_inArray = new Token[_channels][];
}
if ((_audioPutArray == null)
|| (_channels != _audioPutArray.length)) {
_audioPutArray = new double[_channels][];
}
for (int i = 0; i < _channels; i++) {
_audioPutArray[i] = new double[_putSampleSize];
}
} else {
super.attributeChanged(attribute);
return;
}
if (_safeToInitialize == true) {
_initializeWriter();
}
}
/** Open a new audio file for writing. Any existing file
* with the same name will be overwritten.
* @exception IllegalActionException If the file cannot be opened,
* or if the parent class throws it.
*/
public void initialize() throws IllegalActionException {
super.initialize();
// Initialize/Reinitialize audio resources.
_initializeWriter();
_safeToInitialize = true;
}
/** Invoke <i>count</i> iterations of this actor. If there
* at least <i>count</i> tokens on channel 0 (and also on
* channel 1 if stereo mode is used), then read <i>count</i>
* tokens from the corresponding channels and write the token
* values to the specified sound file. Otherwise, do nothing,
* and return a value of NOT_READY. Note that at most one token
* is read from each channel in an iteration.
* <p>
* This method should be called instead of the prefire(),
* fire(), and postfire() methods when this actor is used in a
* domain that supports vectorized actors.
* @param count The number of iterations to perform.
* @return COMPLETED if the actor was successfully iterated the
* specified number of times. Otherwise, return NOT_READY if there
* are not enough tokens on the input port, or throw an exception
* if there is a problem writing audio samples to the specified
* file.
* @see ptolemy.actor.Executable
* @exception IllegalActionException If there is a problem
* writing the audio sample(s) to the specified file.
*/
public int iterate(int count) throws IllegalActionException {
for (int j = 0; j < _channels; j++) {
if (input.hasToken(j, count)) {
// NOTE: inArray[j].length may be > count, in which case
// only the first count tokens are valid.
_inArray[j] = input.get(j, count);
} else {
// Not enough tokens on the input port, so just return.
return NOT_READY;
}
}
// For each sample.
for (int k = 0; k < count; k++) {
// For each channel.
for (int m = 0; m < _channels; m++) {
// Keep writing samples until the array argument to
// putSamples() is full, then call putSamples().
// Array argument to putSamples() is not full yet,
// so write another sample for each channel.
_audioPutArray[m][_curElement] = ((DoubleToken) _inArray[m][k])
.doubleValue();
}
// Increment pointer.
_curElement++;
if (_curElement == _putSampleSize) {
try {
// write out samples to speaker and/or file.
_soundWriter.putSamples(_audioPutArray);
} catch (Exception ex) {
throw new IllegalActionException(this,
"Cannot write audio: \n" + ex.getMessage());
}
// Reset pointer to beginning of array.
_curElement = 0;
}
}
return COMPLETED;
}
/** If there is at least 1 token on channel 0 (and also on
* channel 1 if stereo mode is used), then read 1 token
* from the corresponding channels and write the token
* values to the specified sound file. Otherwise, do nothing,
* and return a value of false.
* @exception IllegalActionException If there is a problem
* writing the audio sample(s) to the specified file.
*/
public boolean postfire() throws IllegalActionException {
int returnVal = iterate(1);
if (returnVal == COMPLETED) {
return true;
} else if (returnVal == NOT_READY) {
// This should never happen.
throw new IllegalActionException(this, "Actor "
+ "is not ready to fire.");
} else if (returnVal == STOP_ITERATING) {
return false;
}
return false;
}
/** Set up the number channels to use.
* for use in the postfire() method.
* @exception IllegalActionException If the parent class throws it.
*/
public void preinitialize() throws IllegalActionException {
super.preinitialize();
_channels = ((IntToken) channels.getToken()).intValue();
}
/** Close the specified file.
* @exception IllegalActionException If there is a problem
* closing the file.
*/
public void wrapup() throws IllegalActionException {
super.wrapup();
// Close any open sound files.
if (_soundWriter != null) {
try {
_soundWriter.closeFile();
} catch (IOException ex) {
throw new IllegalActionException(this, "Error closing file:\n"
+ ex.getMessage());
}
}
_safeToInitialize = false;
}
///////////////////////////////////////////////////////////////////
//// private methods ////
/** Initialize/Reinitialize audio resources. First close any
* open sound files. Then reread all parameters, create a
* new SoundWriter object.
* <p>
* This method is synchronized since it is not safe to call
* SoundWriter methods while this method is executing.
* @exception IllegalActionException If there is a problem
* initializing the SoundWriter object.
*/
private synchronized void _initializeWriter() throws IllegalActionException {
// Close any open sound files. Free
// up audio system resources.
if (_soundWriter != null) {
try {
_soundWriter.closeFile();
} catch (IOException ex) {
throw new IllegalActionException(this, "Cannot write audio: \n"
+ ex.getMessage());
}
}
_putSampleSize = 64;
for (int i = 0; i < _channels; i++) {
_audioPutArray[i] = new double[_putSampleSize];
}
String pathNameString = pathName.getExpression();
// Write audio data to a file.
int sampleRateInt = ((IntToken) sampleRate.getToken()).intValue();
int bitsPerSampleInt = ((IntToken) bitsPerSample.getToken()).intValue();
int channelsInt = ((IntToken) channels.getToken()).intValue();
int putSamplesSize = _putSampleSize;
_soundWriter = new SoundWriter(pathNameString, sampleRateInt,
bitsPerSampleInt, channelsInt, putSamplesSize);
_curElement = 0;
}
///////////////////////////////////////////////////////////////////
//// private variables ////
private SoundWriter _soundWriter;
private int _channels;
private int _putSampleSize;
private double[][] _audioPutArray;
// Pointer to the current sample of the array parameter of
// putSamples() method of SoundWriter.
private int _curElement;
private Token[][] _inArray;
private boolean _safeToInitialize = false;
}