JSyn Programmers Guide |
|
If you have questions or want to report a bug, please visit our technical support page.
Please read the Documentation Home Page first before reading this document! It contains disclaimers, release info, known bugs and links to all relevant documents.
You may wish to read the document that describes how to compile and run JSyn programs on your type of computer.
This document is gradually being replaced by the JSyn Tutorial.
When you connect up a stereo system, you connect the various components so that sound can flow between them. Sound may flow, for example, from a CD player, to a graphic equalizer, to an amplifier, and then to a pair of speakers. In a similar manner, sound generating, and sound processing units are connected together in JSyn to create new sounds. These sound components are traditionally called unit generators. The library of unit generators includes oscillators, filters, ramps and other functions that you would find on a modular analog synthesiser, or a software synthesis package like CSound.
import com.softsynth.jsyn.*;JSyn also has a number of sub packages. If you wish to use classes from them, you must import them as well.
import com.softsynth.jsyn.util.*; // for general JSyn utilities import com.softsynth.jsyn.view102.*; // for graphical AWT1.0.2 related utilities import com.softsynth.jsyn.view11x.*; // for graphical AWT1.1.x related utilities import com.softsynth.jsyn.circuits.*; // for example SynthCircuits that combine units
Synth.startEngine(0);The method startEngine() is static so you do not have to create a Synth object.
You can specify an optional frame rate as a second parameter. By specifying a low sample rate, ideally 1/2 or 1/4 of the default rate, you can reduce the number of samples that must be calculated per second. This will reduce the amount of computation that the CPU must perform. For example:
Synth.startEngine( 0, Synth.DEFAULT_FRAME_RATE / 2.0 );When your program finishes, you must terminate JSyn by calling:
Synth.stopEngine();
myNoise = new WhiteNoise(); myFilter = new StateVariableFilter();
myNoise.output.connect( myFilter.input );
The above code will connect the output of the noise unit to the input of the filter. Input and Output are called "Ports".
Each SynthOutput can be connected to multiple SynthInputs. Each SynthInput can only have one SynthOutput connected to it. This is called "multiple fan-out".
Some units have multi part ports. An example is the Line_Out unit which has a stereo input. To connect to a specific part of a port, do the following:
myFilter.output.connect( 0, myOut.input, 0 ); /* Left side */ myFilter.output.connect( 0, myOut.input, 1 ); /* Right side */See the reference manual for more information on making connections to a SynthOutput.
myOsc.frequency.set( 440.0 ); /* 440 Hz */ myOsc.amplitude.set( 0.5 ); /* Half amplitude.*/You can connect units to the parameter ports of another unit instead of setting them to a constant value. Thus you can do FM by connecting to the Frequency port of a Osc_Sine unit.
If you connect units together and use the result to control another unit, you may wish to coerce the signal type to use the same dimension as the target.
myLFO.output.connect( myAdd.inputA ); /* Mix result of LFO with constant control value. */ myAdd.output.connect( myOsc.frequency ); myAdd.inputB.setSignalType( Synth.SIGNAL_TYPE_OSC_FREQ ); /* Control center frequency using Hz */ myAdd.inputB.set(440.0); /* Set oscillators center frequency to 440 Hz via adder */The set() method is supported by the SynthInputclass which a subclass of SynthVariable.
You may wish to explore the reference material that describes the various Port types.
myOut.start(); myOsc.start();When a unit is started, its function is executed by the synthesis engine for every sample frame. This places a burden on the CPU or DSP. When you are finished making the sound you should stop the unit generators. To stop them:
myOut.stop(); myOsc.stop();
try { /* Make various calls to JSyn between these brackets. */ } catch (SynthException e) { SynthAlert.showError(this,e); }To learn more about catch and throw, please refer to a Java Language reference such as Thinking in Java.
Warning: Netscape will call stop() then start() when you resize a browser window containing the Applet. This can cause a glitch in the sound when your application is terminated and restarted.
Here is a skeleton JSyn program that can be used to used to create your own programs. The main() method is used only when the program is run as an application using the "java" command. It is not used in a browser applet. This is a standard Java technique.
import java.util.*; import java.awt.*; import java.applet.Applet; import com.softsynth.jsyn.*;
public class MyJSynProgram extends Applet { /* Declare Synthesis Objects here */
/* Can be run as either an application or as an applet. */ public static void main(String args[]) { MyJSynProgram applet = new MyJSynProgram(); AppletFrame frame = new AppletFrame("Test JSyn", applet); frame.resize(600,400); frame.show(); frame.test(); }
/* * Setup synthesis by overriding start() method. */ public void start() { try { Synth.startEngine(0); /* Your setup code goes here. */ } catch(SynthException e) { SynthAlert.showError(this,e); } }
/* * Clean up synthesis by overriding stop() method. */ public void stop() { try { /* Your cleanup code goes here. */ Synth.stopEngine(); } catch(SynthException e) { SynthAlert.showError(this,e); } } }
To find out the current time in ticks by calling Synth.getTickCount().
To find out how many audio frames are in a Tick by calling Synth.getFramesPerTick().
To determine the rate of Ticks per Second by calling Synth.getTickRate().
To sleep until a certain time then call Synth.sleepUntilTick( wakeupTimeInTicks
).
To sleep for a certain number of ticks from now use Synth.sleepForTicks(
ticksToSleepFor ).
If you want to maintain synchronized timing over the long run then you should use sleepUntilTick() because sleepForTicks() will incorporate small delays that accumulate over time. Here is an example of code that will do something every 100 ticks. If the initial time is 1000, then doSomething will occur at 1000, 1100, 1200, 1300, 1400, etc.
int time = Synth.getTickCount(); for( int i=0; i<200; i++ ) { doSomething(); time += 100; Synth.sleepUntilTick( time ); }If we had used sleepForTicks() then if doSomething had taken a few ticks to execute, then doSomething might occur at 1000, 1102, 1205, 1307, 1411, etc.
The events that can be scheduled include starting and stopping of SynthUnits, setting of Port values, and queueing of Sample and Envelope data. To use the event buffer, simply put the time in ticks as the first parameter. For example:
myOsc.start(); /* Start oscillator now. */ myOsc.start( 2000 ); /* Start oscillator at time 2000. */ myOsc.start( Synth.getTickCount() + 300 ); /* Start 300 ticks in the future. */
myOsc.frequency.set( 3000, 220.0 ); /* Set frequency at time 3000. */The 'C' Synthesis Engine will defer the deletion of units until all outstanding events have occured. So you can create a SynthUnit, schedule future start(), setPort() and stop() events, and then lose the SynthUnit object reference and it will still play to completion in the background without being deleted.
Samples are typically loaded from an AIFF or WAV file on disk. But they can also be loaded from any Java InputStream coming across a network, or a byte array or other streram source. To create a stream from a file on disk, call:
Once you have a stream, you can load a SynthSample from that stream. For an AIFF format file which is common on the Apple Macintosh, call:FileInputStream stream = new FileInputStream(fileName);
For a WAV format file which is common on the PC, call:SynthSampleAIFF sample = new SynthSampleAIFF( stream );
You can generally tell whether a file is an AIFF or WAV file because the fileName will end with ".aiff" or ".aif" or ".wav". A handy way to determine this is to call:SynthSampleWAV sample = new SynthSampleWAV( stream );
This method will return SynthSample.WAV or SynthSample.AIFF, or possible other formats in the future.int fileType = SynthSample.getFileType( fileName );
A number of these can go wrong when you try to do this. The file might be missing, or it could be corrupted. Or they may not be enough memory to load the sample. Only your application can know what to do when these exceptions occur so you will have to catch them. Here is an example that ties all these calls together with exception catching.
/* Load sample from a file. */ SynthSample sample; try { FileInputStream stream = new FileInputStream(fileName); try { switch( SynthSample.getFileType( fileName ) ) { case SynthSample.AIFF: sample = new SynthSampleAIFF( stream ); break; case SynthSample.WAV: sample = new SynthSampleWAV( stream ); break; default: SynthAlert.showError(this, "Unrecognized sample file suffix on " + fileName ); break; } } catch( IOException e ) { SynthAlert.showError(this,e); } } catch( FileNotFoundException e ) { SynthAlert.showError(this,e); } catch( SecurityException e ) { SynthAlert.showError(this,e); }Take a look at "JSynExamples/TJ_Sample2.java" for a full example of loading and playing a sample. If you would like more control over the parsing of the file, look at the documentation for SynthSampleAIFF and ParseIFF, or the equivalent WAV classes.
myMonoSample = new SynthSample( numFrames );A frame is one or more sample values that will play simultaneously. A monphonic sample has one sample value, or channel, per frame. A stereo sample has two sample values per frame. To create a SynthSample with 2 samples per frame call:
/* Create a short array to build sample image. */ short[] data = new short[NUM_FRAMES]; /* Create a sample and fill it with recognizable data. */ mySample= new SynthSample( NUM_FRAMES ); for( int i=0; i<NUM_FRAMES; i++ ) { data[i] = (short) (i*0x100); /* Ascending data. */ } /* Write all of data to sample memory. */ mySample.write( data ); /* Optionally get rid of array, or reuse it, because it is no ** longer needed. */ data = null;
gSampler = new SampleReader_16V1();Sample players have a special port that you can "connect" samples to. It has a built in queue that portions of samples can be placed in. You can queue up multiple portions of various samples on a sample queue and they will be played in order one after the other. You can optionally specify that a portion of a sample be looped if it is the last thing in the queue. When a loop finishes, it checks to see if something else is in the queue. If so it advances to the next portion. If not then it loops once more. Here is an example of queuing an entire sample starting at frame zero to be looped.
mySampler.samplePort.queueLoop( mySamp, 0, mySamp.getNumFrames() );Imagine a violin sample that has an attack portion, a loop in the middle, and a release portion. To play such a sample, one would first call queue() for the attack portion. Then call queueLoop() for the loop portion.
mySampler.samplePort.queue( mySamp, 0, attackSize ); mySampler.samplePort.queueLoop( mySamp, loopStart, loopSize );When the sample is started, it will play through the attack and begin looping. When you want to release the note, simply call queue() for the release portion.
mySampler.samplePort.queue( mySamp, releaseStart, releaseSize );When the sample player finishes playing the loop it will play the release portion and then stop because the queue will be empty. Samples can be added to an empty queue while a sample reader is playing and it will start immediately.
If you wish to schedule the placing of sample portions in the queue at a future time, pass the time as the first parameter. The event scheduler will place this in the queue at the desired time.
mySampler.samplePort.queue( releaseTime, mySamp, RELEASE_START, RELEASE_SIZE );The rate at which the sample is played is controlled using the rate port.
mySampler.rate.set( 22050.0 );
myTable = new SynthTable( 100 );Tables are only monophonic. Table data is accessed using read() and write() methods similar to SynthSamples.
An interesting synthesis effect can be achieved by connecting a simple waveform to the Input of a WaveShaper. The shape, and thus the timbre, of the Output waveform is now a function of the Amplitude of the input waveform. This technique is called "Wave Shaping". Even though this unit is used most often for Table Lookup, we call it a "WaveShaper" because that sounds better than "TableLookerUpper".
The table can be filled and associated with the WaveShaper in the same
way we did for the TableOscillator example above.
The parse the file and return the data in an array of shorts:SynthSampleAIFF myAIFF = new SynthSampleAIFF();
short shrtData[] = myAIFF.loadShorts( stream, true );Then write the short array into the SynthTable:
if( shrtData != null )
{
allocate( shrtData.length );
write( shrtData );
}
All of the SynthBusOutputs connected to a SynthBusInput are added together
before being used. This makes them handy for mixing signals.
You can convert a normal signal to a bus signal using a BusWriter unit.
It has a SynthInput and a SynthBusOutput. Multiple BusWriter units
can be connected to a single BusReader unit which has a SynthBusInput and
a normal SynthOutput.
This technique is handy if you want to combine an arbitrary number
of signals to be processed together. If you only need to combine
two signals then you could just use a AddUnit unit.
Here is an example that show how to mix an array of oscillators to a single bus. The output of this bus could then be processed by a filter or passed to a reverberation effect, etc.
/* Create a single reader which can connect to as many other units as desired. */ myBusReader = new BusReader();
for( int i=0; i<NUM_OSC; i++) { myBusWriter[i] = new BusWriter(); /* Create bus writers. */ myOsc[i].output.connect( myBusWriter[i].input ); /* Connect oscillator. */ myBusWriter[i].busOutput.connect( myBusReader.busInput ); }
myBusReader.output.connect( myFilter.input );
Creating an envelope is very similar to creating a sample. In fact envelopes and samples share many properties as we shall see. Envelope data is stored internally in a SynthEnvelopeobject. The data is loaded using write() just like with a sample. The difference is, however, that the contents of an envelope are quite different from a sample. A sample frame typically consists of a 16 bit integer or short. Envelope frames, or segments, consist of a pair of double numbers that describe a duration and a value. The duration number describes how long it should take the envelope to reach the value number starting from the value of the previous frame. Consider the following code which creates an envelope with several frames.
/* Create an envelope and fill it with recognizable data. */ double[] data = { 0.02, 1.0, /* duration,value pair for frame[0] */ 0.30, 0.1, /* duration,value pair for frame[1] */ 0.50, 0.7, /* duration,value pair for frame[2] */ 0.50, 0.9, /* duration,value pair for frame[3] */ 0.80, 0.0 /* duration,value pair for frame[4] */ }; numFrames = data.length/2; myEnvData = new SynthEnvelope( numFrames ); myEnvData.write( 0, data, 0, numFrames );The first frame has a duration of 0.02 and a value of 1.0. This means that when this envelope is started that it will take 0.02 seconds to get from its current value to a value of 1.0. If you want to force an envelope to start immediately at a particular value then use a duration of 0.0. When the envelope reaches 1.0 then it will take 0.30 seconds to reach a value of 0.1. The final frame typically has a value of zero for envelopes that control amplitude.
As a convenient alternative you can construct the envelope in one step using a constructor that accepts a double array directly.
/* Create an envelope and fill it with recognizable data. */ myEnvData = new SynthEnvelope( double[] data = { 0.02, 1.0, /* duration,value pair for frame[0] */ 0.30, 0.1, /* duration,value pair for frame[1] */ 0.50, 0.7, /* duration,value pair for frame[2] */ 0.50, 0.9, /* duration,value pair for frame[3] */ 0.80, 0.0 /* duration,value pair for frame[4] */ } );
myEnv = new EnvelopePlayer(); myEnv.envelopePort.clear( ); myEnv.envelopePort.queue( myEnvData, 0, myEnvData.getNumFrames() ); myEnv.start();To simulate the attack and release characteristics of some instruments you could queue up the beginning portion of an envelope when the note is started, then queue the release portion when the note is released.
/* Queue up all segments except last segment. */ if( evt.target == attackButton ) { myEnv.envelopePort.clear( ); myEnv.envelopePort.queue( myEnvData, 0, 3 ); myEnv.envelopePort.queueLoop( myEnvData, 1, 2 ); myEnv.start(); } /* Queue final segment. */ else if( evt.target == releaseButton ) { myEnv.envelopePort.queue( myEnvData, 3, 2 ); }To control another unit's parameters using an envelope, simply connect the output of the envelope player to the port on the other unit.
/* Connect envelope to oscillator amplitude. */ myEnv.output.connect( 0, myOsc.amplitude, 0 );You can adjust the rate of envelope playback using the rate port on the EnvelopePlayer. This rate is an unsigned value that can range from 0.0 to 2.0. Amplitude envelopes of acoustic instruments tend to get shorter as they go higher in pitch. This rate parameter can be used to smulate that effect.
myEnv.rate.set( 0.7 );
To make a JSyn circuit, define a subclass of the SynthCircuit class:
You must declare the units that will be part of of your circuit. You will also need ports so you can control your circuit using connect() and set() just like you control unit generators.public class WindSound extends SynthCircuit
In the constructor for the new circuit, instantiate the new units, and then add them to the circuit. This is similar to creating a subclass of Panel and adding components to it. Here is an example of adding a TriangleOscillator to a circuit:TriangleOscillator triOsc; // declare an oscillator public SynthInput frequency; // declare a frequency port
You can make the circuit's ports control the internal units by setting them equal to the unit's ports. Then use the addPort() method to make the ports visible when one calls getNumPorts() on the circuit.add( triOsc = new TriangleOscillator() );
What you end up with is an object that can be used just like a unit generator but is more complex. You can include SynthCircuits inside of other SynthCircuits to make hierarchies of sound effects. Here is an example of how the above WindSound is used.addPort( frequency = triOsc.frequency );
/* Create circuits and unit generators. */ wind = new WindSound(); myOut = new LineOut();
/* Set Amplitude of LFO to 1.0 */ wind.amplitude.set(1.0);
/* Connect wind to output. */ wind.output.connect( 0, myOut.input, 0 ); wind.output.connect( 0, myOut.input, 1 );You may wish to view an example of a circuit that creates a wind sound by filtering white noise.
So what do you do? Create a special kind of port called a SynthDistributor that distributes the incoming signal to multiple places in the circuit. Declare a SynthDistributor port:addPort( amplitude = squareOsc.amplitude ); // WRONG! addPort( amplitude = triOsc.amplitude ); // WRONG! Overrides previous line.
In the circuit constructor, instantantiate a new SynthDistributor. Pass it the circuit using the "this" keyword, and give it a name.public SynthDistributor amplitude;
Then, inside the circuit constructor, connect the SynthDistributor to as many internal ports as you wish.amplitude = new SynthDistributor( this, "amplitude" );
When you use this circuit, if you connect an external output port to the SynthDistributor, it will automatically be connected to everything that the distributor is connected to. Likewise, if you set() a distributor, every connected port will be set. Since the same value will be passed to each internally connected port, they must all have the same signal type. By default, a SynthDistributor will have the signal type Synth.SIGNAL_RAW_SIGNED which works for amplitudes and other basic signals. You can specify a signal type when you create the SynthDistributor.amplitude.connect( squareOsc.amplitude ); amplitude.connect( triOsc.amplitude );
If you want to match the signal type of one of the internal ports, but you're not sure what the signal type is, then the safest thing to do is to query the signal type.frequency = new SynthDistributor( this, "frequency", Synth.SIGNAL_TYPE_OSC_FREQ );
For an example of using a SynthDistributor, see the circuit "RingModBell.java".frequency = new SynthDistributor( this, "frequency", svFilter.frequency.getSignalType() );
mySampler.samplePort.queue( mySamp, 0, mySamp.getNumFrames(), Synth.FLAG_AUTO_STOP );
<!-- Only EMBED Jsyn plugin for the Netscape browser. --> <script src="smart_embed_jsyn.js"> <!-- hide this stuff from browsers that don't support JavaScript document.writeln("ERROR: could not find smart_embed_jsyn.js!"); // stop hiding javaScript --> </script>
<APPLET CODE="mystuff.TJ_Beep.class" NAME="TJ_Beep" CODEBASE="../../classes" WIDTH="200" HEIGHT="100"> </APPLET>Look in the "usercode" directory for an example JSyn program called "TJ_Beep.java", and an HTML file called "TJ_Beep.html". The installation notes for your machine will explain how to compile and run this example. Look in the examples directory for many HTML examples.
Note that you must have the JSyn plugin installed correctly before JSyn can be used in an Applet. See the JSyn Installation Guide for details.