aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPhil Burk <[email protected]>2014-12-30 16:53:03 -0800
committerPhil Burk <[email protected]>2014-12-30 16:53:03 -0800
commit534969d42ca5168d645678345cd21242fe41f389 (patch)
treee8f5d1cba1ec57685e76ceb923d8da25a7846cfb
parenta4d8ca95178d2e3acfc3299a4b73e84c2646d24e (diff)
Initial commit of code.
-rw-r--r--NOTICE.txt12
-rw-r--r--README.txt23
-rw-r--r--libs/jportaudio.jarbin0 -> 5203 bytes
-rw-r--r--licenses/license_part1.txt1
-rw-r--r--licenses/license_part2.txt13
-rw-r--r--licenses/license_part3.txt4
-rw-r--r--misc/android-formatting.xml252
-rwxr-xr-xscripts/addlicenses.sh82
-rw-r--r--src/com/jsyn/JSyn.java76
-rw-r--r--src/com/jsyn/Synthesizer.java179
-rw-r--r--src/com/jsyn/apps/AboutJSyn.java114
-rw-r--r--src/com/jsyn/apps/InstrumentTester.java124
-rw-r--r--src/com/jsyn/data/AudioSample.java108
-rw-r--r--src/com/jsyn/data/DoubleTable.java105
-rw-r--r--src/com/jsyn/data/FloatSample.java146
-rw-r--r--src/com/jsyn/data/Function.java35
-rw-r--r--src/com/jsyn/data/HammingWindow.java41
-rw-r--r--src/com/jsyn/data/HannWindow.java36
-rw-r--r--src/com/jsyn/data/SampleMarker.java30
-rw-r--r--src/com/jsyn/data/SegmentedEnvelope.java125
-rw-r--r--src/com/jsyn/data/SequentialData.java97
-rw-r--r--src/com/jsyn/data/SequentialDataCommon.java117
-rw-r--r--src/com/jsyn/data/ShortSample.java123
-rw-r--r--src/com/jsyn/data/SpectralWindow.java21
-rw-r--r--src/com/jsyn/data/SpectralWindowFactory.java55
-rw-r--r--src/com/jsyn/data/Spectrum.java97
-rw-r--r--src/com/jsyn/devices/AudioDeviceFactory.java92
-rw-r--r--src/com/jsyn/devices/AudioDeviceInputStream.java31
-rw-r--r--src/com/jsyn/devices/AudioDeviceManager.java120
-rw-r--r--src/com/jsyn/devices/AudioDeviceOutputStream.java30
-rw-r--r--src/com/jsyn/devices/javasound/JavaSoundAudioDevice.java434
-rw-r--r--src/com/jsyn/devices/javasound/MidiDeviceTools.java80
-rw-r--r--src/com/jsyn/devices/jportaudio/JPortAudioDevice.java259
-rw-r--r--src/com/jsyn/engine/LoadAnalyzer.java61
-rw-r--r--src/com/jsyn/engine/MultiTable.java230
-rw-r--r--src/com/jsyn/engine/SynthesisEngine.java683
-rw-r--r--src/com/jsyn/exceptions/ChannelMismatchException.java35
-rw-r--r--src/com/jsyn/instruments/DrumWoodFM.java159
-rw-r--r--src/com/jsyn/instruments/JSynInstrumentLibrary.java45
-rw-r--r--src/com/jsyn/instruments/NoiseHit.java114
-rw-r--r--src/com/jsyn/instruments/SubtractiveSynthVoice.java182
-rw-r--r--src/com/jsyn/instruments/WaveShapingVoice.java187
-rw-r--r--src/com/jsyn/io/AudioFifo.java186
-rw-r--r--src/com/jsyn/io/AudioInputStream.java46
-rw-r--r--src/com/jsyn/io/AudioOutputStream.java29
-rw-r--r--src/com/jsyn/midi/MessageParser.java67
-rw-r--r--src/com/jsyn/midi/MidiConstants.java59
-rw-r--r--src/com/jsyn/package.html17
-rw-r--r--src/com/jsyn/ports/ConnectableInput.java38
-rw-r--r--src/com/jsyn/ports/ConnectableOutput.java23
-rw-r--r--src/com/jsyn/ports/GettablePort.java27
-rw-r--r--src/com/jsyn/ports/InputMixingBlockPart.java108
-rw-r--r--src/com/jsyn/ports/PortBlockPart.java205
-rw-r--r--src/com/jsyn/ports/QueueDataCommand.java166
-rw-r--r--src/com/jsyn/ports/QueueDataEvent.java80
-rw-r--r--src/com/jsyn/ports/SequentialDataCrossfade.java139
-rw-r--r--src/com/jsyn/ports/SettablePort.java28
-rw-r--r--src/com/jsyn/ports/UnitBlockPort.java110
-rw-r--r--src/com/jsyn/ports/UnitDataQueueCallback.java31
-rw-r--r--src/com/jsyn/ports/UnitDataQueuePort.java468
-rw-r--r--src/com/jsyn/ports/UnitFunctionPort.java48
-rw-r--r--src/com/jsyn/ports/UnitGatePort.java158
-rw-r--r--src/com/jsyn/ports/UnitInputPort.java235
-rw-r--r--src/com/jsyn/ports/UnitOutputPort.java103
-rw-r--r--src/com/jsyn/ports/UnitPort.java85
-rw-r--r--src/com/jsyn/ports/UnitSpectralInputPort.java83
-rw-r--r--src/com/jsyn/ports/UnitSpectralOutputPort.java69
-rw-r--r--src/com/jsyn/ports/UnitVariablePort.java64
-rw-r--r--src/com/jsyn/ports/package.html13
-rw-r--r--src/com/jsyn/scope/AudioScope.java98
-rw-r--r--src/com/jsyn/scope/AudioScopeModel.java157
-rw-r--r--src/com/jsyn/scope/AudioScopeProbe.java94
-rw-r--r--src/com/jsyn/scope/DefaultWaveTraceModel.java48
-rw-r--r--src/com/jsyn/scope/MultiChannelScopeProbeUnit.java246
-rw-r--r--src/com/jsyn/scope/TriggerModel.java67
-rw-r--r--src/com/jsyn/scope/WaveTraceModel.java27
-rw-r--r--src/com/jsyn/scope/swing/AudioScopeProbeView.java45
-rw-r--r--src/com/jsyn/scope/swing/AudioScopeView.java112
-rw-r--r--src/com/jsyn/scope/swing/MultipleWaveDisplay.java58
-rw-r--r--src/com/jsyn/scope/swing/ScopeControlPanel.java46
-rw-r--r--src/com/jsyn/scope/swing/ScopeProbePanel.java82
-rw-r--r--src/com/jsyn/scope/swing/ScopeTriggerPanel.java45
-rw-r--r--src/com/jsyn/scope/swing/WaveTraceView.java122
-rw-r--r--src/com/jsyn/swing/ASCIIMusicKeyboard.java191
-rw-r--r--src/com/jsyn/swing/DoubleBoundedRangeModel.java86
-rw-r--r--src/com/jsyn/swing/DoubleBoundedRangeSlider.java101
-rw-r--r--src/com/jsyn/swing/DoubleBoundedTextField.java96
-rw-r--r--src/com/jsyn/swing/EnvelopeEditorBox.java570
-rw-r--r--src/com/jsyn/swing/EnvelopeEditorPanel.java164
-rw-r--r--src/com/jsyn/swing/EnvelopePoints.java234
-rw-r--r--src/com/jsyn/swing/ExponentialRangeModel.java105
-rw-r--r--src/com/jsyn/swing/InstrumentBrowser.java118
-rw-r--r--src/com/jsyn/swing/JAppletFrame.java65
-rw-r--r--src/com/jsyn/swing/PortBoundedRangeModel.java45
-rw-r--r--src/com/jsyn/swing/PortControllerFactory.java60
-rw-r--r--src/com/jsyn/swing/PortModelFactory.java64
-rw-r--r--src/com/jsyn/swing/PresetSelectionListener.java23
-rw-r--r--src/com/jsyn/swing/RotaryController.java329
-rw-r--r--src/com/jsyn/swing/RotaryTextController.java53
-rw-r--r--src/com/jsyn/swing/SoundTweaker.java124
-rw-r--r--src/com/jsyn/swing/XYController.java132
-rw-r--r--src/com/jsyn/unitgen/Add.java50
-rw-r--r--src/com/jsyn/unitgen/AsymptoticRamp.java81
-rw-r--r--src/com/jsyn/unitgen/ChannelIn.java59
-rw-r--r--src/com/jsyn/unitgen/ChannelOut.java52
-rw-r--r--src/com/jsyn/unitgen/Circuit.java86
-rw-r--r--src/com/jsyn/unitgen/Compare.java38
-rw-r--r--src/com/jsyn/unitgen/ContinuousRamp.java91
-rw-r--r--src/com/jsyn/unitgen/CrossFade.java59
-rw-r--r--src/com/jsyn/unitgen/Delay.java57
-rw-r--r--src/com/jsyn/unitgen/Divide.java53
-rw-r--r--src/com/jsyn/unitgen/DualInTwoOut.java59
-rw-r--r--src/com/jsyn/unitgen/EdgeDetector.java49
-rw-r--r--src/com/jsyn/unitgen/EnvelopeAttackDecay.java145
-rw-r--r--src/com/jsyn/unitgen/EnvelopeDAHDSR.java294
-rw-r--r--src/com/jsyn/unitgen/ExponentialRamp.java103
-rw-r--r--src/com/jsyn/unitgen/FFT.java36
-rw-r--r--src/com/jsyn/unitgen/FFTBase.java86
-rw-r--r--src/com/jsyn/unitgen/FilterAllPass.java62
-rw-r--r--src/com/jsyn/unitgen/FilterBandPass.java44
-rw-r--r--src/com/jsyn/unitgen/FilterBandStop.java49
-rw-r--r--src/com/jsyn/unitgen/FilterBiquad.java147
-rw-r--r--src/com/jsyn/unitgen/FilterBiquadCommon.java99
-rw-r--r--src/com/jsyn/unitgen/FilterBiquadShelf.java111
-rw-r--r--src/com/jsyn/unitgen/FilterFourPoles.java150
-rw-r--r--src/com/jsyn/unitgen/FilterHighPass.java46
-rw-r--r--src/com/jsyn/unitgen/FilterHighShelf.java38
-rw-r--r--src/com/jsyn/unitgen/FilterLowPass.java63
-rw-r--r--src/com/jsyn/unitgen/FilterLowShelf.java40
-rw-r--r--src/com/jsyn/unitgen/FilterOnePole.java62
-rw-r--r--src/com/jsyn/unitgen/FilterOnePoleOneZero.java68
-rw-r--r--src/com/jsyn/unitgen/FilterOneZero.java65
-rw-r--r--src/com/jsyn/unitgen/FilterPeakingEQ.java68
-rw-r--r--src/com/jsyn/unitgen/FilterStateVariable.java119
-rw-r--r--src/com/jsyn/unitgen/FilterTwoPoles.java66
-rw-r--r--src/com/jsyn/unitgen/FilterTwoPolesTwoZeros.java79
-rw-r--r--src/com/jsyn/unitgen/FixedRateMonoReader.java52
-rw-r--r--src/com/jsyn/unitgen/FixedRateMonoWriter.java51
-rw-r--r--src/com/jsyn/unitgen/FixedRateStereoReader.java59
-rw-r--r--src/com/jsyn/unitgen/FixedRateStereoWriter.java58
-rw-r--r--src/com/jsyn/unitgen/FourWayFade.java94
-rw-r--r--src/com/jsyn/unitgen/FunctionEvaluator.java76
-rw-r--r--src/com/jsyn/unitgen/FunctionOscillator.java58
-rw-r--r--src/com/jsyn/unitgen/Grain.java89
-rw-r--r--src/com/jsyn/unitgen/GrainCommon.java32
-rw-r--r--src/com/jsyn/unitgen/GrainEnvelope.java52
-rw-r--r--src/com/jsyn/unitgen/GrainFarm.java176
-rw-r--r--src/com/jsyn/unitgen/GrainScheduler.java44
-rw-r--r--src/com/jsyn/unitgen/GrainSource.java36
-rw-r--r--src/com/jsyn/unitgen/GrainSourceSine.java51
-rw-r--r--src/com/jsyn/unitgen/IFFT.java36
-rw-r--r--src/com/jsyn/unitgen/ImpulseOscillator.java59
-rw-r--r--src/com/jsyn/unitgen/ImpulseOscillatorBL.java39
-rw-r--r--src/com/jsyn/unitgen/Integrate.java82
-rw-r--r--src/com/jsyn/unitgen/InterpolatingDelay.java117
-rw-r--r--src/com/jsyn/unitgen/Latch.java53
-rw-r--r--src/com/jsyn/unitgen/LatchZeroCrossing.java72
-rw-r--r--src/com/jsyn/unitgen/LineIn.java51
-rw-r--r--src/com/jsyn/unitgen/LineOut.java49
-rw-r--r--src/com/jsyn/unitgen/LinearRamp.java86
-rw-r--r--src/com/jsyn/unitgen/Maximum.java42
-rw-r--r--src/com/jsyn/unitgen/Minimum.java43
-rw-r--r--src/com/jsyn/unitgen/MixerMono.java77
-rw-r--r--src/com/jsyn/unitgen/MixerMonoRamped.java54
-rw-r--r--src/com/jsyn/unitgen/MixerStereo.java94
-rw-r--r--src/com/jsyn/unitgen/MixerStereoRamped.java71
-rw-r--r--src/com/jsyn/unitgen/MonoStreamWriter.java47
-rw-r--r--src/com/jsyn/unitgen/Multiply.java64
-rw-r--r--src/com/jsyn/unitgen/MultiplyAdd.java57
-rw-r--r--src/com/jsyn/unitgen/Pan.java64
-rw-r--r--src/com/jsyn/unitgen/PanControl.java61
-rw-r--r--src/com/jsyn/unitgen/ParabolicEnvelope.java110
-rw-r--r--src/com/jsyn/unitgen/PassThrough.java37
-rw-r--r--src/com/jsyn/unitgen/PeakFollower.java87
-rw-r--r--src/com/jsyn/unitgen/PhaseShifter.java90
-rw-r--r--src/com/jsyn/unitgen/PinkNoise.java128
-rw-r--r--src/com/jsyn/unitgen/PitchDetector.java115
-rw-r--r--src/com/jsyn/unitgen/PowerOfTwo.java99
-rw-r--r--src/com/jsyn/unitgen/PulseOscillator.java59
-rw-r--r--src/com/jsyn/unitgen/PulseOscillatorBL.java57
-rw-r--r--src/com/jsyn/unitgen/RaisedCosineEnvelope.java73
-rw-r--r--src/com/jsyn/unitgen/RangeConverter.java50
-rw-r--r--src/com/jsyn/unitgen/RectangularWindow.java39
-rw-r--r--src/com/jsyn/unitgen/RedNoise.java80
-rw-r--r--src/com/jsyn/unitgen/SampleGrainFarm.java69
-rw-r--r--src/com/jsyn/unitgen/SampleGrainSource.java69
-rw-r--r--src/com/jsyn/unitgen/SawtoothOscillator.java47
-rw-r--r--src/com/jsyn/unitgen/SawtoothOscillatorBL.java65
-rw-r--r--src/com/jsyn/unitgen/SawtoothOscillatorDPW.java76
-rw-r--r--src/com/jsyn/unitgen/SchmidtTrigger.java83
-rw-r--r--src/com/jsyn/unitgen/Select.java57
-rw-r--r--src/com/jsyn/unitgen/SequentialDataReader.java38
-rw-r--r--src/com/jsyn/unitgen/SequentialDataWriter.java34
-rw-r--r--src/com/jsyn/unitgen/SineOscillator.java85
-rw-r--r--src/com/jsyn/unitgen/SineOscillatorPhaseModulated.java74
-rw-r--r--src/com/jsyn/unitgen/SpectralFFT.java130
-rw-r--r--src/com/jsyn/unitgen/SpectralFilter.java130
-rw-r--r--src/com/jsyn/unitgen/SpectralIFFT.java92
-rw-r--r--src/com/jsyn/unitgen/SpectralProcessor.java73
-rw-r--r--src/com/jsyn/unitgen/SquareOscillator.java49
-rw-r--r--src/com/jsyn/unitgen/SquareOscillatorBL.java47
-rw-r--r--src/com/jsyn/unitgen/StereoStreamWriter.java52
-rw-r--r--src/com/jsyn/unitgen/StochasticGrainScheduler.java43
-rw-r--r--src/com/jsyn/unitgen/Subtract.java42
-rw-r--r--src/com/jsyn/unitgen/TriangleOscillator.java59
-rw-r--r--src/com/jsyn/unitgen/TunableFilter.java41
-rw-r--r--src/com/jsyn/unitgen/TwoInDualOut.java56
-rw-r--r--src/com/jsyn/unitgen/UnitBinaryOperator.java41
-rw-r--r--src/com/jsyn/unitgen/UnitFilter.java47
-rw-r--r--src/com/jsyn/unitgen/UnitGate.java54
-rw-r--r--src/com/jsyn/unitgen/UnitGenerator.java325
-rw-r--r--src/com/jsyn/unitgen/UnitOscillator.java93
-rw-r--r--src/com/jsyn/unitgen/UnitSink.java43
-rw-r--r--src/com/jsyn/unitgen/UnitSource.java30
-rw-r--r--src/com/jsyn/unitgen/UnitStreamWriter.java43
-rw-r--r--src/com/jsyn/unitgen/UnitVoice.java59
-rw-r--r--src/com/jsyn/unitgen/Unzipper.java47
-rw-r--r--src/com/jsyn/unitgen/VariableRateDataReader.java29
-rw-r--r--src/com/jsyn/unitgen/VariableRateMonoReader.java113
-rw-r--r--src/com/jsyn/unitgen/VariableRateStereoReader.java113
-rw-r--r--src/com/jsyn/unitgen/WhiteNoise.java56
-rw-r--r--src/com/jsyn/unitgen/ZeroCrossingCounter.java61
-rw-r--r--src/com/jsyn/util/AudioSampleLoader.java42
-rw-r--r--src/com/jsyn/util/AudioStreamReader.java85
-rw-r--r--src/com/jsyn/util/AutoCorrelator.java329
-rw-r--r--src/com/jsyn/util/Instrument.java38
-rw-r--r--src/com/jsyn/util/InstrumentLibrary.java32
-rw-r--r--src/com/jsyn/util/JavaSoundSampleLoader.java150
-rw-r--r--src/com/jsyn/util/JavaTools.java59
-rw-r--r--src/com/jsyn/util/PolyphonicInstrument.java155
-rw-r--r--src/com/jsyn/util/PseudoRandom.java89
-rw-r--r--src/com/jsyn/util/RecursiveSequenceGenerator.java214
-rw-r--r--src/com/jsyn/util/SampleLoader.java229
-rw-r--r--src/com/jsyn/util/SignalCorrelator.java48
-rw-r--r--src/com/jsyn/util/StreamingThread.java121
-rw-r--r--src/com/jsyn/util/TransportListener.java31
-rw-r--r--src/com/jsyn/util/TransportModel.java67
-rw-r--r--src/com/jsyn/util/VoiceAllocator.java238
-rw-r--r--src/com/jsyn/util/VoiceDescription.java68
-rw-r--r--src/com/jsyn/util/WaveFileWriter.java293
-rw-r--r--src/com/jsyn/util/WaveRecorder.java134
-rw-r--r--src/com/jsyn/util/soundfile/AIFFFileParser.java226
-rw-r--r--src/com/jsyn/util/soundfile/AudioFileParser.java129
-rw-r--r--src/com/jsyn/util/soundfile/ChunkHandler.java49
-rw-r--r--src/com/jsyn/util/soundfile/CustomSampleLoader.java60
-rw-r--r--src/com/jsyn/util/soundfile/IFFParser.java307
-rw-r--r--src/com/jsyn/util/soundfile/WAVEFileParser.java334
-rw-r--r--src/com/softsynth/math/AudioMath.java60
-rw-r--r--src/com/softsynth/math/ChebyshevPolynomial.java45
-rw-r--r--src/com/softsynth/math/FourierMath.java254
-rw-r--r--src/com/softsynth/math/JustRatio.java47
-rw-r--r--src/com/softsynth/math/Polynomial.java253
-rw-r--r--src/com/softsynth/math/PolynomialTableData.java64
-rw-r--r--src/com/softsynth/math/PrimeFactors.java244
-rw-r--r--src/com/softsynth/shared/time/ScheduledCommand.java21
-rw-r--r--src/com/softsynth/shared/time/ScheduledQueue.java81
-rw-r--r--src/com/softsynth/shared/time/TimeStamp.java56
-rw-r--r--src/com/softsynth/util/AlertBox.java84
-rw-r--r--src/com/softsynth/util/FileSearch.java193
-rw-r--r--src/com/softsynth/util/IndentingWriter.java94
-rw-r--r--src/com/softsynth/util/InsetPanel.java78
-rw-r--r--src/com/softsynth/util/Logger.java133
-rw-r--r--src/com/softsynth/util/NumericOutput.java187
-rw-r--r--src/com/softsynth/util/RandomOutputStream.java60
-rw-r--r--src/com/softsynth/util/Semaphore.java36
-rw-r--r--src/com/softsynth/util/TextOutput.java186
-rw-r--r--src/com/softsynth/util/XMLListener.java36
-rw-r--r--src/com/softsynth/util/XMLPrinter.java102
-rw-r--r--src/com/softsynth/util/XMLReader.java328
-rw-r--r--src/com/softsynth/util/XMLTools.java122
-rw-r--r--src/com/softsynth/util/XMLWriter.java122
-rw-r--r--tests/com/jsyn/SynthTestSuite.java74
-rw-r--r--tests/com/jsyn/benchmarks/BenchJSyn.java186
-rw-r--r--tests/com/jsyn/data/TestShortSample.java78
-rw-r--r--tests/com/jsyn/engine/TestAudioOutput.java86
-rw-r--r--tests/com/jsyn/engine/TestDevices.java69
-rw-r--r--tests/com/jsyn/engine/TestEngine.java180
-rw-r--r--tests/com/jsyn/engine/TestFifo.java219
-rw-r--r--tests/com/jsyn/engine/TestWaveFileReadWrite.java107
-rw-r--r--tests/com/jsyn/examples/AudioPassThrough.java71
-rw-r--r--tests/com/jsyn/examples/ChebyshevSong.java181
-rw-r--r--tests/com/jsyn/examples/CircuitTester.java109
-rw-r--r--tests/com/jsyn/examples/CustomCubeUnit.java48
-rw-r--r--tests/com/jsyn/examples/DualOscilloscope.java164
-rw-r--r--tests/com/jsyn/examples/EditEnvelope1.java148
-rw-r--r--tests/com/jsyn/examples/FFTPassthrough.java100
-rw-r--r--tests/com/jsyn/examples/GoogleWaveOscillator.java73
-rw-r--r--tests/com/jsyn/examples/HearDAHDSR.java125
-rw-r--r--tests/com/jsyn/examples/HearMoogFilter.java196
-rw-r--r--tests/com/jsyn/examples/HearSinePM.java129
-rw-r--r--tests/com/jsyn/examples/HearSpectralFilter.java206
-rw-r--r--tests/com/jsyn/examples/ListAudioDevices.java46
-rw-r--r--tests/com/jsyn/examples/LongEcho.java125
-rw-r--r--tests/com/jsyn/examples/MonoPassThrough.java66
-rw-r--r--tests/com/jsyn/examples/NotesToTone.java214
-rw-r--r--tests/com/jsyn/examples/PlayChords.java188
-rw-r--r--tests/com/jsyn/examples/PlayCustomUnit.java73
-rw-r--r--tests/com/jsyn/examples/PlayFunction.java91
-rw-r--r--tests/com/jsyn/examples/PlayGrains.java212
-rw-r--r--tests/com/jsyn/examples/PlayNotes.java103
-rw-r--r--tests/com/jsyn/examples/PlayPartials.java110
-rw-r--r--tests/com/jsyn/examples/PlaySample.java121
-rw-r--r--tests/com/jsyn/examples/PlaySampleCrossfade.java183
-rw-r--r--tests/com/jsyn/examples/PlaySegmentedEnvelope.java151
-rw-r--r--tests/com/jsyn/examples/PlaySegmentedEnvelopeCallback.java114
-rw-r--r--tests/com/jsyn/examples/PlaySequence.java85
-rw-r--r--tests/com/jsyn/examples/PlayTone.java80
-rw-r--r--tests/com/jsyn/examples/RecordSineSweep.java136
-rw-r--r--tests/com/jsyn/examples/SampleHoldNoteBlaster.java153
-rw-r--r--tests/com/jsyn/examples/SawFaders.java104
-rw-r--r--tests/com/jsyn/examples/SeeGoogleWave.java111
-rw-r--r--tests/com/jsyn/examples/SeeOscillators.java183
-rw-r--r--tests/com/jsyn/examples/ShowWaves.java121
-rw-r--r--tests/com/jsyn/examples/SwarmOfOscillators.java146
-rw-r--r--tests/com/jsyn/examples/UseMidiKeyboard.java198
-rw-r--r--tests/com/jsyn/examples/WindCircuit.java90
-rw-r--r--tests/com/jsyn/ports/TestQueuedDataPort.java492
-rw-r--r--tests/com/jsyn/ports/TestSequentialData.java50
-rw-r--r--tests/com/jsyn/ports/TestSet.java96
-rw-r--r--tests/com/jsyn/research/BenchMultiThreading.java143
-rw-r--r--tests/com/jsyn/research/RecordVariousRamps.java183
-rw-r--r--tests/com/jsyn/swing/TestRangeModels.java60
-rw-r--r--tests/com/jsyn/unitgen/CalibrateMoogFilter.java141
-rw-r--r--tests/com/jsyn/unitgen/EnablingGate.java51
-rw-r--r--tests/com/jsyn/unitgen/NonRealTimeTestCase.java48
-rw-r--r--tests/com/jsyn/unitgen/RecordMoogFilter.java153
-rw-r--r--tests/com/jsyn/unitgen/TestConnections.java113
-rw-r--r--tests/com/jsyn/unitgen/TestDelay.java73
-rw-r--r--tests/com/jsyn/unitgen/TestEnable.java78
-rw-r--r--tests/com/jsyn/unitgen/TestEnvelopeAttackDecay.java126
-rw-r--r--tests/com/jsyn/unitgen/TestEnvelopeDAHDSR.java339
-rw-r--r--tests/com/jsyn/unitgen/TestFunction.java78
-rw-r--r--tests/com/jsyn/unitgen/TestMath.java392
-rw-r--r--tests/com/jsyn/unitgen/TestRamps.java196
-rw-r--r--tests/com/jsyn/unitgen/TestUnitGate.java80
-rw-r--r--tests/com/jsyn/util/DebugSampleLoader.java138
-rw-r--r--tests/com/jsyn/util/TestFFT.java98
-rw-r--r--tests/com/jsyn/util/TestPseudoRandom.java82
-rw-r--r--tests/com/jsyn/util/TestVoiceAllocator.java111
339 files changed, 35446 insertions, 0 deletions
diff --git a/NOTICE.txt b/NOTICE.txt
new file mode 100644
index 0000000..a1dbde1
--- /dev/null
+++ b/NOTICE.txt
@@ -0,0 +1,12 @@
+This is the NOTICE file for JSyn as described by the Apache License Version 2.0.
+
+JSyn - Copyright 1997-2014 Mobileer Inc
+
+JSyn was released under Apache License Version 2.0 by Mobileer Inc in December 2014.
+
+Phil Burk was the primary developer of JSyn.
+
+Portions of this software were developed by Nick Didkovsky of Mobileer.
+Nick was also a major tester and user of JSyn.
+
+Lisa Tolenti converted many of the unit generators from 'C' to Java in 2009.
diff --git a/README.txt b/README.txt
new file mode 100644
index 0000000..edb93d0
--- /dev/null
+++ b/README.txt
@@ -0,0 +1,23 @@
+README for JSyn
+
+JSyn - Copyright 1997-2014 Mobileer Inc
+
+JSyn was released under Apache License Version 2.0 in December 2014.
+
+JSyn is a modular audio synthesizer for Java by Phil Burk.
+
+You can use JSyn to create unit generators, such as oscillators, filters,
+and envelopes. Units can be connected together and controlled
+in real-time from a Java program.
+
+The JSyn source code is available at:
+
+ https://github.com/philburk/jsyn
+
+More information about JSyn, including documentation, is at:
+
+ http://www.softsynth.com/jsyn/
+
+Pre-compiled JSyn JAR files are at:
+
+ http://www.softsynth.com/jsyn/developers/download.php
diff --git a/libs/jportaudio.jar b/libs/jportaudio.jar
new file mode 100644
index 0000000..f58df5a
--- /dev/null
+++ b/libs/jportaudio.jar
Binary files differ
diff --git a/licenses/license_part1.txt b/licenses/license_part1.txt
new file mode 100644
index 0000000..33662f5
--- /dev/null
+++ b/licenses/license_part1.txt
@@ -0,0 +1 @@
+/*
diff --git a/licenses/license_part2.txt b/licenses/license_part2.txt
new file mode 100644
index 0000000..ca476b0
--- /dev/null
+++ b/licenses/license_part2.txt
@@ -0,0 +1,13 @@
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
diff --git a/licenses/license_part3.txt b/licenses/license_part3.txt
new file mode 100644
index 0000000..d484d6d
--- /dev/null
+++ b/licenses/license_part3.txt
@@ -0,0 +1,4 @@
+/*
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ */
+ \ No newline at end of file
diff --git a/misc/android-formatting.xml b/misc/android-formatting.xml
new file mode 100644
index 0000000..9c2af85
--- /dev/null
+++ b/misc/android-formatting.xml
@@ -0,0 +1,252 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<profiles version="10">
+<profile name="Android" version="10">
+<setting id="org.eclipse.jdt.core.formatter.align_type_members_on_columns" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_assignment" value="0"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_binary_expression" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_compact_if" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_conditional_expression" value="80"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_enum_constants" value="0"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_multiple_fields" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_after_imports" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_after_package" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_field" value="0"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration" value="0"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_imports" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_member_type" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_method" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_package" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_array_initializer" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_block" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_block_in_case" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_enum_constant" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_method_declaration" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_switch" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_type_declaration" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.clear_blank_lines" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.format_comments" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.format_header" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.format_html" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.format_source_code" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.indent_parameter_description" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.indent_root_tags" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.line_length" value="100"/>
+<setting id="org.eclipse.jdt.core.formatter.compact_else_if" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.continuation_indentation" value="2"/>
+<setting id="org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer" value="2"/>
+<setting id="org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_empty_lines" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_statements_compare_to_block" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_statements_compare_to_body" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.indentation.size" value="4"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_binary_operator" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_ellipsis" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_unary_operator" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_binary_operator" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_ellipsis" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_semicolon" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_unary_operator" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.lineSplit" value="100"/>
+<setting id="org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body" value="0"/>
+<setting id="org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.tabulation.char" value="space"/>
+<setting id="org.eclipse.jdt.core.formatter.tabulation.size" value="4"/>
+<setting id="org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.join_wrapped_lines" value="false"/>
+</profile>
+</profiles>
diff --git a/scripts/addlicenses.sh b/scripts/addlicenses.sh
new file mode 100755
index 0000000..112dfff
--- /dev/null
+++ b/scripts/addlicenses.sh
@@ -0,0 +1,82 @@
+#!/bin/bash
+echo "Prepend Apache Licenses to JSyn source."
+
+
+file_year=0
+files_missed=0
+files_processed=0
+license_dir=$HOME/Documents/workspace/JSyn/licenses
+
+function prepend_license_file {
+ filename=$1
+ year=$2
+ add_author=0
+ if [ $year -eq 0 ]; then
+ year=2009
+ add_author=1
+ fi
+ grep "Licensed under the Apache License" $filename > /dev/null
+ if [ $? -eq 1 ]; then
+ mv $filename temp2.txt
+ cat $license_dir/license_part1.txt >temp1.txt
+ echo " * Copyright $year Phil Burk, Mobileer Inc" >> temp1.txt
+ cat $license_dir/license_part2.txt >> temp1.txt
+ if [ $add_author -eq 1 ]; then
+ cat $license_dir/license_part3.txt >> temp1.txt
+ fi
+ cat temp1.txt temp2.txt > $filename
+ rm temp1.txt
+ rm temp2.txt
+ (( files_processed += 1 ))
+ fi
+}
+
+function process_file_year {
+ filename=$1
+ year=$2
+ grep "(C) $year" $filename > /dev/null
+ if [ $? -eq 0 ]; then
+ # echo "prepend $year license file"
+ prepend_license_file $filename $year
+ file_year=$year
+ fi
+}
+
+function process_java_file {
+ filename=$1
+ # echo "process $filename"
+ file_year=0
+ N=1997
+ while [[ $N -lt 2015 && $file_year -eq 0 ]]; do
+ process_file_year $filename $N
+ let N=N+1
+ done
+ if [ $file_year -eq 0 ]; then
+ (( files_missed += 1 ))
+ echo "FILE $1 did not have a copyright."
+ prepend_license_file $filename 0
+ fi
+}
+
+function process_directory {
+ for filename in *.java; do
+ if [ -e $filename ]; then
+ process_java_file $filename
+ fi
+ done
+ # now scan subdirectories
+ for filename in *; do
+ if [ -d "$filename" ]; then
+ echo "$filename is a directory"
+ cd $filename
+ pwd
+ process_directory
+ cd ../
+ fi
+ done
+}
+
+process_directory
+# prepend_license_file src/com/jsyn/JSyn.java 2007
+echo "$files_missed files missed"
+echo "$files_processed files processed"
diff --git a/src/com/jsyn/JSyn.java b/src/com/jsyn/JSyn.java
new file mode 100644
index 0000000..c6e35b2
--- /dev/null
+++ b/src/com/jsyn/JSyn.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn;
+
+import java.sql.Date;
+import java.util.GregorianCalendar;
+
+import com.jsyn.devices.AudioDeviceManager;
+import com.jsyn.engine.SynthesisEngine;
+
+/**
+ * JSyn Synthesizer for Java. Use this factory class to create a synthesizer. This code demonstrates
+ * how to start playing a sine wave: <code><pre>
+ // Create a context for the synthesizer.
+ synth = JSyn.createSynthesizer();
+
+ // Start synthesizer using default stereo output at 44100 Hz.
+ synth.start();
+
+ // Add a tone generator.
+ synth.add( osc = new SineOscillator() );
+ // Add a stereo audio output unit.
+ synth.add( lineOut = new LineOut() );
+
+ // Connect the oscillator to both channels of the output.
+ osc.output.connect( 0, lineOut.input, 0 );
+ osc.output.connect( 0, lineOut.input, 1 );
+
+ // Set the frequency and amplitude for the sine wave.
+ osc.frequency.set( 345.0 );
+ osc.amplitude.set( 0.6 );
+
+ // We only need to start the LineOut. It will pull data from the oscillator.
+ lineOut.start();
+ </pre></code>
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class JSyn {
+ // Update these for every release.
+ private final static int VERSION_MAJOR = 16;
+ private final static int VERSION_MINOR = 7;
+ private final static int VERSION_REVISION = 4;
+ public final static int BUILD_NUMBER = 457;
+ private final static long BUILD_TIME = new GregorianCalendar(2014, GregorianCalendar.DECEMBER,
+ 25).getTime().getTime();
+
+ public final static String VERSION = VERSION_MAJOR + "." + VERSION_MINOR + "."
+ + VERSION_REVISION;
+ public final static int VERSION_CODE = (VERSION_MAJOR << 16) + (VERSION_MINOR << 8)
+ + VERSION_REVISION;
+ public final static String VERSION_TEXT = "V" + VERSION + " (build " + BUILD_NUMBER + ", "
+ + (new Date(BUILD_TIME)) + ")";
+
+ public static Synthesizer createSynthesizer() {
+ return new SynthesisEngine();
+ }
+
+ public static Synthesizer createSynthesizer(AudioDeviceManager audioDeviceManager) {
+ return new SynthesisEngine(audioDeviceManager);
+ }
+}
diff --git a/src/com/jsyn/Synthesizer.java b/src/com/jsyn/Synthesizer.java
new file mode 100644
index 0000000..dbf75df
--- /dev/null
+++ b/src/com/jsyn/Synthesizer.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn;
+
+import com.jsyn.devices.AudioDeviceManager;
+import com.jsyn.unitgen.UnitGenerator;
+import com.softsynth.shared.time.ScheduledCommand;
+import com.softsynth.shared.time.TimeStamp;
+
+/**
+ * A synthesizer used by JSyn to generate audio. The synthesizer executes a network of unit
+ * generators to create an audio signal.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public interface Synthesizer {
+
+ public final static int FRAMES_PER_BLOCK = 8;
+
+ /**
+ * Starts a background thread that generates audio using the default frame rate of 44100 and two
+ * stereo output channels.
+ */
+ public void start();
+
+ /**
+ * Starts a background thread that generates audio using the specified frame rate and two stereo
+ * output channels.
+ *
+ * @param frameRate in Hertz
+ */
+ public void start(int frameRate);
+
+ /**
+ * Starts the synthesizer using specific audio devices.
+ *
+ * @param frameRate in Hertz
+ * @param inputDeviceID obtained from an {@link AudioDeviceManager} or pass
+ * AudioDeviceManager.USE_DEFAULT_DEVICE
+ * @param numInputChannels 1 for mono, 2 for stereo, etcetera
+ * @param ouputDeviceID obtained from an AudioDeviceManager or pass
+ * AudioDeviceManager.USE_DEFAULT_DEVICE
+ * @param numOutputChannels 1 for mono, 2 for stereo, etcetera
+ */
+ public void start(int frameRate, int inputDeviceID, int numInputChannels, int ouputDeviceID,
+ int numOutputChannels);
+
+ /** @return JSyn version as a string */
+ public String getVersion();
+
+ /** @return version as an integer that always increases */
+ public int getVersionCode();
+
+ /** Stops the background thread that generates the audio. */
+ public void stop();
+
+ /**
+ * An AudioDeviceManager is an interface to audio hardware. It might be implemented using
+ * JavaSound or a wrapper around PortAudio.
+ *
+ * @return audio device manager being used by the synthesizer.
+ */
+ public AudioDeviceManager getAudioDeviceManager();
+
+ /** @return the frame rate in samples per second */
+ public int getFrameRate();
+
+ /**
+ * Add a unit generator to the synthesizer so it can be played. This is required before starting
+ * or connecting a unit generator into a network.
+ *
+ * @param ugen a unit generator to be executed by the synthesizer
+ */
+ public void add(UnitGenerator ugen);
+
+ /** Removes a unit generator added using add(). */
+ public void remove(UnitGenerator ugen);
+
+ /** @return the current audio time in seconds */
+ public double getCurrentTime();
+
+ /**
+ * Start a unit generator at the specified time. This is not needed if a unit generator's output
+ * is connected to other units. Typically you only need to start units that have no outputs, for
+ * example LineOut.
+ */
+ public void startUnit(UnitGenerator unit, double time);
+
+ public void startUnit(UnitGenerator unit, TimeStamp timeStamp);
+
+ /** Start a unit generator now. */
+ public void startUnit(UnitGenerator unit);
+
+ public void stopUnit(UnitGenerator unit, double time);
+
+ public void stopUnit(UnitGenerator unit, TimeStamp timeStamp);
+
+ public void stopUnit(UnitGenerator unit);
+
+ /**
+ * Sleep until the specified audio time is reached. In non-real-time mode, this will enable the
+ * synthesizer to run.
+ */
+ public void sleepUntil(double time) throws InterruptedException;
+
+ /**
+ * Sleep for the specified audio time duration. In non-real-time mode, this will enable the
+ * synthesizer to run.
+ */
+ public void sleepFor(double duration) throws InterruptedException;
+
+ /**
+ * If set true then the synthesizer will generate audio in real-time. Set it true for live
+ * audio. If false then JSyn will run in non-real-time mode. This can be used to generate audio
+ * to be written to a file. The default is true.
+ *
+ * @param realTime
+ */
+ public void setRealTime(boolean realTime);
+
+ /** Is JSyn running in real-time mode? */
+ public boolean isRealTime();
+
+ /** Create a TimeStamp using the current audio time. */
+ public TimeStamp createTimeStamp();
+
+ /** @return the current CPU usage as a fraction between 0.0 and 1.0 */
+ public double getUsage();
+
+ /** @return inverse of frameRate, to avoid expensive divides */
+ public double getFramePeriod();
+
+ /**
+ * This count is not reset if you stop and restart.
+ *
+ * @return number of frames synthesized
+ */
+ public long getFrameCount();
+
+ /** Queue a command to be processed at a specific time in the background audio thread. */
+ public void scheduleCommand(TimeStamp timeStamp, ScheduledCommand command);
+
+ /** Queue a command to be processed at a specific time in the background audio thread. */
+ public void scheduleCommand(double time, ScheduledCommand command);
+
+ /** Queue a command to be processed as soon as possible in the background audio thread. */
+ public void queueCommand(ScheduledCommand command);
+
+ /**
+ * @return true if the Synthesizer has been started
+ */
+ public boolean isRunning();
+
+ /**
+ * Add a task that will get run on the Audio Thread before it generates a new block of Audio.
+ * This task must be very quick and should not perform any blocking operations. If you are not
+ * certain that you need an Audio rate task then don't use this.
+ *
+ * @param blockTask
+ */
+ public void addAudioTask(Runnable task);
+
+ public void removeAudioTask(Runnable task);
+
+}
diff --git a/src/com/jsyn/apps/AboutJSyn.java b/src/com/jsyn/apps/AboutJSyn.java
new file mode 100644
index 0000000..e6c1fbd
--- /dev/null
+++ b/src/com/jsyn/apps/AboutJSyn.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 1997 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.apps;
+
+import java.awt.GridLayout;
+
+import javax.swing.JApplet;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.SwingConstants;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import com.jsyn.swing.JAppletFrame;
+import com.jsyn.swing.PortControllerFactory;
+import com.jsyn.unitgen.LineOut;
+import com.jsyn.unitgen.LinearRamp;
+import com.jsyn.unitgen.SineOscillator;
+import com.jsyn.unitgen.UnitOscillator;
+
+/**
+ * Show the version of JSyn and play some sine waves. This program will be run if you double click
+ * the JSyn jar file.
+ *
+ * @author Phil Burk (C) 2011 Mobileer Inc
+ */
+public class AboutJSyn extends JApplet {
+ private static final long serialVersionUID = -2704222221111608377L;
+ private Synthesizer synth;
+ private UnitOscillator osc1;
+ private UnitOscillator osc2;
+ private LinearRamp lag;
+ private LineOut lineOut;
+
+ @Override
+ public void init() {
+ synth = JSyn.createSynthesizer();
+
+ // Add a tone generator.
+ synth.add(osc1 = new SineOscillator());
+ synth.add(osc2 = new SineOscillator());
+ // Add a lag to smooth out amplitude changes and avoid pops.
+ synth.add(lag = new LinearRamp());
+ // Add an output mixer.
+ synth.add(lineOut = new LineOut());
+ // Connect the oscillator to the output.
+ osc1.output.connect(0, lineOut.input, 0);
+ osc2.output.connect(0, lineOut.input, 1);
+
+ // Arrange the faders in a stack.
+ setLayout(new GridLayout(0, 1));
+
+ JPanel infoPanel = new JPanel();
+ infoPanel.setLayout(new GridLayout(0, 1));
+ infoPanel.add(new JLabel("About: " + synth, SwingConstants.CENTER));
+ infoPanel.add(new JLabel("From: http://www.softsynth.com/", SwingConstants.CENTER));
+ infoPanel.add(new JLabel("(C) 1997-2011 Mobileer Inc", SwingConstants.CENTER));
+ add(infoPanel);
+
+ // Set the minimum, current and maximum values for the port.
+ lag.output.connect(osc1.amplitude);
+ lag.output.connect(osc2.amplitude);
+ lag.input.setup(0.001, 0.5, 1.0);
+ lag.time.set(0.1);
+ lag.input.setName("Amplitude");
+ add(PortControllerFactory.createExponentialPortSlider(lag.input));
+
+ osc1.frequency.setup(50.0, 300.0, 3000.0);
+ osc1.frequency.setName("Frequency (Left)");
+ add(PortControllerFactory.createExponentialPortSlider(osc1.frequency));
+ osc2.frequency.setup(50.0, 302.0, 3000.0);
+ osc2.frequency.setName("Frequency (Right)");
+ add(PortControllerFactory.createExponentialPortSlider(osc2.frequency));
+ validate();
+ }
+
+ @Override
+ public void start() {
+ // Start synthesizer using default stereo output at 44100 Hz.
+ synth.start();
+ // We only need to start the LineOut. It will pull data from the
+ // oscillator.
+ lineOut.start();
+ }
+
+ @Override
+ public void stop() {
+ synth.stop();
+ }
+
+ /* Can be run as either an application or as an applet. */
+ public static void main(String args[]) {
+ AboutJSyn applet = new AboutJSyn();
+ JAppletFrame frame = new JAppletFrame("About JSyn", applet);
+ frame.setSize(440, 300);
+ frame.setVisible(true);
+ frame.test();
+ }
+
+}
diff --git a/src/com/jsyn/apps/InstrumentTester.java b/src/com/jsyn/apps/InstrumentTester.java
new file mode 100644
index 0000000..4186703
--- /dev/null
+++ b/src/com/jsyn/apps/InstrumentTester.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2012 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.apps;
+
+import java.awt.BorderLayout;
+
+import javax.swing.JApplet;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import com.jsyn.instruments.JSynInstrumentLibrary;
+import com.jsyn.swing.InstrumentBrowser;
+import com.jsyn.swing.JAppletFrame;
+import com.jsyn.swing.PresetSelectionListener;
+import com.jsyn.swing.SoundTweaker;
+import com.jsyn.unitgen.LineOut;
+import com.jsyn.unitgen.UnitSource;
+import com.jsyn.unitgen.UnitVoice;
+import com.jsyn.util.PolyphonicInstrument;
+import com.jsyn.util.VoiceDescription;
+
+/**
+ * Let the user select an instrument using the InstrumentBrowser and play them using the ASCII
+ * keyboard. Sound parameters can be tweaked using faders.
+ *
+ * @author Phil Burk (C) 2012 Mobileer Inc
+ */
+public class InstrumentTester extends JApplet {
+ private static final long serialVersionUID = -2704222221111608377L;
+ private Synthesizer synth;
+ private LineOut lineOut;
+ private SoundTweaker tweaker;
+
+ @Override
+ public void init() {
+ setLayout(new BorderLayout());
+
+ synth = JSyn.createSynthesizer();
+ synth.add(lineOut = new LineOut());
+
+ InstrumentBrowser browser = new InstrumentBrowser(new JSynInstrumentLibrary());
+ browser.addPresetSelectionListener(new PresetSelectionListener() {
+
+ @Override
+ public void presetSelected(VoiceDescription voiceDescription, int presetIndex) {
+ UnitVoice[] voices = new UnitVoice[8];
+ for (int i = 0; i < voices.length; i++) {
+ voices[i] = voiceDescription.createUnitVoice();
+ }
+ PolyphonicInstrument instrument = new PolyphonicInstrument(voices);
+ synth.add(instrument);
+ instrument.usePreset(presetIndex, synth.createTimeStamp());
+ String title = voiceDescription.getVoiceClassName() + ": "
+ + voiceDescription.getPresetNames()[presetIndex];
+ useSource(instrument, title);
+ }
+ });
+ add(browser, BorderLayout.NORTH);
+
+ validate();
+ }
+
+ private void useSource(UnitSource voice, String title) {
+
+ lineOut.input.disconnectAll(0);
+ lineOut.input.disconnectAll(1);
+
+ // Connect the source to both left and right output.
+ voice.getOutput().connect(0, lineOut.input, 0);
+ voice.getOutput().connect(0, lineOut.input, 1);
+
+ if (tweaker != null) {
+ remove(tweaker);
+ }
+ try {
+ if (synth.isRunning()) {
+ synth.sleepFor(0.1);
+ }
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ tweaker = new SoundTweaker(synth, title, voice);
+ add(tweaker, BorderLayout.CENTER);
+ validate();
+ }
+
+ @Override
+ public void start() {
+ // Start synthesizer using default stereo output at 44100 Hz.
+ synth.start();
+ // We only need to start the LineOut. It will pull data from the
+ // oscillator.
+ lineOut.start();
+ }
+
+ @Override
+ public void stop() {
+ synth.stop();
+ }
+
+ /* Can be run as either an application or as an applet. */
+ public static void main(String args[]) {
+ InstrumentTester applet = new InstrumentTester();
+ JAppletFrame frame = new JAppletFrame("InstrumentTester", applet);
+ frame.setSize(600, 800);
+ frame.setVisible(true);
+ frame.test();
+ }
+
+}
diff --git a/src/com/jsyn/data/AudioSample.java b/src/com/jsyn/data/AudioSample.java
new file mode 100644
index 0000000..42d8501
--- /dev/null
+++ b/src/com/jsyn/data/AudioSample.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.data;
+
+import java.util.ArrayList;
+
+/**
+ * Base class for FloatSample and ShortSample.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public abstract class AudioSample extends SequentialDataCommon {
+ protected int numFrames;
+ protected int channelsPerFrame;
+ private double frameRate;
+ private double pitch;
+ private ArrayList<SampleMarker> markers;
+
+ public abstract void allocate(int numFrames, int channelsPerFrame);
+
+ @Override
+ public double getRateScaler(int index, double synthesisRate) {
+ return 1.0;
+ }
+
+ public double getFrameRate() {
+ return frameRate;
+ }
+
+ public void setFrameRate(double f) {
+ this.frameRate = f;
+ }
+
+ @Override
+ public int getNumFrames() {
+ return numFrames;
+ }
+
+ @Override
+ public int getChannelsPerFrame() {
+ return channelsPerFrame;
+ }
+
+ public void setChannelsPerFrame(int channelsPerFrame) {
+ this.channelsPerFrame = channelsPerFrame;
+ }
+
+ /**
+ * Set the recorded pitch as a fractional MIDI semitone value where 60 is Middle C.
+ *
+ * @param pitch
+ */
+ public void setPitch(double pitch) {
+ this.pitch = pitch;
+ }
+
+ public double getPitch() {
+ return pitch;
+ }
+
+ public int getMarkerCount() {
+ if (markers == null)
+ return 0;
+ else
+ return markers.size();
+ }
+
+ public SampleMarker getMarker(int index) {
+ if (markers == null)
+ return null;
+ else
+ return markers.get(index);
+ }
+
+ /**
+ * Add a marker that will be stored sorted by position. This is normally used internally by the
+ * SampleLoader.
+ *
+ * @param marker
+ */
+ public void addMarker(SampleMarker marker) {
+ if (markers == null)
+ markers = new ArrayList<SampleMarker>();
+ int idx = markers.size();
+ for (int k = 0; k < markers.size(); k++) {
+ SampleMarker cue = markers.get(k);
+ if (cue.position > marker.position) {
+ idx = k;
+ break;
+ }
+ }
+ markers.add(idx, marker);
+ }
+}
diff --git a/src/com/jsyn/data/DoubleTable.java b/src/com/jsyn/data/DoubleTable.java
new file mode 100644
index 0000000..0a34a95
--- /dev/null
+++ b/src/com/jsyn/data/DoubleTable.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.data;
+
+import com.jsyn.exceptions.ChannelMismatchException;
+
+/**
+ * Evaluate a Function by interpolating between the values in a table. This can be used for
+ * wavetable lookup or waveshaping.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class DoubleTable implements Function {
+ private double[] table;
+
+ public DoubleTable(int numFrames) {
+ allocate(numFrames);
+ }
+
+ public DoubleTable(double[] data) {
+ allocate(data.length);
+ write(data);
+ }
+
+ public DoubleTable(ShortSample shortSample) {
+ if (shortSample.getChannelsPerFrame() != 1) {
+ throw new ChannelMismatchException("DoubleTable can only be built from mono samples.");
+ }
+ short[] buffer = new short[256];
+ int framesLeft = shortSample.getNumFrames();
+ allocate(framesLeft);
+ int cursor = 0;
+ while (framesLeft > 0) {
+ int numTransfer = framesLeft;
+ if (numTransfer > buffer.length) {
+ numTransfer = buffer.length;
+ }
+ shortSample.read(cursor, buffer, 0, numTransfer);
+ write(cursor, buffer, 0, numTransfer);
+ cursor += numTransfer;
+ framesLeft -= numTransfer;
+ }
+ }
+
+ public void allocate(int numFrames) {
+ table = new double[numFrames];
+ }
+
+ public void write(double[] data) {
+ write(0, data, 0, data.length);
+ }
+
+ public void write(int startFrame, short[] data, int startIndex, int numFrames) {
+ for (int i = 0; i < numFrames; i++) {
+ table[startFrame + i] = data[startIndex + i] * (1.0 / 32768.0);
+ }
+ }
+
+ public void write(int startFrame, double[] data, int startIndex, int numFrames) {
+ for (int i = 0; i < numFrames; i++) {
+ table[startFrame + i] = data[startIndex + i];
+ }
+ }
+
+ /**
+ * Treat the double array as a lookup table with a domain of -1.0 to 1.0. If the input is out of
+ * range then the output will clip to the end values.
+ *
+ * @param input
+ * @return interpolated value from table
+ */
+ @Override
+ public double evaluate(double input) {
+ double interp;
+ if (input < -1.0) {
+ interp = table[0];
+ } else if (input < 1.0) {
+ double fractionalIndex = (table.length - 1) * (input - (-1.0)) / 2.0;
+ // We don't need floor() because fractionalIndex >= 0.0
+ int index = (int) fractionalIndex;
+ double fraction = fractionalIndex - index;
+
+ double s1 = table[index];
+ double s2 = table[index + 1];
+ interp = ((s2 - s1) * fraction) + s1;
+ } else {
+ interp = table[table.length - 1];
+ }
+ return interp;
+ }
+}
diff --git a/src/com/jsyn/data/FloatSample.java b/src/com/jsyn/data/FloatSample.java
new file mode 100644
index 0000000..78c71d7
--- /dev/null
+++ b/src/com/jsyn/data/FloatSample.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.data;
+
+import com.jsyn.unitgen.FixedRateMonoReader;
+import com.jsyn.unitgen.FixedRateStereoReader;
+import com.jsyn.unitgen.VariableRateMonoReader;
+import com.jsyn.unitgen.VariableRateStereoReader;
+import com.jsyn.util.SampleLoader;
+
+/**
+ * Store multi-channel floating point audio data in an interleaved buffer. The values are stored as
+ * 32-bit floats. You can play samples using one of the readers, for example VariableRateMonoReader.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ * @see SampleLoader
+ * @see FixedRateMonoReader
+ * @see FixedRateStereoReader
+ * @see VariableRateMonoReader
+ * @see VariableRateStereoReader
+ */
+public class FloatSample extends AudioSample {
+ private float[] buffer;
+
+ public FloatSample() {
+ }
+
+ /** Constructor for mono samples. */
+ public FloatSample(int numFrames) {
+ this(numFrames, 1);
+ }
+
+ /** Constructor for mono samples with data. */
+ public FloatSample(float[] data) {
+ this(data.length, 1);
+ write(data);
+ }
+
+ /** Constructor for multi-channel samples with data. */
+ public FloatSample(float[] data, int channelsPerFrame) {
+ this(data.length / channelsPerFrame, channelsPerFrame);
+ write(data);
+ }
+
+ /**
+ * Create an silent sample with enough memory to hold the audio data. The number of sample
+ * numbers in the array will be numFrames*channelsPerFrame.
+ *
+ * @param numFrames number of sample groups. A stereo frame contains 2 samples.
+ * @param channelsPerFrame 1 for mono, 2 for stereo
+ */
+ public FloatSample(int numFrames, int channelsPerFrame) {
+ allocate(numFrames, channelsPerFrame);
+ }
+
+ /**
+ * Allocate memory to hold the audio data. The number of sample numbers in the array will be
+ * numFrames*channelsPerFrame.
+ *
+ * @param numFrames number of sample groups. A stereo frame contains 2 samples.
+ * @param channelsPerFrame 1 for mono, 2 for stereo
+ */
+ @Override
+ public void allocate(int numFrames, int channelsPerFrame) {
+ buffer = new float[numFrames * channelsPerFrame];
+ this.numFrames = numFrames;
+ this.channelsPerFrame = channelsPerFrame;
+ }
+
+ /**
+ * Note that in a stereo sample, a frame has two values.
+ *
+ * @param startFrame index of frame in the sample
+ * @param data data to be written
+ * @param startIndex index of first value in array
+ * @param numFrames
+ */
+ public void write(int startFrame, float[] data, int startIndex, int numFrames) {
+ int numSamplesToWrite = numFrames * channelsPerFrame;
+ int firstSampleIndexToWrite = startFrame * channelsPerFrame;
+ System.arraycopy(data, startIndex, buffer, firstSampleIndexToWrite, numSamplesToWrite);
+ }
+
+ /**
+ * Note that in a stereo sample, a frame has two values.
+ *
+ * @param startFrame index of frame in the sample
+ * @param data array to receive the data from the sample
+ * @param startIndex index of first location in array to start filling
+ * @param numFrames
+ */
+ public void read(int startFrame, float[] data, int startIndex, int numFrames) {
+ int numSamplesToRead = numFrames * channelsPerFrame;
+ int firstSampleIndexToRead = startFrame * channelsPerFrame;
+ System.arraycopy(buffer, firstSampleIndexToRead, data, startIndex, numSamplesToRead);
+ }
+
+ /**
+ * Write the entire array to the sample. The sample data must have already been allocated with
+ * enough room to contain the data.
+ *
+ * @param data
+ */
+ public void write(float[] data) {
+ write(0, data, 0, data.length / getChannelsPerFrame());
+ }
+
+ public void read(float[] data) {
+ read(0, data, 0, data.length / getChannelsPerFrame());
+ }
+
+ @Override
+ public double readDouble(int index) {
+ return buffer[index];
+ }
+
+ @Override
+ public void writeDouble(int index, double value) {
+ buffer[index] = (float) value;
+ }
+
+ /*
+ * @param fractionalIndex must be >=0 and < (size-1)
+ */
+ public double interpolate(double fractionalIndex) {
+ int index = (int) fractionalIndex;
+ float phase = (float) (fractionalIndex - index);
+ float source = buffer[index];
+ float target = buffer[index + 1];
+ return ((target - source) * phase) + source;
+ }
+}
diff --git a/src/com/jsyn/data/Function.java b/src/com/jsyn/data/Function.java
new file mode 100644
index 0000000..c0e6566
--- /dev/null
+++ b/src/com/jsyn/data/Function.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.data;
+
+import com.jsyn.unitgen.FunctionEvaluator;
+import com.jsyn.unitgen.FunctionOscillator;
+
+/**
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ * @see FunctionEvaluator
+ * @see FunctionOscillator
+ */
+public interface Function {
+ /**
+ * Convert an input value to an output value.
+ *
+ * @param input
+ * @return generated value
+ */
+ public double evaluate(double input);
+}
diff --git a/src/com/jsyn/data/HammingWindow.java b/src/com/jsyn/data/HammingWindow.java
new file mode 100644
index 0000000..d8e1238
--- /dev/null
+++ b/src/com/jsyn/data/HammingWindow.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.data;
+
+public class HammingWindow implements SpectralWindow {
+ private double[] data;
+
+ /** Construct a generalized Hamming Window */
+ public HammingWindow(int length, double alpha, double beta) {
+ data = new double[length];
+ double scaler = 2.0 * Math.PI / (length - 1);
+ for (int i = 0; i < length; i++) {
+ data[i] = alpha - (beta * (Math.cos(i * scaler)));
+ }
+ }
+
+ /** Traditional Hamming Window with alpha = 25/46 and beta = 21/46 */
+ public HammingWindow(int length) {
+ this(length, 25.0 / 46.0, 21.0 / 46.0);
+ }
+
+ @Override
+ public double get(int index) {
+ return data[index];
+ }
+
+}
diff --git a/src/com/jsyn/data/HannWindow.java b/src/com/jsyn/data/HannWindow.java
new file mode 100644
index 0000000..878d07c
--- /dev/null
+++ b/src/com/jsyn/data/HannWindow.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2013 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.data;
+
+import com.jsyn.unitgen.SpectralFFT;
+import com.jsyn.unitgen.SpectralIFFT;
+
+/**
+ * A HammingWindow with alpha and beta = 0.5.
+ *
+ * @author Phil Burk (C) 2013 Mobileer Inc
+ * @see SpectralWindow
+ * @see SpectralFFT
+ * @see SpectralIFFT
+ */
+public class HannWindow extends HammingWindow {
+
+ public HannWindow(int length) {
+ super(length, 0.5, 0.5);
+ }
+
+}
diff --git a/src/com/jsyn/data/SampleMarker.java b/src/com/jsyn/data/SampleMarker.java
new file mode 100644
index 0000000..d3db1d4
--- /dev/null
+++ b/src/com/jsyn/data/SampleMarker.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2012 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.data;
+
+/**
+ * A marker for an audio sample.
+ *
+ * @author (C) 2012 Phil Burk, Mobileer Inc
+ */
+
+public class SampleMarker {
+ /** Sample frame index. */
+ public int position;
+ public String name;
+ public String comment;
+}
diff --git a/src/com/jsyn/data/SegmentedEnvelope.java b/src/com/jsyn/data/SegmentedEnvelope.java
new file mode 100644
index 0000000..efdfd89
--- /dev/null
+++ b/src/com/jsyn/data/SegmentedEnvelope.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.data;
+
+import com.jsyn.unitgen.EnvelopeDAHDSR;
+import com.jsyn.unitgen.VariableRateMonoReader;
+
+/**
+ * Store an envelope as a series of line segments. Each line is described as a duration and a target
+ * value. The envelope can be played using a {@link VariableRateMonoReader}. Here is an example that
+ * generates an envelope that looks like a traditional ADSR envelope.
+ *
+ * <pre>
+ * <code>
+ * // Create an amplitude envelope and fill it with data.
+ * double[] ampData = {
+ * 0.02, 0.9, // duration,value pair 0, "attack"
+ * 0.10, 0.5, // pair 1, "decay"
+ * 0.50, 0.0 // pair 2, "release"
+ * };
+ * SegmentedEnvelope ampEnvelope = new SegmentedEnvelope( ampData );
+ *
+ * // Hang at end of decay segment to provide a "sustain" segment.
+ * ampEnvelope.setSustainBegin( 1 );
+ * ampEnvelope.setSustainEnd( 1 );
+ *
+ * // Play the envelope using queueOn so that it uses the sustain and release information.
+ * synth.add( ampEnv = new VariableRateMonoReader() );
+ * ampEnv.dataQueue.queueOn( ampEnvelope );
+ * </code>
+ * </pre>
+ *
+ * As an alternative you could use an {@link EnvelopeDAHDSR}.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ * @see VariableRateMonoReader
+ * @see EnvelopeDAHDSR
+ */
+public class SegmentedEnvelope extends SequentialDataCommon {
+ private double[] buffer;
+
+ public SegmentedEnvelope(int maxFrames) {
+ allocate(maxFrames);
+ }
+
+ public SegmentedEnvelope(double[] pairs) {
+ this(pairs.length / 2);
+ write(pairs);
+ }
+
+ public void allocate(int maxFrames) {
+ buffer = new double[maxFrames * 2];
+ this.maxFrames = maxFrames;
+ this.numFrames = 0;
+ }
+
+ /**
+ * Write frames of envelope data. A frame consists of a duration and a value.
+ *
+ * @param startFrame Index of frame in envelope to write to.
+ * @param data Pairs of duration and value.
+ * @param startIndex Index of frame in data[] to read from.
+ * @param numToWrite Number of frames (pairs) to write.
+ */
+ public void write(int startFrame, double[] data, int startIndex, int numToWrite) {
+ System.arraycopy(data, startIndex * 2, buffer, startFrame * 2, numToWrite * 2);
+ if ((startFrame + numToWrite) > numFrames) {
+ numFrames = startFrame + numToWrite;
+ }
+ }
+
+ public void read(int startFrame, double[] data, int startIndex, int numToRead) {
+ System.arraycopy(buffer, startFrame * 2, data, startIndex * 2, numToRead * 2);
+ }
+
+ public void write(double[] data) {
+ write(0, data, 0, data.length / 2);
+ }
+
+ public void read(double[] data) {
+ read(0, data, 0, data.length / 2);
+ }
+
+ /** Read the value of an envelope, not the duration. */
+ @Override
+ public double readDouble(int index) {
+ return buffer[(index * 2) + 1];
+ }
+
+ @Override
+ public void writeDouble(int index, double value) {
+ buffer[(index * 2) + 1] = value;
+ if ((index + 1) > numFrames) {
+ numFrames = index + 1;
+ }
+ }
+
+ @Override
+ public double getRateScaler(int index, double synthesisPeriod) {
+ double duration = buffer[index * 2];
+ if (duration < synthesisPeriod) {
+ duration = synthesisPeriod;
+ }
+ return 1.0 / duration;
+ }
+
+ @Override
+ public int getChannelsPerFrame() {
+ return 1;
+ }
+}
diff --git a/src/com/jsyn/data/SequentialData.java b/src/com/jsyn/data/SequentialData.java
new file mode 100644
index 0000000..c5ce2f0
--- /dev/null
+++ b/src/com/jsyn/data/SequentialData.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.data;
+
+import com.jsyn.unitgen.FixedRateMonoReader;
+import com.jsyn.unitgen.FixedRateMonoWriter;
+import com.jsyn.unitgen.FixedRateStereoReader;
+import com.jsyn.unitgen.FixedRateStereoWriter;
+import com.jsyn.unitgen.VariableRateMonoReader;
+import com.jsyn.unitgen.VariableRateStereoReader;
+
+/**
+ * Interface for objects that can be read and/or written by index. The index is not stored
+ * internally so they can be shared by multiple readers.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ * @see FixedRateMonoReader
+ * @see FixedRateStereoReader
+ * @see FixedRateMonoWriter
+ * @see FixedRateStereoWriter
+ * @see VariableRateMonoReader
+ * @see VariableRateStereoReader
+ */
+public interface SequentialData {
+ /**
+ * Write a value at the given index.
+ *
+ * @param index sample index is ((frameIndex * channelsPerFrame) + channelIndex)
+ * @param value the value to be written
+ */
+ void writeDouble(int index, double value);
+
+ /**
+ * Read a value from the sample independently from the internal storage format.
+ *
+ * @param index sample index is ((frameIndex * channelsPerFrame) + channelIndex)
+ */
+
+ double readDouble(int index);
+
+ /***
+ * @return Beginning of sustain loop or -1 if no loop.
+ */
+ public int getSustainBegin();
+
+ /**
+ * SustainEnd value is the frame index of the frame just past the end of the loop. The number of
+ * frames included in the loop is (SustainEnd - SustainBegin).
+ *
+ * @return End of sustain loop or -1 if no loop.
+ */
+ public int getSustainEnd();
+
+ /***
+ * @return Beginning of release loop or -1 if no loop.
+ */
+ public int getReleaseBegin();
+
+ /***
+ * @return End of release loop or -1 if no loop.
+ */
+ public int getReleaseEnd();
+
+ /**
+ * Get rate to play the data. In an envelope this correspond to the inverse of the frame
+ * duration and would vary frame to frame. For an audio sample it is 1.0.
+ *
+ * @param index
+ * @param synthesisRate
+ * @return rate to scale the playback speed.
+ */
+ double getRateScaler(int index, double synthesisRate);
+
+ /**
+ * @return For a stereo sample, return 2.
+ */
+ int getChannelsPerFrame();
+
+ /**
+ * @return The number of valid frames that can be read.
+ */
+ int getNumFrames();
+}
diff --git a/src/com/jsyn/data/SequentialDataCommon.java b/src/com/jsyn/data/SequentialDataCommon.java
new file mode 100644
index 0000000..190cd92
--- /dev/null
+++ b/src/com/jsyn/data/SequentialDataCommon.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.data;
+
+/**
+ * Abstract base class for envelopes and samples that adds sustain and release loops.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public abstract class SequentialDataCommon implements SequentialData {
+ protected int numFrames;
+ protected int maxFrames;
+ private int sustainBegin = -1;
+ private int sustainEnd = -1;
+ private int releaseBegin = -1;
+ private int releaseEnd = -1;
+
+ @Override
+ public abstract void writeDouble(int index, double value);
+
+ @Override
+ public abstract double readDouble(int index);
+
+ @Override
+ public abstract double getRateScaler(int index, double synthesisRate);
+
+ @Override
+ public abstract int getChannelsPerFrame();
+
+ /**
+ * @return Maximum number of frames of data.
+ */
+ public int getMaxFrames() {
+ return maxFrames;
+ }
+
+ /**
+ * Set number of frames of data. Input will be clipped to maxFrames. This is useful when
+ * changing the contents of a sample or envelope.
+ */
+ public void setNumFrames(int numFrames) {
+ if (numFrames > maxFrames)
+ numFrames = maxFrames;
+ this.numFrames = numFrames;
+ }
+
+ @Override
+ public int getNumFrames() {
+ return numFrames;
+ }
+
+ // JavaDocs will be copied from SequentialData
+
+ @Override
+ public int getSustainBegin() {
+ return this.sustainBegin;
+ }
+
+ @Override
+ public int getSustainEnd() {
+ return this.sustainEnd;
+ }
+
+ @Override
+ public int getReleaseBegin() {
+ return this.releaseBegin;
+ }
+
+ @Override
+ public int getReleaseEnd() {
+ return this.releaseEnd;
+ }
+
+ public void setSustainBegin(int sustainBegin) {
+ this.sustainBegin = sustainBegin;
+ }
+
+ /**
+ * SustainEnd value is the frame index of the frame just past the end of the loop. The number of
+ * frames included in the loop is (SustainEnd - SustainBegin).
+ *
+ * @param sustainEnd
+ */
+ public void setSustainEnd(int sustainEnd) {
+ this.sustainEnd = sustainEnd;
+ }
+
+ public void setReleaseBegin(int releaseBegin) {
+ this.releaseBegin = releaseBegin;
+ }
+
+ /**
+ * ReleaseEnd value is the frame index of the frame just past the end of the loop. The number of
+ * frames included in the loop is (ReleaseEnd - ReleaseBegin).
+ *
+ * @param releaseEnd
+ */
+
+ public void setReleaseEnd(int releaseEnd) {
+ this.releaseEnd = releaseEnd;
+ }
+
+}
diff --git a/src/com/jsyn/data/ShortSample.java b/src/com/jsyn/data/ShortSample.java
new file mode 100644
index 0000000..4a4110e
--- /dev/null
+++ b/src/com/jsyn/data/ShortSample.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.data;
+
+import com.jsyn.engine.SynthesisEngine;
+import com.jsyn.unitgen.FixedRateMonoReader;
+import com.jsyn.unitgen.FixedRateStereoReader;
+import com.jsyn.unitgen.VariableRateMonoReader;
+import com.jsyn.unitgen.VariableRateStereoReader;
+import com.jsyn.util.SampleLoader;
+
+/**
+ * Store multi-channel short audio data in an interleaved buffer.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ * @see SampleLoader
+ * @see FixedRateMonoReader
+ * @see FixedRateStereoReader
+ * @see VariableRateMonoReader
+ * @see VariableRateStereoReader
+ */
+public class ShortSample extends AudioSample {
+ private short[] buffer;
+
+ public ShortSample() {
+ }
+
+ public ShortSample(int numFrames, int channelsPerFrame) {
+ allocate(numFrames, channelsPerFrame);
+ }
+
+ /** Constructor for mono samples with data. */
+ public ShortSample(short[] data) {
+ this(data.length, 1);
+ write(data);
+ }
+
+ /** Constructor for multi-channel samples with data. */
+ public ShortSample(short[] data, int channelsPerFrame) {
+ this(data.length / channelsPerFrame, channelsPerFrame);
+ write(data);
+ }
+
+ @Override
+ public void allocate(int numFrames, int channelsPerFrame) {
+ buffer = new short[numFrames * channelsPerFrame];
+ this.numFrames = numFrames;
+ this.channelsPerFrame = channelsPerFrame;
+ }
+
+ /**
+ * Note that in a stereo sample, a frame has two values.
+ *
+ * @param startFrame index of frame in the sample
+ * @param data data to be written
+ * @param startIndex index of first value in array
+ * @param numFrames
+ */
+ public void write(int startFrame, short[] data, int startIndex, int numFrames) {
+ int numSamplesToWrite = numFrames * channelsPerFrame;
+ int firstSampleIndexToWrite = startFrame * channelsPerFrame;
+ System.arraycopy(data, startIndex, buffer, firstSampleIndexToWrite, numSamplesToWrite);
+ }
+
+ /**
+ * Note that in a stereo sample, a frame has two values.
+ *
+ * @param startFrame index of frame in the sample
+ * @param data array to receive the data from the sample
+ * @param startIndex index of first location in array to start filling
+ * @param numFrames
+ */
+ public void read(int startFrame, short[] data, int startIndex, int numFrames) {
+ int numSamplesToRead = numFrames * channelsPerFrame;
+ int firstSampleIndexToRead = startFrame * channelsPerFrame;
+ System.arraycopy(buffer, firstSampleIndexToRead, data, startIndex, numSamplesToRead);
+ }
+
+ public void write(short[] data) {
+ write(0, data, 0, data.length);
+ }
+
+ public void read(short[] data) {
+ read(0, data, 0, data.length);
+ }
+
+ public short readShort(int index) {
+ return buffer[index];
+ }
+
+ public void writeShort(int index, short value) {
+ buffer[index] = value;
+ }
+
+ /** Read a sample converted to a double in the range -1.0 to almost 1.0. */
+ @Override
+ public double readDouble(int index) {
+ return SynthesisEngine.convertShortToDouble(buffer[index]);
+ }
+
+ /**
+ * Write a double that will be clipped to the range -1.0 to almost 1.0 and converted to a short.
+ */
+ @Override
+ public void writeDouble(int index, double value) {
+ buffer[index] = SynthesisEngine.convertDoubleToShort(value);
+ }
+
+}
diff --git a/src/com/jsyn/data/SpectralWindow.java b/src/com/jsyn/data/SpectralWindow.java
new file mode 100644
index 0000000..0fcfac4
--- /dev/null
+++ b/src/com/jsyn/data/SpectralWindow.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.data;
+
+public interface SpectralWindow {
+ public double get(int index);
+}
diff --git a/src/com/jsyn/data/SpectralWindowFactory.java b/src/com/jsyn/data/SpectralWindowFactory.java
new file mode 100644
index 0000000..01cced6
--- /dev/null
+++ b/src/com/jsyn/data/SpectralWindowFactory.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2013 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.data;
+
+import com.jsyn.unitgen.SpectralFFT;
+import com.jsyn.unitgen.SpectralFilter;
+import com.jsyn.unitgen.SpectralIFFT;
+
+/**
+ * Create shared windows as needed for use with FFTs and IFFTs.
+ *
+ * @author Phil Burk (C) 2013 Mobileer Inc
+ * @see SpectralWindow
+ * @see SpectralFFT
+ * @see SpectralIFFT
+ * @see SpectralFilter
+ */
+public class SpectralWindowFactory {
+ private static final int NUM_WINDOWS = 16;
+ private static final int MIN_SIZE_LOG_2 = 2;
+ private static HammingWindow[] hammingWindows = new HammingWindow[NUM_WINDOWS];
+ private static HannWindow[] hannWindows = new HannWindow[NUM_WINDOWS];
+
+ /** @return a shared standard HammingWindow */
+ public static HammingWindow getHammingWindow(int sizeLog2) {
+ int index = sizeLog2 - MIN_SIZE_LOG_2;
+ if (hammingWindows[index] == null) {
+ hammingWindows[index] = new HammingWindow(1 << sizeLog2);
+ }
+ return hammingWindows[index];
+ }
+
+ /** @return a shared HannWindow */
+ public static HannWindow getHannWindow(int sizeLog2) {
+ int index = sizeLog2 - MIN_SIZE_LOG_2;
+ if (hannWindows[index] == null) {
+ hannWindows[index] = new HannWindow(1 << sizeLog2);
+ }
+ return hannWindows[index];
+ }
+}
diff --git a/src/com/jsyn/data/Spectrum.java b/src/com/jsyn/data/Spectrum.java
new file mode 100644
index 0000000..66e4ee4
--- /dev/null
+++ b/src/com/jsyn/data/Spectrum.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2013 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.data;
+
+import com.jsyn.unitgen.SpectralFFT;
+import com.jsyn.unitgen.SpectralIFFT;
+import com.jsyn.unitgen.SpectralProcessor;
+
+/**
+ * Complex spectrum with real and imaginary parts. The frequency associated with each bin of the
+ * spectrum is:
+ *
+ * <pre>
+ * frequency = binIndex * sampleRate / size
+ * </pre>
+ *
+ * Note that the upper half of the spectrum is above the Nyquist frequency. Those frequencies are
+ * mirrored around the Nyquist frequency. Note that this spectral API is experimental and may change
+ * at any time.
+ *
+ * @author Phil Burk (C) 2013 Mobileer Inc
+ * @version 016
+ * @see SpectralFFT
+ * @see SpectralIFFT
+ * @see SpectralProcessor
+ */
+public class Spectrum {
+ private double[] real;
+ private double[] imaginary;
+ public static final int DEFAULT_SIZE_LOG_2 = 9;
+ public static final int DEFAULT_SIZE = 1 << DEFAULT_SIZE_LOG_2;
+
+ public Spectrum() {
+ this(DEFAULT_SIZE);
+ }
+
+ public Spectrum(int size) {
+ setSize(size);
+ }
+
+ public double[] getReal() {
+ return real;
+ }
+
+ public double[] getImaginary() {
+ return imaginary;
+ }
+
+ /**
+ * If you change the size of the spectrum then the real and imaginary arrays will be
+ * reallocated.
+ *
+ * @param size
+ */
+ public void setSize(int size) {
+ if ((real == null) || (real.length != size)) {
+ real = new double[size];
+ imaginary = new double[size];
+ }
+ }
+
+ public int size() {
+ return real.length;
+ }
+
+ /**
+ * Copy this spectrum to another spectrum of the same length.
+ *
+ * @param destination
+ */
+ public void copyTo(Spectrum destination) {
+ assert (size() == destination.size());
+ System.arraycopy(real, 0, destination.real, 0, real.length);
+ System.arraycopy(imaginary, 0, destination.imaginary, 0, imaginary.length);
+ }
+
+ public void clear() {
+ for (int i = 0; i < real.length; i++) {
+ real[i] = 0.0;
+ imaginary[i] = 0.0;
+ }
+ }
+}
diff --git a/src/com/jsyn/devices/AudioDeviceFactory.java b/src/com/jsyn/devices/AudioDeviceFactory.java
new file mode 100644
index 0000000..58b9772
--- /dev/null
+++ b/src/com/jsyn/devices/AudioDeviceFactory.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.devices;
+
+import com.jsyn.util.JavaTools;
+
+/**
+ * Create a device appropriate for the platform.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class AudioDeviceFactory {
+ private static AudioDeviceManager instance;
+
+ /**
+ * Use a custom device interface. Overrides the selection of a default device manager.
+ *
+ * @param instance
+ */
+ public static void setInstance(AudioDeviceManager instance) {
+ AudioDeviceFactory.instance = instance;
+ }
+
+ /**
+ * Try to load JPortAudio or JavaSound devices.
+ *
+ * @return A device supported on this platform.
+ */
+ public static AudioDeviceManager createAudioDeviceManager() {
+ return createAudioDeviceManager(false);
+ }
+
+ /**
+ * Try to load JPortAudio or JavaSound devices.
+ *
+ * @param preferJavaSound if true then try to create a JavaSound manager before other types.
+ * @return A device supported on this platform.
+ */
+ public static AudioDeviceManager createAudioDeviceManager(boolean preferJavaSound) {
+ if (preferJavaSound) {
+ tryJavaSound();
+ tryJPortAudio();
+ } else {
+ tryJPortAudio();
+ tryJavaSound();
+ }
+ return instance;
+ }
+
+ private static void tryJavaSound() {
+ if (instance == null) {
+ try {
+ Class<AudioDeviceManager> clazz = JavaTools.loadClass(
+ "com.jsyn.devices.javasound.JavaSoundAudioDevice", false);
+ if (clazz != null) {
+ instance = clazz.newInstance();
+ }
+ } catch (Throwable e) {
+ System.err.println("Could not load JavaSound device. " + e);
+ }
+ }
+ }
+
+ private static void tryJPortAudio() {
+ if (instance == null) {
+ try {
+ if (JavaTools.loadClass("com.portaudio.PortAudio", false) != null) {
+ instance = (AudioDeviceManager) JavaTools.loadClass(
+ "com.jsyn.devices.jportaudio.JPortAudioDevice").newInstance();
+ }
+
+ } catch (Throwable e) {
+ System.err.println("Could not load JPortAudio device. " + e);
+ }
+ }
+ }
+
+}
diff --git a/src/com/jsyn/devices/AudioDeviceInputStream.java b/src/com/jsyn/devices/AudioDeviceInputStream.java
new file mode 100644
index 0000000..a3d1854
--- /dev/null
+++ b/src/com/jsyn/devices/AudioDeviceInputStream.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.devices;
+
+import com.jsyn.io.AudioInputStream;
+
+public interface AudioDeviceInputStream extends AudioInputStream {
+ /** Start the input device. */
+ public void start();
+
+ public void stop();
+
+ /**
+ * @return Estimated latency in seconds.
+ */
+ public double getLatency();
+}
diff --git a/src/com/jsyn/devices/AudioDeviceManager.java b/src/com/jsyn/devices/AudioDeviceManager.java
new file mode 100644
index 0000000..ac8d47c
--- /dev/null
+++ b/src/com/jsyn/devices/AudioDeviceManager.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.devices;
+
+/**
+ * Interface for an audio system. This may be implemented using JavaSound, or a native device
+ * wrapper.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public interface AudioDeviceManager {
+ /**
+ * Pass this value to the start method to request the default device ID.
+ */
+ public final static int USE_DEFAULT_DEVICE = -1;
+
+ /**
+ * @return The number of devices available.
+ */
+ public int getDeviceCount();
+
+ /**
+ * Get the name of an audio device.
+ *
+ * @param deviceID An index between 0 to deviceCount-1.
+ * @return A name that can be shown to the user.
+ */
+ public String getDeviceName(int deviceID);
+
+ /**
+ * @return A name of the device manager that can be shown to the user.
+ */
+ public String getName();
+
+ /**
+ * The user can generally select a default device using a control panel that is part of the
+ * operating system.
+ *
+ * @return The ID for the input device that the user has selected as the default.
+ */
+ public int getDefaultInputDeviceID();
+
+ /**
+ * The user can generally select a default device using a control panel that is part of the
+ * operating system.
+ *
+ * @return The ID for the output device that the user has selected as the default.
+ */
+ public int getDefaultOutputDeviceID();
+
+ /**
+ * @param deviceID
+ * @return The maximum number of channels that the device will support.
+ */
+ public int getMaxInputChannels(int deviceID);
+
+ /**
+ * @param deviceID An index between 0 to numDevices-1.
+ * @return The maximum number of channels that the device will support.
+ */
+ public int getMaxOutputChannels(int deviceID);
+
+ /**
+ * This the lowest latency that the device can support reliably. It should be used for
+ * applications that require low latency such as live processing of guitar signals.
+ *
+ * @param deviceID An index between 0 to numDevices-1.
+ * @return Latency in seconds.
+ */
+ public double getDefaultLowInputLatency(int deviceID);
+
+ /**
+ * This the highest latency that the device can support. High latency is recommended for
+ * applications that are not time critical, such as recording.
+ *
+ * @param deviceID An index between 0 to numDevices-1.
+ * @return Latency in seconds.
+ */
+ public double getDefaultHighInputLatency(int deviceID);
+
+ public double getDefaultLowOutputLatency(int deviceID);
+
+ public double getDefaultHighOutputLatency(int deviceID);
+
+ /**
+ * Set latency in seconds for the audio device. If set to zero then the DefaultLowLatency value
+ * for the device will be used. This is just a suggestion that will be used when the
+ * AudioDeviceInputStream is started.
+ **/
+ public int setSuggestedInputLatency(double latency);
+
+ public int setSuggestedOutputLatency(double latency);
+
+ /**
+ * Create a stream that can be used internally by JSyn for outputting audio data. Applications
+ * should not call this directly.
+ */
+ AudioDeviceOutputStream createOutputStream(int deviceID, int frameRate, int numOutputChannels);
+
+ /**
+ * Create a stream that can be used internally by JSyn for acquiring audio input data.
+ * Applications should not call this directly.
+ */
+ AudioDeviceInputStream createInputStream(int deviceID, int frameRate, int numInputChannels);
+
+}
diff --git a/src/com/jsyn/devices/AudioDeviceOutputStream.java b/src/com/jsyn/devices/AudioDeviceOutputStream.java
new file mode 100644
index 0000000..5c17efb
--- /dev/null
+++ b/src/com/jsyn/devices/AudioDeviceOutputStream.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.devices;
+
+import com.jsyn.io.AudioOutputStream;
+
+public interface AudioDeviceOutputStream extends AudioOutputStream {
+ public void start();
+
+ public void stop();
+
+ /**
+ * @return Estimated latency in seconds.
+ */
+ public double getLatency();
+}
diff --git a/src/com/jsyn/devices/javasound/JavaSoundAudioDevice.java b/src/com/jsyn/devices/javasound/JavaSoundAudioDevice.java
new file mode 100644
index 0000000..56b3912
--- /dev/null
+++ b/src/com/jsyn/devices/javasound/JavaSoundAudioDevice.java
@@ -0,0 +1,434 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.devices.javasound;
+
+import java.util.ArrayList;
+import java.util.logging.Logger;
+
+import javax.sound.sampled.AudioFormat;
+import javax.sound.sampled.AudioSystem;
+import javax.sound.sampled.DataLine;
+import javax.sound.sampled.Line;
+import javax.sound.sampled.LineUnavailableException;
+import javax.sound.sampled.Mixer;
+import javax.sound.sampled.SourceDataLine;
+import javax.sound.sampled.TargetDataLine;
+
+import com.jsyn.devices.AudioDeviceInputStream;
+import com.jsyn.devices.AudioDeviceManager;
+import com.jsyn.devices.AudioDeviceOutputStream;
+
+/**
+ * Use JavaSound to access the audio hardware.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ */
+public class JavaSoundAudioDevice implements AudioDeviceManager {
+ private static final int BYTES_PER_SAMPLE = 2;
+ private static final boolean USE_BIG_ENDIAN = false;
+
+ ArrayList<DeviceInfo> deviceRecords;
+ private double suggestedOutputLatency = 0.040;
+ private double suggestedInputLatency = 0.100;
+ private int defaultInputDeviceID = -1;
+ private int defaultOutputDeviceID = -1;
+
+ static Logger logger = Logger.getLogger(JavaSoundAudioDevice.class.getName());
+
+ public JavaSoundAudioDevice() {
+ String osName = System.getProperty("os.name");
+ if (osName.contains("Windows")) {
+ suggestedOutputLatency = 0.08;
+ logger.info("JSyn: default output latency set to "
+ + ((int) (suggestedOutputLatency * 1000)) + " msec for " + osName);
+ }
+ deviceRecords = new ArrayList<DeviceInfo>();
+ sniffAvailableMixers();
+ dumpAvailableMixers();
+ }
+
+ private void dumpAvailableMixers() {
+ for (DeviceInfo deviceInfo : deviceRecords) {
+ logger.fine("" + deviceInfo);
+ }
+ }
+
+ /**
+ * Build device info and determine default devices.
+ */
+ private void sniffAvailableMixers() {
+ Mixer.Info[] mixers = AudioSystem.getMixerInfo();
+ for (int i = 0; i < mixers.length; i++) {
+ DeviceInfo deviceInfo = new DeviceInfo();
+
+ deviceInfo.name = mixers[i].getName();
+ Mixer mixer = AudioSystem.getMixer(mixers[i]);
+
+ Line.Info[] lines = mixer.getTargetLineInfo();
+ deviceInfo.maxInputs = scanMaxChannels(lines);
+ // Remember first device that supports input.
+ if ((defaultInputDeviceID < 0) && (deviceInfo.maxInputs > 0)) {
+ defaultInputDeviceID = i;
+ }
+
+ lines = mixer.getSourceLineInfo();
+ deviceInfo.maxOutputs = scanMaxChannels(lines);
+ // Remember first device that supports output.
+ if ((defaultOutputDeviceID < 0) && (deviceInfo.maxOutputs > 0)) {
+ defaultOutputDeviceID = i;
+ }
+
+ deviceRecords.add(deviceInfo);
+ }
+ }
+
+ private int scanMaxChannels(Line.Info[] lines) {
+ int maxChannels = 0;
+ for (Line.Info line : lines) {
+ if (line instanceof DataLine.Info) {
+ int numChannels = scanMaxChannels(((DataLine.Info) line));
+ if (numChannels > maxChannels) {
+ maxChannels = numChannels;
+ }
+ }
+ }
+ return maxChannels;
+ }
+
+ private int scanMaxChannels(DataLine.Info info) {
+ int maxChannels = 0;
+ for (AudioFormat format : info.getFormats()) {
+ int numChannels = format.getChannels();
+ if (numChannels > maxChannels) {
+ maxChannels = numChannels;
+ }
+ }
+ return maxChannels;
+ }
+
+ class DeviceInfo {
+ String name;
+ int maxInputs;
+ int maxOutputs;
+
+ @Override
+ public String toString() {
+ return "AudioDevice: " + name + ", max in = " + maxInputs + ", max out = " + maxOutputs;
+ }
+ }
+
+ private class JavaSoundStream {
+ AudioFormat format;
+ byte[] bytes;
+ int frameRate;
+ int deviceID;
+ int samplesPerFrame;
+
+ public JavaSoundStream(int deviceID, int frameRate, int samplesPerFrame) {
+ this.deviceID = deviceID;
+ this.frameRate = frameRate;
+ this.samplesPerFrame = samplesPerFrame;
+ format = new AudioFormat(frameRate, 16, samplesPerFrame, true, USE_BIG_ENDIAN);
+ }
+
+ Line getDataLine(DataLine.Info info) throws LineUnavailableException {
+ Line dataLine;
+ if (deviceID >= 0) {
+ Mixer.Info[] mixers = AudioSystem.getMixerInfo();
+ Mixer mixer = AudioSystem.getMixer(mixers[deviceID]);
+ dataLine = mixer.getLine(info);
+ } else {
+ dataLine = AudioSystem.getLine(info);
+ }
+ return dataLine;
+ }
+
+ int calculateBufferSize(double suggestedOutputLatency) {
+ int numFrames = (int) (suggestedOutputLatency * frameRate);
+ int numBytes = numFrames * samplesPerFrame * BYTES_PER_SAMPLE;
+ return numBytes;
+ }
+
+ }
+
+ private class JavaSoundOutputStream extends JavaSoundStream implements AudioDeviceOutputStream {
+ SourceDataLine line;
+
+ public JavaSoundOutputStream(int deviceID, int frameRate, int samplesPerFrame) {
+ super(deviceID, frameRate, samplesPerFrame);
+ }
+
+ @Override
+ public void start() {
+ DataLine.Info info = new DataLine.Info(SourceDataLine.class, format);
+ if (!AudioSystem.isLineSupported(info)) {
+ // Handle the error.
+ logger.severe("JavaSoundOutputStream - not supported." + format);
+ } else {
+ try {
+ line = (SourceDataLine) getDataLine(info);
+ int bufferSize = calculateBufferSize(suggestedOutputLatency);
+ line.open(format, bufferSize);
+ logger.fine("Output buffer size = " + bufferSize + " bytes.");
+ line.start();
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ line = null;
+ }
+ }
+ }
+
+ /** Grossly inefficient. Call the array version instead. */
+ @Override
+ public void write(double value) {
+ double[] buffer = new double[1];
+ buffer[0] = value;
+ write(buffer, 0, 1);
+ }
+
+ @Override
+ public void write(double[] buffer) {
+ write(buffer, 0, buffer.length);
+ }
+
+ @Override
+ public void write(double[] buffer, int start, int count) {
+ // Allocate byte buffer if needed.
+ if ((bytes == null) || ((bytes.length * 2) < count)) {
+ bytes = new byte[count * 2];
+ }
+
+ // Convert float samples to LittleEndian bytes.
+ int byteIndex = 0;
+ for (int i = 0; i < count; i++) {
+ // Offset before casting so that we can avoid using floor().
+ // Also round by adding 0.5 so that very small signals go to zero.
+ double temp = (32767.0 * buffer[i + start]) + 32768.5;
+ int sample = ((int) temp) - 32768;
+ if (sample > Short.MAX_VALUE) {
+ sample = Short.MAX_VALUE;
+ } else if (sample < Short.MIN_VALUE) {
+ sample = Short.MIN_VALUE;
+ }
+ bytes[byteIndex++] = (byte) sample; // little end
+ bytes[byteIndex++] = (byte) (sample >> 8); // big end
+ }
+
+ // sleepUntilNonBlocking( byteIndex ); // This was just an attempt to get rid of clicks
+ // on Apple.
+
+ line.write(bytes, 0, byteIndex);
+ }
+
+ @Override
+ public void stop() {
+ if (line != null) {
+ line.stop();
+ line.flush();
+ line.close();
+ line = null;
+ } else {
+ new RuntimeException("AudioOutput stop attempted when no line created.")
+ .printStackTrace();
+ }
+ }
+
+ @Override
+ public double getLatency() {
+ if (line == null) {
+ return 0.0;
+ }
+ int numBytes = line.getBufferSize();
+ int numFrames = numBytes / (BYTES_PER_SAMPLE * samplesPerFrame);
+ return ((double) numFrames) / frameRate;
+ }
+
+ @Override
+ public void close() {
+ }
+
+ }
+
+ private class JavaSoundInputStream extends JavaSoundStream implements AudioDeviceInputStream {
+ TargetDataLine line;
+
+ public JavaSoundInputStream(int deviceID, int frameRate, int samplesPerFrame) {
+ super(deviceID, frameRate, samplesPerFrame);
+ }
+
+ @Override
+ public void start() {
+ DataLine.Info info = new DataLine.Info(TargetDataLine.class, format);
+ if (!AudioSystem.isLineSupported(info)) {
+ // Handle the error.
+ logger.severe("JavaSoundInputStream - not supported." + format);
+ } else {
+ try {
+ line = (TargetDataLine) getDataLine(info);
+ int bufferSize = calculateBufferSize(suggestedInputLatency);
+ line.open(format, bufferSize);
+ logger.fine("Input buffer size = " + bufferSize + " bytes.");
+ line.start();
+ } catch (Exception e) {
+ e.printStackTrace();
+ line = null;
+ }
+ }
+ }
+
+ @Override
+ public double read() {
+ double[] buffer = new double[1];
+ read(buffer, 0, 1);
+ return buffer[0];
+ }
+
+ @Override
+ public int read(double[] buffer) {
+ return read(buffer, 0, buffer.length);
+ }
+
+ @Override
+ public int read(double[] buffer, int start, int count) {
+ // Allocate byte buffer if needed.
+ if ((bytes == null) || ((bytes.length * 2) < count)) {
+ bytes = new byte[count * 2];
+ }
+ int bytesRead = line.read(bytes, 0, bytes.length);
+
+ // Convert BigEndian bytes to float samples
+ int bi = 0;
+ for (int i = 0; i < count; i++) {
+ int sample = bytes[bi++] & 0x00FF; // little end
+ sample = sample + (bytes[bi++] << 8); // big end
+ buffer[i + start] = sample * (1.0 / 32767.0);
+ }
+ return bytesRead / 4;
+ }
+
+ @Override
+ public void stop() {
+ if (line != null) {
+ line.drain();
+ line.close();
+ } else {
+ new RuntimeException("AudioInput stop attempted when no line created.")
+ .printStackTrace();
+ }
+ }
+
+ @Override
+ public double getLatency() {
+ if (line == null) {
+ return 0.0;
+ }
+ int numBytes = line.getBufferSize();
+ int numFrames = numBytes / (BYTES_PER_SAMPLE * samplesPerFrame);
+ return ((double) numFrames) / frameRate;
+ }
+
+ @Override
+ public int available() {
+ return line.available() / BYTES_PER_SAMPLE;
+ }
+
+ @Override
+ public void close() {
+ }
+
+ }
+
+ @Override
+ public AudioDeviceOutputStream createOutputStream(int deviceID, int frameRate,
+ int samplesPerFrame) {
+ return new JavaSoundOutputStream(deviceID, frameRate, samplesPerFrame);
+ }
+
+ @Override
+ public AudioDeviceInputStream createInputStream(int deviceID, int frameRate, int samplesPerFrame) {
+ return new JavaSoundInputStream(deviceID, frameRate, samplesPerFrame);
+ }
+
+ @Override
+ public double getDefaultHighInputLatency(int deviceID) {
+ return 0.300;
+ }
+
+ @Override
+ public double getDefaultHighOutputLatency(int deviceID) {
+ return 0.300;
+ }
+
+ @Override
+ public int getDefaultInputDeviceID() {
+ return defaultInputDeviceID;
+ }
+
+ @Override
+ public int getDefaultOutputDeviceID() {
+ return defaultOutputDeviceID;
+ }
+
+ @Override
+ public double getDefaultLowInputLatency(int deviceID) {
+ return 0.100;
+ }
+
+ @Override
+ public double getDefaultLowOutputLatency(int deviceID) {
+ return 0.100;
+ }
+
+ @Override
+ public int getDeviceCount() {
+ return deviceRecords.size();
+ }
+
+ @Override
+ public String getDeviceName(int deviceID) {
+ return deviceRecords.get(deviceID).name;
+ }
+
+ @Override
+ public int getMaxInputChannels(int deviceID) {
+ return deviceRecords.get(deviceID).maxInputs;
+ }
+
+ @Override
+ public int getMaxOutputChannels(int deviceID) {
+ return deviceRecords.get(deviceID).maxOutputs;
+ }
+
+ @Override
+ public int setSuggestedOutputLatency(double latency) {
+ suggestedOutputLatency = latency;
+ return 0;
+ }
+
+ @Override
+ public int setSuggestedInputLatency(double latency) {
+ suggestedInputLatency = latency;
+ return 0;
+ }
+
+ @Override
+ public String getName() {
+ return "JavaSound";
+ }
+
+}
diff --git a/src/com/jsyn/devices/javasound/MidiDeviceTools.java b/src/com/jsyn/devices/javasound/MidiDeviceTools.java
new file mode 100644
index 0000000..413beca
--- /dev/null
+++ b/src/com/jsyn/devices/javasound/MidiDeviceTools.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.devices.javasound;
+
+import javax.sound.midi.MidiDevice;
+import javax.sound.midi.MidiSystem;
+import javax.sound.midi.MidiUnavailableException;
+import javax.sound.midi.Sequencer;
+import javax.sound.midi.Synthesizer;
+
+public class MidiDeviceTools {
+ /** Print the available MIDI Devices. */
+ public static void listDevices() {
+ // Ask the MidiSystem what is available.
+ MidiDevice.Info[] infos = MidiSystem.getMidiDeviceInfo();
+ // Print info about each device.
+ for (MidiDevice.Info info : infos) {
+ System.out.println("MIDI Info: " + info.getDescription() + ", " + info.getName() + ", "
+ + info.getVendor() + ", " + info.getVersion());
+ // Get the device for more information.
+ try {
+ MidiDevice device = MidiSystem.getMidiDevice(info);
+ System.out.println(" Device: " + ", #recv = " + device.getMaxReceivers()
+ + ", #xmit = " + device.getMaxTransmitters() + ", open = "
+ + device.isOpen() + ", " + device);
+ } catch (MidiUnavailableException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ /** Find a MIDI transmitter that contains text in the name. */
+ public static MidiDevice findKeyboard(String text) {
+ MidiDevice keyboard = null;
+ // Ask the MidiSystem what is available.
+ MidiDevice.Info[] infos = MidiSystem.getMidiDeviceInfo();
+ // Print info about each device.
+ for (MidiDevice.Info info : infos) {
+ try {
+ MidiDevice device = MidiSystem.getMidiDevice(info);
+ // Hardware devices are not Synthesizers or Sequencers.
+ if (!(device instanceof Synthesizer) && !(device instanceof Sequencer)) {
+ // Is this a transmitter?
+ // Might be -1 if unlimited.
+ if (device.getMaxTransmitters() != 0) {
+ if ((text == null)
+ || (info.getDescription().toLowerCase()
+ .contains(text.toLowerCase()))) {
+ keyboard = device;
+ System.out.println("Chose: " + info.getDescription());
+ break;
+ }
+ }
+ }
+ } catch (MidiUnavailableException e) {
+ e.printStackTrace();
+ }
+ }
+ return keyboard;
+ }
+
+ public static MidiDevice findKeyboard() {
+ return findKeyboard(null);
+ }
+
+}
diff --git a/src/com/jsyn/devices/jportaudio/JPortAudioDevice.java b/src/com/jsyn/devices/jportaudio/JPortAudioDevice.java
new file mode 100644
index 0000000..a8e574a
--- /dev/null
+++ b/src/com/jsyn/devices/jportaudio/JPortAudioDevice.java
@@ -0,0 +1,259 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.devices.jportaudio;
+
+import com.jsyn.devices.AudioDeviceInputStream;
+import com.jsyn.devices.AudioDeviceManager;
+import com.jsyn.devices.AudioDeviceOutputStream;
+import com.portaudio.BlockingStream;
+import com.portaudio.DeviceInfo;
+import com.portaudio.HostApiInfo;
+import com.portaudio.PortAudio;
+import com.portaudio.StreamParameters;
+
+public class JPortAudioDevice implements AudioDeviceManager {
+ private double suggestedOutputLatency = 0.030;
+ private double suggestedInputLatency = 0.050;
+ private static final int FRAMES_PER_BUFFER = 128;
+
+ // static Logger logger = Logger.getLogger( JPortAudioDevice.class.getName() );
+
+ public JPortAudioDevice() {
+ PortAudio.initialize();
+ }
+
+ @Override
+ public int getDeviceCount() {
+ return PortAudio.getDeviceCount();
+ }
+
+ @Override
+ public String getDeviceName(int deviceID) {
+ DeviceInfo deviceInfo = PortAudio.getDeviceInfo(deviceID);
+ HostApiInfo hostInfo = PortAudio.getHostApiInfo(deviceInfo.hostApi);
+ return deviceInfo.name + " - " + hostInfo.name;
+ }
+
+ @Override
+ public int getDefaultInputDeviceID() {
+ return PortAudio.getDefaultInputDevice();
+ }
+
+ @Override
+ public int getDefaultOutputDeviceID() {
+ return PortAudio.getDefaultOutputDevice();
+ }
+
+ @Override
+ public int getMaxInputChannels(int deviceID) {
+ if (deviceID < 0) {
+ deviceID = PortAudio.getDefaultInputDevice();
+ }
+ return PortAudio.getDeviceInfo(deviceID).maxInputChannels;
+ }
+
+ @Override
+ public int getMaxOutputChannels(int deviceID) {
+ if (deviceID < 0) {
+ deviceID = PortAudio.getDefaultOutputDevice();
+ }
+ return PortAudio.getDeviceInfo(deviceID).maxOutputChannels;
+ }
+
+ @Override
+ public double getDefaultLowInputLatency(int deviceID) {
+ if (deviceID < 0) {
+ deviceID = PortAudio.getDefaultInputDevice();
+ }
+ return PortAudio.getDeviceInfo(deviceID).defaultLowInputLatency;
+ }
+
+ @Override
+ public double getDefaultHighInputLatency(int deviceID) {
+ if (deviceID < 0) {
+ deviceID = PortAudio.getDefaultInputDevice();
+ }
+ return PortAudio.getDeviceInfo(deviceID).defaultHighInputLatency;
+ }
+
+ @Override
+ public double getDefaultLowOutputLatency(int deviceID) {
+ if (deviceID < 0) {
+ deviceID = PortAudio.getDefaultOutputDevice();
+ }
+ return PortAudio.getDeviceInfo(deviceID).defaultLowOutputLatency;
+ }
+
+ @Override
+ public double getDefaultHighOutputLatency(int deviceID) {
+ if (deviceID < 0) {
+ deviceID = PortAudio.getDefaultOutputDevice();
+ }
+ return PortAudio.getDeviceInfo(deviceID).defaultHighOutputLatency;
+ }
+
+ @Override
+ public int setSuggestedOutputLatency(double latency) {
+ suggestedOutputLatency = latency;
+ return 0;
+ }
+
+ @Override
+ public int setSuggestedInputLatency(double latency) {
+ suggestedInputLatency = latency;
+ return 0;
+ }
+
+ @Override
+ public AudioDeviceOutputStream createOutputStream(int deviceID, int frameRate,
+ int samplesPerFrame) {
+ return new JPAOutputStream(deviceID, frameRate, samplesPerFrame);
+ }
+
+ @Override
+ public AudioDeviceInputStream createInputStream(int deviceID, int frameRate, int samplesPerFrame) {
+ return new JPAInputStream(deviceID, frameRate, samplesPerFrame);
+ }
+
+ private class JPAStream {
+ BlockingStream blockingStream;
+ float[] floatBuffer = null;
+ int samplesPerFrame;
+
+ public void close() {
+ blockingStream.close();
+ }
+
+ public void start() {
+ blockingStream.start();
+ }
+
+ public void stop() {
+ blockingStream.stop();
+ }
+
+ }
+
+ private class JPAOutputStream extends JPAStream implements AudioDeviceOutputStream {
+
+ private JPAOutputStream(int deviceID, int frameRate, int samplesPerFrame) {
+ this.samplesPerFrame = samplesPerFrame;
+ StreamParameters streamParameters = new StreamParameters();
+ streamParameters.channelCount = samplesPerFrame;
+ if (deviceID < 0) {
+ deviceID = PortAudio.getDefaultOutputDevice();
+ }
+ streamParameters.device = deviceID;
+ streamParameters.suggestedLatency = suggestedOutputLatency;
+ int flags = 0;
+ System.out.println("Audio output on " + getDeviceName(deviceID));
+ blockingStream = PortAudio.openStream(null, streamParameters, frameRate,
+ FRAMES_PER_BUFFER, flags);
+ }
+
+ /** Grossly inefficient. Call the array version instead. */
+ @Override
+ public void write(double value) {
+ double[] buffer = new double[1];
+ buffer[0] = value;
+ write(buffer, 0, 1);
+ }
+
+ @Override
+ public void write(double[] buffer) {
+ write(buffer, 0, buffer.length);
+ }
+
+ @Override
+ public void write(double[] buffer, int start, int count) {
+ // Allocate float buffer if needed.
+ if ((floatBuffer == null) || (floatBuffer.length < count)) {
+ floatBuffer = new float[count];
+ }
+ for (int i = 0; i < count; i++) {
+
+ floatBuffer[i] = (float) buffer[i + start];
+ }
+ blockingStream.write(floatBuffer, count / samplesPerFrame);
+ }
+
+ @Override
+ public double getLatency() {
+ return blockingStream.getInfo().outputLatency;
+ }
+ }
+
+ private class JPAInputStream extends JPAStream implements AudioDeviceInputStream {
+ private JPAInputStream(int deviceID, int frameRate, int samplesPerFrame) {
+ this.samplesPerFrame = samplesPerFrame;
+ StreamParameters streamParameters = new StreamParameters();
+ streamParameters.channelCount = samplesPerFrame;
+ if (deviceID < 0) {
+ deviceID = PortAudio.getDefaultInputDevice();
+ }
+ streamParameters.device = deviceID;
+ streamParameters.suggestedLatency = suggestedInputLatency;
+ int flags = 0;
+ System.out.println("Audio input from " + getDeviceName(deviceID));
+ blockingStream = PortAudio.openStream(streamParameters, null, frameRate,
+ FRAMES_PER_BUFFER, flags);
+ }
+
+ @Override
+ public double read() {
+ double[] buffer = new double[1];
+ read(buffer, 0, 1);
+ return buffer[0];
+ }
+
+ @Override
+ public int read(double[] buffer) {
+ return read(buffer, 0, buffer.length);
+ }
+
+ @Override
+ public int read(double[] buffer, int start, int count) {
+ // Allocate float buffer if needed.
+ if ((floatBuffer == null) || (floatBuffer.length < count)) {
+ floatBuffer = new float[count];
+ }
+ blockingStream.read(floatBuffer, count / samplesPerFrame);
+
+ for (int i = 0; i < count; i++) {
+
+ buffer[i + start] = floatBuffer[i];
+ }
+ return count;
+ }
+
+ @Override
+ public double getLatency() {
+ return blockingStream.getInfo().inputLatency;
+ }
+
+ @Override
+ public int available() {
+ return blockingStream.getReadAvailable() * samplesPerFrame;
+ }
+
+ }
+
+ @Override
+ public String getName() {
+ return "JPortAudio";
+ }
+}
diff --git a/src/com/jsyn/engine/LoadAnalyzer.java b/src/com/jsyn/engine/LoadAnalyzer.java
new file mode 100644
index 0000000..cbf7ed5
--- /dev/null
+++ b/src/com/jsyn/engine/LoadAnalyzer.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.engine;
+
+/** Measure CPU load. */
+public class LoadAnalyzer {
+ private long stopTime;
+ private long previousStopTime;
+ private long startTime;
+ private double averageTotalTime;
+ private double averageOnTime;
+
+ protected LoadAnalyzer() {
+ stopTime = System.nanoTime();
+ }
+
+ /**
+ * Call this when you stop doing something. Ideally all of the time since start() was spent on
+ * doing something without interruption.
+ */
+ public void stop() {
+ previousStopTime = stopTime;
+ stopTime = System.nanoTime();
+ long onTime = stopTime - startTime;
+ long totalTime = stopTime - previousStopTime;
+ if (totalTime > 0) {
+ // Recursive averaging filter.
+ double rate = 0.01;
+ averageOnTime = (averageOnTime * (1.0 - rate)) + (onTime * rate);
+ averageTotalTime = (averageTotalTime * (1.0 - rate)) + (totalTime * rate);
+ }
+ }
+
+ /** Call this when you start doing something. */
+ public void start() {
+ startTime = System.nanoTime();
+ }
+
+ /** Calculate, on average, how much of the time was spent doing something. */
+ public double getAverageLoad() {
+ if (averageTotalTime > 0.0) {
+ return averageOnTime / averageTotalTime;
+ } else {
+ return 0.0;
+ }
+ }
+}
diff --git a/src/com/jsyn/engine/MultiTable.java b/src/com/jsyn/engine/MultiTable.java
new file mode 100644
index 0000000..48b03cd
--- /dev/null
+++ b/src/com/jsyn/engine/MultiTable.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.engine;
+
+/*
+ * Multiple tables of sawtooth data.
+ * organized by octaves below the Nyquist Rate.
+ * used to generate band-limited Sawtooth, Impulse, Pulse, Square and Triangle BL waveforms
+ *
+ <pre>
+ Analysis of octave requirements for tables.
+
+ OctavesIndex Frequency Partials
+ 0 N/2 11025 1
+ 1 N/4 5512 2
+ 2 N/8 2756 4
+ 3 N/16 1378 8
+ 4 N/32 689 16
+ 5 N/64 344 32
+ 6 N/128 172 64
+ 7 N/256 86 128
+ </pre>
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ */
+public class MultiTable {
+
+ public final static int NUM_TABLES = 8;
+ public final static int CYCLE_SIZE = (1 << 10);
+
+ private static MultiTable instance = new MultiTable(NUM_TABLES, CYCLE_SIZE);
+ private double phaseScalar;
+ private float[][] tables; // array of array of tables
+
+ /**************************************************************************
+ * Initialize sawtooth wavetables. Table[0] should contain a pure sine wave. Succeeding tables
+ * should have increasing numbers of partials.
+ */
+ public MultiTable(int numTables, int cycleSize) {
+ int tableSize = cycleSize + 1;
+
+ // Allocate array of arrays.
+ tables = new float[numTables][tableSize];
+
+ float[] sineTable = tables[0];
+
+ phaseScalar = (float) (cycleSize * 0.5);
+
+ /* Fill initial sine table with values for -PI to PI. */
+ for (int j = 0; j < tableSize; j++) {
+ sineTable[j] = (float) Math.sin(((((double) j) / (double) cycleSize) * Math.PI * 2.0)
+ - Math.PI);
+ }
+
+ /*
+ * Build each table from scratch and scale partials by raised cosine* to eliminate Gibbs
+ * effect.
+ */
+ for (int i = 1; i < numTables; i++) {
+ int numPartials;
+ double kGibbs;
+ float[] table = tables[i];
+
+ /* Add together partials for this table. */
+ numPartials = 1 << i;
+ kGibbs = Math.PI / (2 * numPartials);
+ for (int k = 0; k < numPartials; k++) {
+ double ampl, cGibbs;
+ int sineIndex = 0;
+ int partial = k + 1;
+ cGibbs = Math.cos(k * kGibbs);
+ /* Calculate amplitude for Nth partial */
+ ampl = cGibbs * cGibbs / partial;
+
+ for (int j = 0; j < tableSize; j++) {
+ table[j] += (float) ampl * sineTable[sineIndex];
+ sineIndex += partial;
+ /* Wrap index at end of table.. */
+ if (sineIndex >= cycleSize) {
+ sineIndex -= cycleSize;
+ }
+ }
+ }
+ }
+
+ /* Normalize after */
+ for (int i = 1; i < numTables; i++) {
+ normalizeArray(tables[i]);
+ }
+ }
+
+ /**************************************************************************/
+ public static float normalizeArray(float[] fdata) {
+ float max, val, gain;
+ int i;
+
+ // determine maximum value.
+ max = 0.0f;
+ for (i = 0; i < fdata.length; i++) {
+ val = Math.abs(fdata[i]);
+ if (val > max)
+ max = val;
+ }
+ if (max < 0.0000001f)
+ max = 0.0000001f;
+ // scale array
+ gain = 1.0f / max;
+ for (i = 0; i < fdata.length; i++)
+ fdata[i] *= gain;
+ return gain;
+ }
+
+ /*****************************************************************************
+ * When the phaseInc maps to the highest level table, then we start interpolating between the
+ * highest table and the raw sawtooth value (phase). When phaseInc points to highest table:
+ * flevel = NUM_TABLES - 1 = -1 - log2(pInc); log2(pInc) = - NUM_TABLES pInc = 2**(-NUM_TABLES)
+ */
+ private final static double LOWEST_PHASE_INC_INV = (1 << NUM_TABLES);
+
+ /**************************************************************************/
+ /* Phase ranges from -1.0 to +1.0 */
+ public double calculateSawtooth(double currentPhase, double positivePhaseIncrement,
+ double flevel) {
+ float[] tableBase;
+ double val;
+ double hiSam; /* Use when verticalFraction is 1.0 */
+ double loSam; /* Use when verticalFraction is 0.0 */
+ double sam1, sam2;
+
+ /* Use Phase to determine sampleIndex into table. */
+ double findex = ((phaseScalar * currentPhase) + phaseScalar);
+ // findex is > 0 so we do not need to call floor().
+ int sampleIndex = (int) findex;
+ double horizontalFraction = findex - sampleIndex;
+ int tableIndex = (int) flevel;
+
+ if (tableIndex > (NUM_TABLES - 2)) {
+ /*
+ * Just use top table and mix with arithmetic sawtooth if below lowest frequency.
+ * Generate new fraction for interpolating between 0.0 and lowest table frequency.
+ */
+ double fraction = positivePhaseIncrement * LOWEST_PHASE_INC_INV;
+ tableBase = tables[(NUM_TABLES - 1)];
+
+ /* Get adjacent samples. Assume guard point present. */
+ sam1 = tableBase[sampleIndex];
+ sam2 = tableBase[sampleIndex + 1];
+ /* Interpolate between adjacent samples. */
+ loSam = sam1 + (horizontalFraction * (sam2 - sam1));
+
+ /* Use arithmetic version for low frequencies. */
+ /* fraction is 0.0 at 0 Hz */
+ val = currentPhase + (fraction * (loSam - currentPhase));
+ } else {
+
+ double verticalFraction = flevel - tableIndex;
+
+ if (tableIndex < 0) {
+ if (tableIndex < -1) // above Nyquist!
+ {
+ val = 0.0;
+ } else {
+ /*
+ * At top of supported range, interpolate between 0.0 and first partial.
+ */
+ tableBase = tables[0]; /* Sine wave table. */
+
+ /* Get adjacent samples. Assume guard point present. */
+ sam1 = tableBase[sampleIndex];
+ sam2 = tableBase[sampleIndex + 1];
+
+ /* Interpolate between adjacent samples. */
+ hiSam = sam1 + (horizontalFraction * (sam2 - sam1));
+ /* loSam = 0.0 */
+ // verticalFraction is 0.0 at Nyquist
+ val = verticalFraction * hiSam;
+ }
+ } else {
+ /*
+ * Interpolate between adjacent levels to prevent harmonics from popping.
+ */
+ tableBase = tables[tableIndex + 1];
+
+ /* Get adjacent samples. Assume guard point present. */
+ sam1 = tableBase[sampleIndex];
+ sam2 = tableBase[sampleIndex + 1];
+
+ /* Interpolate between adjacent samples. */
+ hiSam = sam1 + (horizontalFraction * (sam2 - sam1));
+
+ /* Get adjacent samples. Assume guard point present. */
+ tableBase = tables[tableIndex];
+ sam1 = tableBase[sampleIndex];
+ sam2 = tableBase[sampleIndex + 1];
+
+ /* Interpolate between adjacent samples. */
+ loSam = sam1 + (horizontalFraction * (sam2 - sam1));
+
+ val = loSam + (verticalFraction * (hiSam - loSam));
+ }
+ }
+ return val;
+ }
+
+ public double convertPhaseIncrementToLevel(double positivePhaseIncrement) {
+ if (positivePhaseIncrement < 1.0e-30) {
+ positivePhaseIncrement = 1.0e-30;
+ }
+ return -1.0 - (Math.log(positivePhaseIncrement) / Math.log(2.0));
+ }
+
+ public static MultiTable getInstance() {
+ return instance;
+ }
+
+}
diff --git a/src/com/jsyn/engine/SynthesisEngine.java b/src/com/jsyn/engine/SynthesisEngine.java
new file mode 100644
index 0000000..119435f
--- /dev/null
+++ b/src/com/jsyn/engine/SynthesisEngine.java
@@ -0,0 +1,683 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.engine;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.logging.Logger;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import com.jsyn.devices.AudioDeviceFactory;
+import com.jsyn.devices.AudioDeviceInputStream;
+import com.jsyn.devices.AudioDeviceManager;
+import com.jsyn.devices.AudioDeviceOutputStream;
+import com.jsyn.unitgen.UnitGenerator;
+import com.softsynth.shared.time.ScheduledCommand;
+import com.softsynth.shared.time.ScheduledQueue;
+import com.softsynth.shared.time.TimeStamp;
+
+//TODO Resolve problem with HearDAHDSR where "Rate" port.set is not reflected in knob. Engine not running.
+//TODO new tutorial and docs on website
+//TODO AutoStop on DAHDSR
+//TODO Test/example SequentialData queueOn and queueOff
+
+//TODO Abstract device interface. File device!
+//TODO Measure thread switching sync, performance for multi-core synthesis. Use 4 core pro.
+//TODO Optimize SineOscillatorPhaseModulated
+//TODO More circuits.
+//TODO DC blocker
+//TODO Swing scope probe UIs, auto ranging
+
+/**
+ * Internal implementation of JSyn Synthesizer. The public API is in the Synthesizer interface. This
+ * class might be used directly internally.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ * @see Synthesizer
+ */
+public class SynthesisEngine implements Runnable, Synthesizer {
+ private final static int BLOCKS_PER_BUFFER = 8;
+ private final static int FRAMES_PER_BUFFER = Synthesizer.FRAMES_PER_BLOCK * BLOCKS_PER_BUFFER;
+ public static final int DEFAULT_FRAME_RATE = 44100;
+
+ private AudioDeviceManager audioDeviceManager;
+ private AudioDeviceOutputStream audioOutputStream;
+ private AudioDeviceInputStream audioInputStream;
+ private Thread audioThread;
+ private ScheduledQueue<ScheduledCommand> commandQueue = new ScheduledQueue<ScheduledCommand>();
+ private volatile boolean go;
+
+ private InterleavingBuffer inputBuffer;
+ private InterleavingBuffer outputBuffer;
+ private double inverseNyquist;
+ private long frameCount;
+ private boolean pullDataEnabled = true;
+ private boolean useRealTime = true;
+ private boolean started;
+ private int frameRate = DEFAULT_FRAME_RATE;
+ private double framePeriod = 1.0 / frameRate;
+
+ // List of all units added to the synth.
+ private ArrayList<UnitGenerator> allUnitList = new ArrayList<UnitGenerator>();
+ // List of running units.
+ private ArrayList<UnitGenerator> runningUnitList = new ArrayList<UnitGenerator>();
+ // List of units stopping because of autoStop.
+ private ArrayList<UnitGenerator> stoppingUnitList = new ArrayList<UnitGenerator>();
+
+ private LoadAnalyzer loadAnalyzer;
+ // private int numOutputChannels;
+ // private int numInputChannels;
+ private CopyOnWriteArrayList<Runnable> audioTasks = new CopyOnWriteArrayList<Runnable>();
+ /** A fraction corresponding to exactly -96 dB. */
+ public static final double DB96 = (1.0 / 63095.73444801943);
+ /** A fraction that is approximately -90.3 dB. Defined as 1 bit of an S16. */
+ public static final double DB90 = (1.0 / (1 << 15));
+
+ static Logger logger = Logger.getLogger(SynthesisEngine.class.getName());
+
+ public SynthesisEngine(AudioDeviceManager audioDeviceManager) {
+ this.audioDeviceManager = audioDeviceManager;
+ }
+
+ public SynthesisEngine() {
+ this(AudioDeviceFactory.createAudioDeviceManager());
+ }
+
+ @Override
+ public String getVersion() {
+ return JSyn.VERSION;
+ }
+
+ @Override
+ public int getVersionCode() {
+ return JSyn.VERSION_CODE;
+ }
+
+ @Override
+ public String toString() {
+ return "JSyn " + JSyn.VERSION_TEXT;
+ }
+
+ public boolean isPullDataEnabled() {
+ return pullDataEnabled;
+ }
+
+ /**
+ * If set true then audio data will be pulled from the output ports of connected unit
+ * generators. The final unit in a tree of units needs to be start()ed.
+ *
+ * @param pullDataEnabled
+ */
+ public void setPullDataEnabled(boolean pullDataEnabled) {
+ this.pullDataEnabled = pullDataEnabled;
+ }
+
+ private void setupAudioBuffers(int numInputChannels, int numOutputChannels) {
+ inputBuffer = new InterleavingBuffer(FRAMES_PER_BUFFER, Synthesizer.FRAMES_PER_BLOCK,
+ numInputChannels);
+ outputBuffer = new InterleavingBuffer(FRAMES_PER_BUFFER, Synthesizer.FRAMES_PER_BLOCK,
+ numOutputChannels);
+ }
+
+ public void terminate() {
+ }
+
+ class InterleavingBuffer {
+ private double[] interleavedBuffer;
+ ChannelBlockBuffer[] blockBuffers;
+
+ InterleavingBuffer(int framesPerBuffer, int framesPerBlock, int samplesPerFrame) {
+ interleavedBuffer = new double[framesPerBuffer * samplesPerFrame];
+ // Allocate buffers for each channel of synthesis output.
+ blockBuffers = new ChannelBlockBuffer[samplesPerFrame];
+ for (int i = 0; i < blockBuffers.length; i++) {
+ blockBuffers[i] = new ChannelBlockBuffer(framesPerBlock);
+ }
+ }
+
+ int deinterleave(int inIndex) {
+ for (int jf = 0; jf < Synthesizer.FRAMES_PER_BLOCK; jf++) {
+ for (int iob = 0; iob < blockBuffers.length; iob++) {
+ ChannelBlockBuffer buffer = blockBuffers[iob];
+ buffer.values[jf] = interleavedBuffer[inIndex++];
+ }
+ }
+ return inIndex;
+ }
+
+ int interleave(int outIndex) {
+ for (int jf = 0; jf < Synthesizer.FRAMES_PER_BLOCK; jf++) {
+ for (int iob = 0; iob < blockBuffers.length; iob++) {
+ ChannelBlockBuffer buffer = blockBuffers[iob];
+ interleavedBuffer[outIndex++] = buffer.values[jf];
+ }
+ }
+ return outIndex;
+ }
+
+ public double[] getChannelBuffer(int i) {
+ return blockBuffers[i].values;
+ }
+
+ public void clear() {
+ for (int i = 0; i < blockBuffers.length; i++) {
+ blockBuffers[i].clear();
+ }
+ }
+ }
+
+ class ChannelBlockBuffer {
+ private double[] values;
+
+ ChannelBlockBuffer(int framesPerBlock) {
+ values = new double[framesPerBlock];
+ }
+
+ void clear() {
+ for (int i = 0; i < values.length; i++) {
+ values[i] = 0.0f;
+ }
+ }
+ }
+
+ @Override
+ public void start() {
+ // TODO Use constants.
+ start(DEFAULT_FRAME_RATE, -1, 0, -1, 2);
+ }
+
+ @Override
+ public void start(int frameRate) {
+ // TODO Use constants.
+ start(frameRate, -1, 0, -1, 2);
+ }
+
+ @Override
+ public synchronized void start(int frameRate, int inputDeviceID, int numInputChannels,
+ int outputDeviceID, int numOutputChannels) {
+ if (started) {
+ logger.info("JSyn already started.");
+ return;
+ }
+
+ this.frameRate = frameRate;
+ this.framePeriod = 1.0 / frameRate;
+
+ // Set rate for any units that have already been added.
+ for (UnitGenerator ugen : allUnitList) {
+ ugen.setFrameRate(frameRate);
+ }
+
+ // this.numInputChannels = numInputChannels;
+ // this.numOutputChannels = numOutputChannels;
+ setupAudioBuffers(numInputChannels, numOutputChannels);
+
+ logger.info("Pure Java JSyn from www.softsynth.com, rate = " + frameRate + ", "
+ + (useRealTime ? "RT" : "NON-RealTime") + ", " + JSyn.VERSION_TEXT);
+
+ inverseNyquist = 2.0 / frameRate;
+
+ if (useRealTime) {
+ if (numInputChannels > 0) {
+ audioInputStream = audioDeviceManager.createInputStream(inputDeviceID, frameRate,
+ numInputChannels);
+ }
+ if (numOutputChannels > 0) {
+ audioOutputStream = audioDeviceManager.createOutputStream(outputDeviceID,
+ frameRate, numOutputChannels);
+ }
+ audioThread = new Thread(this);
+ logger.fine("Synth thread old priority = " + audioThread.getPriority());
+ audioThread.setPriority(audioThread.getPriority() + 2);
+ logger.fine("Synth thread new priority = " + audioThread.getPriority());
+ go = true;
+ audioThread.start();
+ }
+
+ started = true;
+ }
+
+ @Override
+ public boolean isRunning() {
+ return go;
+ }
+
+ @Override
+ public synchronized void stop() {
+ if (!started) {
+ logger.info("JSyn already stopped.");
+ return;
+ }
+
+ if (useRealTime) {
+ // Stop audio synthesis and all units.
+ go = false;
+ if (audioThread != null) {
+ try {
+ // Interrupt now, otherwise audio thread will wait for audio I/O.
+ audioThread.interrupt();
+
+ audioThread.join(1000);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ synchronized (runningUnitList) {
+ runningUnitList.clear();
+ }
+ started = false;
+ }
+
+ @Override
+ public void run() {
+ logger.fine("JSyn synthesis thread starting.");
+ try {
+ if (audioInputStream != null) {
+ logger.finer("JSyn synthesis thread trying to start audio INPUT!");
+ audioInputStream.start();
+ String msg = String.format("Input Latency in = %5.1f msec",
+ 1000 * audioInputStream.getLatency());
+ logger.fine(msg);
+ }
+ if (audioOutputStream != null) {
+ logger.finer("JSyn synthesis thread trying to start audio OUTPUT!");
+ audioOutputStream.start();
+ String msg = String.format("Output Latency = %5.1f msec",
+ 1000 * audioOutputStream.getLatency());
+ logger.fine(msg);
+ // Buy some time while we fill the buffer.
+ audioOutputStream.write(outputBuffer.interleavedBuffer);
+ }
+ loadAnalyzer = new LoadAnalyzer();
+ while (go) {
+ if (audioInputStream != null) {
+ audioInputStream.read(inputBuffer.interleavedBuffer);
+ }
+
+ loadAnalyzer.start();
+ runAudioTasks();
+ generateNextBuffer();
+ loadAnalyzer.stop();
+
+ if (audioOutputStream != null) {
+ // This call will block when the output is full.
+ audioOutputStream.write(outputBuffer.interleavedBuffer);
+ }
+ }
+
+ } catch (Throwable e) {
+ e.printStackTrace();
+ go = false;
+
+ } finally {
+ logger.info("JSyn synthesis thread in finally code.");
+ // Stop audio system.
+ if (audioInputStream != null) {
+ audioInputStream.stop();
+ }
+ if (audioOutputStream != null) {
+ audioOutputStream.stop();
+ }
+ }
+ logger.fine("JSyn synthesis thread exiting.");
+ }
+
+ private void runAudioTasks() {
+ for (Runnable task : audioTasks) {
+ task.run();
+ }
+ }
+
+ // TODO We need to implement a sharedSleeper like we use in JSyn1.
+ public void generateNextBuffer() {
+ int outIndex = 0;
+ int inIndex = 0;
+ for (int i = 0; i < BLOCKS_PER_BUFFER; i++) {
+ if (inputBuffer != null) {
+ inIndex = inputBuffer.deinterleave(inIndex);
+ }
+
+ TimeStamp timeStamp = createTimeStamp();
+ // Try putting this up here so incoming time-stamped events will get
+ // scheduled later.
+ processScheduledCommands(timeStamp);
+ clearBlockBuffers();
+ synthesizeBuffer();
+
+ if (outputBuffer != null) {
+ outIndex = outputBuffer.interleave(outIndex);
+ }
+ frameCount += Synthesizer.FRAMES_PER_BLOCK;
+ }
+ }
+
+ @Override
+ public double getCurrentTime() {
+ return frameCount * framePeriod;
+ }
+
+ @Override
+ public TimeStamp createTimeStamp() {
+ return new TimeStamp(getCurrentTime());
+ }
+
+ private void processScheduledCommands(TimeStamp timeStamp) {
+ List<ScheduledCommand> timeList = commandQueue.removeNextList(timeStamp);
+
+ while (timeList != null) {
+ while (!timeList.isEmpty()) {
+ ScheduledCommand command = timeList.remove(0);
+ logger.fine("processing " + command + ", at time " + timeStamp.getTime());
+ command.run();
+ }
+ // Get next list of commands at the given time.
+ timeList = commandQueue.removeNextList(timeStamp);
+ }
+ }
+
+ @Override
+ public void scheduleCommand(TimeStamp timeStamp, ScheduledCommand command) {
+ if ((Thread.currentThread() == audioThread) && (timeStamp.getTime() <= getCurrentTime())) {
+ command.run();
+ } else {
+ logger.fine("scheduling " + command + ", at time " + timeStamp.getTime());
+ commandQueue.add(timeStamp, command);
+ }
+ }
+
+ @Override
+ public void scheduleCommand(double time, ScheduledCommand command) {
+ TimeStamp timeStamp = new TimeStamp(time);
+ scheduleCommand(timeStamp, command);
+ }
+
+ @Override
+ public void queueCommand(ScheduledCommand command) {
+ TimeStamp timeStamp = createTimeStamp();
+ scheduleCommand(timeStamp, command);
+ }
+
+ private void clearBlockBuffers() {
+ outputBuffer.clear();
+ }
+
+ private void synthesizeBuffer() {
+ synchronized (runningUnitList) {
+ ListIterator<UnitGenerator> iterator = runningUnitList.listIterator();
+ while (iterator.hasNext()) {
+ UnitGenerator unit = iterator.next();
+ if (pullDataEnabled) {
+ unit.pullData(getFrameCount(), 0, Synthesizer.FRAMES_PER_BLOCK);
+ } else {
+ unit.generate(0, Synthesizer.FRAMES_PER_BLOCK);
+ }
+ }
+ // Remove any units that got auto stopped.
+ for (UnitGenerator ugen : stoppingUnitList) {
+ runningUnitList.remove(ugen);
+ ugen.flattenOutputs();
+ }
+ }
+ stoppingUnitList.clear();
+ }
+
+ public double[] getInputBuffer(int i) {
+ try {
+ return inputBuffer.getChannelBuffer(i);
+ } catch (ArrayIndexOutOfBoundsException e) {
+ throw new RuntimeException("Audio Input not configured in start() method.");
+ }
+ }
+
+ public double[] getOutputBuffer(int i) {
+ try {
+ return outputBuffer.getChannelBuffer(i);
+ } catch (ArrayIndexOutOfBoundsException e) {
+ throw new RuntimeException("Audio Output not configured in start() method.");
+ }
+ }
+
+ private void internalStopUnit(UnitGenerator unit) {
+ synchronized (runningUnitList) {
+ runningUnitList.remove(unit);
+ }
+ unit.flattenOutputs();
+ }
+
+ public void autoStopUnit(UnitGenerator unitGenerator) {
+ synchronized (stoppingUnitList) {
+ stoppingUnitList.add(unitGenerator);
+ }
+ }
+
+ @Override
+ public void startUnit(UnitGenerator unit, double time) {
+ startUnit(unit, new TimeStamp(time));
+ }
+
+ @Override
+ public void stopUnit(UnitGenerator unit, double time) {
+ stopUnit(unit, new TimeStamp(time));
+ }
+
+ @Override
+ public void startUnit(final UnitGenerator unit, TimeStamp timeStamp) {
+ // Don't start if it is a component in a circuit because it will be
+ // executed by the circuit.
+ if (unit.getCircuit() == null) {
+ scheduleCommand(timeStamp, new ScheduledCommand() {
+ @Override
+ public void run() {
+ internalStartUnit(unit);
+ }
+ });
+ }
+ }
+
+ @Override
+ public void stopUnit(final UnitGenerator unit, TimeStamp timeStamp) {
+ scheduleCommand(timeStamp, new ScheduledCommand() {
+ @Override
+ public void run() {
+ internalStopUnit(unit);
+ }
+ });
+ }
+
+ @Override
+ public void startUnit(UnitGenerator unit) {
+ startUnit(unit, createTimeStamp());
+ }
+
+ @Override
+ public void stopUnit(UnitGenerator unit) {
+ stopUnit(unit, createTimeStamp());
+ }
+
+ private void internalStartUnit(UnitGenerator unit) {
+ // logger.info( "internalStartUnit " + unit + " with circuit " +
+ // unit.getCircuit() );
+ if (unit.getCircuit() == null) {
+ synchronized (runningUnitList) {
+ if (!runningUnitList.contains(unit)) {
+ runningUnitList.add(unit);
+ }
+ }
+ }
+ // else
+ // {
+ // logger.info(
+ // "internalStartUnit detected race condition !!!! from old JSyn" + unit
+ // + " with circuit " + unit.getCircuit() );
+ // }
+ }
+
+ public double getInverseNyquist() {
+ return inverseNyquist;
+ }
+
+ public double convertTimeToExponentialScaler(double duration) {
+ // Calculate scaler so that scaler^frames = target/source
+ double numFrames = duration * getFrameRate();
+ return Math.pow(DB90, (1.0 / numFrames));
+ }
+
+ @Override
+ public long getFrameCount() {
+ return frameCount;
+ }
+
+ /**
+ * @return the frameRate
+ */
+ @Override
+ public int getFrameRate() {
+ return frameRate;
+ }
+
+ /**
+ * @return the inverse of the frameRate for efficiency
+ */
+ @Override
+ public double getFramePeriod() {
+ return framePeriod;
+ }
+
+ /** Convert a short value to a double in the range -1.0 to almost 1.0. */
+ public static double convertShortToDouble(short sdata) {
+ return (sdata * (1.0 / Short.MAX_VALUE));
+ }
+
+ /**
+ * Convert a double value in the range -1.0 to almost 1.0 to a short. Double value is clipped
+ * before converting.
+ */
+ public static short convertDoubleToShort(double d) {
+ final double maxValue = ((double) (Short.MAX_VALUE - 1)) / Short.MAX_VALUE;
+ if (d > maxValue) {
+ d = maxValue;
+ } else if (d < -1.0) {
+ d = -1.0;
+ }
+ return (short) (d * Short.MAX_VALUE);
+ }
+
+ @Override
+ public void addAudioTask(Runnable blockTask) {
+ audioTasks.add(blockTask);
+ }
+
+ @Override
+ public void removeAudioTask(Runnable blockTask) {
+ audioTasks.remove(blockTask);
+ }
+
+ @Override
+ public double getUsage() {
+ // use temp so we don't have to synchronize
+ LoadAnalyzer temp = loadAnalyzer;
+ if (temp != null) {
+ return temp.getAverageLoad();
+ } else {
+ return 0.0;
+ }
+ }
+
+ @Override
+ public AudioDeviceManager getAudioDeviceManager() {
+ return audioDeviceManager;
+ }
+
+ @Override
+ public void setRealTime(boolean realTime) {
+ useRealTime = realTime;
+ }
+
+ @Override
+ public boolean isRealTime() {
+ return useRealTime;
+ }
+
+ public double getOutputLatency() {
+ if (audioOutputStream != null) {
+ return audioOutputStream.getLatency();
+ } else {
+ return 0;
+ }
+ }
+
+ public double getInputLatency() {
+ if (audioInputStream != null) {
+ return audioInputStream.getLatency();
+ } else {
+ return 0;
+ }
+ }
+
+ @Override
+ public void add(UnitGenerator ugen) {
+ ugen.setSynthesisEngine(this);
+ allUnitList.add(ugen);
+ if (frameRate > 0) {
+ ugen.setFrameRate(frameRate);
+ }
+ }
+
+ @Override
+ public void remove(UnitGenerator ugen) {
+ allUnitList.remove(ugen);
+ }
+
+ @Override
+ public void sleepUntil(double time) throws InterruptedException {
+ double timeToSleep = time - getCurrentTime();
+ while (timeToSleep > 0.0) {
+ if (useRealTime) {
+ long msecToSleep = (long) (1000 * timeToSleep);
+ if (msecToSleep <= 0) {
+ msecToSleep = 1;
+ }
+ Thread.sleep(msecToSleep);
+ } else {
+
+ generateNextBuffer();
+ }
+ timeToSleep = time - getCurrentTime();
+ }
+ }
+
+ @Override
+ public void sleepFor(double duration) throws InterruptedException {
+ sleepUntil(getCurrentTime() + duration);
+ }
+
+ public void printConnections() {
+ if (pullDataEnabled) {
+ ListIterator<UnitGenerator> iterator = runningUnitList.listIterator();
+ while (iterator.hasNext()) {
+ UnitGenerator unit = iterator.next();
+ unit.printConnections();
+ }
+ }
+
+ }
+
+}
diff --git a/src/com/jsyn/exceptions/ChannelMismatchException.java b/src/com/jsyn/exceptions/ChannelMismatchException.java
new file mode 100644
index 0000000..a1554cd
--- /dev/null
+++ b/src/com/jsyn/exceptions/ChannelMismatchException.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.exceptions;
+
+/**
+ * This will get thrown if, for example, stereo data is queued to a mono player.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class ChannelMismatchException extends RuntimeException {
+
+ public ChannelMismatchException(String message) {
+ super(message);
+ }
+
+ /**
+ *
+ */
+ private static final long serialVersionUID = -5345224363387498119L;
+
+}
diff --git a/src/com/jsyn/instruments/DrumWoodFM.java b/src/com/jsyn/instruments/DrumWoodFM.java
new file mode 100644
index 0000000..ba6cd1b
--- /dev/null
+++ b/src/com/jsyn/instruments/DrumWoodFM.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2011 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.instruments;
+
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.ports.UnitOutputPort;
+import com.jsyn.unitgen.Add;
+import com.jsyn.unitgen.Circuit;
+import com.jsyn.unitgen.EnvelopeAttackDecay;
+import com.jsyn.unitgen.Multiply;
+import com.jsyn.unitgen.PassThrough;
+import com.jsyn.unitgen.SineOscillator;
+import com.jsyn.unitgen.SineOscillatorPhaseModulated;
+import com.jsyn.unitgen.UnitVoice;
+import com.jsyn.util.VoiceDescription;
+import com.softsynth.shared.time.TimeStamp;
+
+/**
+ * Drum instruments using 2 Operator FM.
+ *
+ * @author Phil Burk (C) 2011 Mobileer Inc
+ */
+public class DrumWoodFM extends Circuit implements UnitVoice {
+ private static final int NUM_PRESETS = 3;
+ // Declare units and ports.
+ EnvelopeAttackDecay ampEnv;
+ SineOscillatorPhaseModulated carrierOsc;
+ EnvelopeAttackDecay modEnv;
+ SineOscillator modOsc;
+ PassThrough freqDistributor;
+ Add modSummer;
+ Multiply frequencyMultiplier;
+
+ public UnitInputPort mcratio;
+ public UnitInputPort index;
+ public UnitInputPort modRange;
+ public UnitInputPort frequency;
+
+ public DrumWoodFM() {
+ // Create unit generators.
+ add(carrierOsc = new SineOscillatorPhaseModulated());
+ add(freqDistributor = new PassThrough());
+ add(modSummer = new Add());
+ add(ampEnv = new EnvelopeAttackDecay());
+ add(modEnv = new EnvelopeAttackDecay());
+ add(modOsc = new SineOscillator());
+ add(frequencyMultiplier = new Multiply());
+
+ addPort(mcratio = frequencyMultiplier.inputB, "MCRatio");
+ addPort(index = modSummer.inputA, "Index");
+ addPort(modRange = modEnv.amplitude, "ModRange");
+ addPort(frequency = freqDistributor.input, "Frequency");
+
+ ampEnv.export(this, "Amp");
+ modEnv.export(this, "Mod");
+
+ freqDistributor.output.connect(carrierOsc.frequency);
+ freqDistributor.output.connect(frequencyMultiplier.inputA);
+
+ carrierOsc.output.connect(ampEnv.amplitude);
+ modEnv.output.connect(modSummer.inputB);
+ modSummer.output.connect(modOsc.amplitude);
+ modOsc.output.connect(carrierOsc.modulation);
+ frequencyMultiplier.output.connect(modOsc.frequency);
+
+ // Make the circuit turn off when the envelope finishes to reduce CPU load.
+ ampEnv.setupAutoDisable(this);
+
+ usePreset(0);
+ }
+
+ @Override
+ public void noteOff(TimeStamp timeStamp) {
+ }
+
+ @Override
+ public void noteOn(double freq, double ampl, TimeStamp timeStamp) {
+ carrierOsc.amplitude.set(ampl, timeStamp);
+ ampEnv.input.trigger(timeStamp);
+ modEnv.input.trigger(timeStamp);
+ }
+
+ @Override
+ public UnitOutputPort getOutput() {
+ return ampEnv.output;
+ }
+
+ @Override
+ public void usePreset(int presetIndex) {
+ mcratio.setup(0.001, 0.6875, 20.0);
+ ampEnv.attack.setup(0.001, 0.005, 8.0);
+ modEnv.attack.setup(0.001, 0.005, 8.0);
+
+ int n = presetIndex % NUM_PRESETS;
+ switch (n) {
+ case 0:
+ ampEnv.decay.setup(0.001, 0.293, 8.0);
+ modEnv.decay.setup(0.001, 0.07, 8.0);
+ frequency.setup(0.0, 349.0, 3000.0);
+ index.setup(0.001, 0.05, 10.0);
+ modRange.setup(0.001, 0.4, 10.0);
+ break;
+ case 1:
+ default:
+ ampEnv.decay.setup(0.001, 0.12, 8.0);
+ modEnv.decay.setup(0.001, 0.06, 8.0);
+ frequency.setup(0.0, 1400.0, 3000.0);
+ index.setup(0.001, 0.16, 10.0);
+ modRange.setup(0.001, 0.17, 10.0);
+ break;
+ }
+ }
+
+ static class MyVoiceDescription extends VoiceDescription {
+ static String[] presetNames = {
+ "WoodBlockFM", "ClaveFM"
+ };
+ static String[] tags = {
+ "electronic", "drum"
+ };
+
+ public MyVoiceDescription() {
+ super("DrumWoodFM", presetNames);
+ }
+
+ @Override
+ public UnitVoice createUnitVoice() {
+ return new DrumWoodFM();
+ }
+
+ @Override
+ public String[] getTags(int presetIndex) {
+ return tags;
+ }
+
+ @Override
+ public String getVoiceClassName() {
+ return DrumWoodFM.class.getName();
+ }
+ }
+
+ public static VoiceDescription getVoiceDescription() {
+ return new MyVoiceDescription();
+ }
+}
diff --git a/src/com/jsyn/instruments/JSynInstrumentLibrary.java b/src/com/jsyn/instruments/JSynInstrumentLibrary.java
new file mode 100644
index 0000000..c5ed91e
--- /dev/null
+++ b/src/com/jsyn/instruments/JSynInstrumentLibrary.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2011 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.instruments;
+
+import com.jsyn.swing.InstrumentBrowser;
+import com.jsyn.util.InstrumentLibrary;
+import com.jsyn.util.VoiceDescription;
+
+/**
+ * Stock instruments provided with the JSyn distribution.
+ *
+ * @author Phil Burk (C) 2011 Mobileer Inc
+ * @see InstrumentBrowser
+ */
+
+public class JSynInstrumentLibrary implements InstrumentLibrary {
+ static VoiceDescription[] descriptions = {
+ WaveShapingVoice.getVoiceDescription(), SubtractiveSynthVoice.getVoiceDescription(),
+ NoiseHit.getVoiceDescription(), DrumWoodFM.getVoiceDescription()
+ };
+
+ @Override
+ public VoiceDescription[] getVoiceDescriptions() {
+ return descriptions;
+ }
+
+ @Override
+ public String getName() {
+ return "JSynInstruments";
+ }
+}
diff --git a/src/com/jsyn/instruments/NoiseHit.java b/src/com/jsyn/instruments/NoiseHit.java
new file mode 100644
index 0000000..b8714fc
--- /dev/null
+++ b/src/com/jsyn/instruments/NoiseHit.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.instruments;
+
+import com.jsyn.ports.UnitOutputPort;
+import com.jsyn.unitgen.Circuit;
+import com.jsyn.unitgen.EnvelopeAttackDecay;
+import com.jsyn.unitgen.PinkNoise;
+import com.jsyn.unitgen.UnitVoice;
+import com.jsyn.util.VoiceDescription;
+import com.softsynth.shared.time.TimeStamp;
+
+/**
+ * Cheap synthetic cymbal sound.
+ */
+public class NoiseHit extends Circuit implements UnitVoice {
+ EnvelopeAttackDecay ampEnv;
+ PinkNoise noise;
+ private static final int NUM_PRESETS = 3;
+
+ public NoiseHit() {
+ // Create unit generators.
+ add(noise = new PinkNoise());
+ add(ampEnv = new EnvelopeAttackDecay());
+ noise.output.connect(ampEnv.amplitude);
+
+ ampEnv.export(this, "Amp");
+
+ // Make the circuit turn off when the envelope finishes to reduce CPU load.
+ ampEnv.setupAutoDisable(this);
+
+ usePreset(0);
+ }
+
+ @Override
+ public void noteOff(TimeStamp timeStamp) {
+ }
+
+ @Override
+ public void noteOn(double freq, double ampl, TimeStamp timeStamp) {
+ noise.amplitude.set(ampl, timeStamp);
+ ampEnv.input.trigger();
+ }
+
+ @Override
+ public UnitOutputPort getOutput() {
+ return ampEnv.output;
+ }
+
+ @Override
+ public void usePreset(int presetIndex) {
+ int n = presetIndex % NUM_PRESETS;
+ switch (n) {
+ case 0:
+ ampEnv.attack.set(0.001);
+ ampEnv.decay.set(0.1);
+ break;
+ case 1:
+ ampEnv.attack.set(0.03);
+ ampEnv.decay.set(1.4);
+ break;
+ default:
+ ampEnv.attack.set(0.9);
+ ampEnv.decay.set(0.3);
+ break;
+ }
+ }
+
+ static class MyVoiceDescription extends VoiceDescription {
+ static String[] presetNames = {
+ "ShortNoiseHit", "LongNoiseHit", "SlowNoiseHit"
+ };
+ static String[] tags = {
+ "electronic", "noise"
+ };
+
+ public MyVoiceDescription() {
+ super("NoiseHit", presetNames);
+ }
+
+ @Override
+ public UnitVoice createUnitVoice() {
+ return new NoiseHit();
+ }
+
+ @Override
+ public String[] getTags(int presetIndex) {
+ return tags;
+ }
+
+ @Override
+ public String getVoiceClassName() {
+ return NoiseHit.class.getName();
+ }
+ }
+
+ public static VoiceDescription getVoiceDescription() {
+ return new MyVoiceDescription();
+ }
+}
diff --git a/src/com/jsyn/instruments/SubtractiveSynthVoice.java b/src/com/jsyn/instruments/SubtractiveSynthVoice.java
new file mode 100644
index 0000000..9ea593e
--- /dev/null
+++ b/src/com/jsyn/instruments/SubtractiveSynthVoice.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.instruments;
+
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.ports.UnitOutputPort;
+import com.jsyn.unitgen.Add;
+import com.jsyn.unitgen.Circuit;
+import com.jsyn.unitgen.EnvelopeDAHDSR;
+import com.jsyn.unitgen.FilterLowPass;
+import com.jsyn.unitgen.Multiply;
+import com.jsyn.unitgen.SawtoothOscillatorBL;
+import com.jsyn.unitgen.UnitOscillator;
+import com.jsyn.unitgen.UnitVoice;
+import com.jsyn.util.VoiceDescription;
+import com.softsynth.shared.time.TimeStamp;
+
+/**
+ * Typical synthesizer voice with an oscillator and resonant filter. Modulate the amplitude and
+ * filter using DAHDSR envelopes.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class SubtractiveSynthVoice extends Circuit implements UnitVoice {
+ private static final long serialVersionUID = -2704222221111608377L;
+ private UnitOscillator osc;
+ private FilterLowPass filter;
+ private EnvelopeDAHDSR ampEnv;
+ private EnvelopeDAHDSR filterEnv;
+ private Add cutoffAdder;
+ private Multiply frequencyScaler;
+
+ public UnitInputPort amplitude;
+ public UnitInputPort frequency;
+ /**
+ * This scales the frequency value. You can use this to modulate a group of instruments using a
+ * shared LFO and they will stay in tune.
+ */
+ public UnitInputPort pitchModulation;
+ public UnitInputPort cutoff;
+ public UnitInputPort cutoffRange;
+ public UnitInputPort Q;
+
+ public SubtractiveSynthVoice() {
+ add(frequencyScaler = new Multiply());
+ // Add a tone generator.
+ add(osc = new SawtoothOscillatorBL());
+
+ // Use an envelope to control the amplitude.
+ add(ampEnv = new EnvelopeDAHDSR());
+
+ // Use an envelope to control the filter cutoff.
+ add(filterEnv = new EnvelopeDAHDSR());
+ add(filter = new FilterLowPass());
+ add(cutoffAdder = new Add());
+
+ filterEnv.output.connect(cutoffAdder.inputA);
+ cutoffAdder.output.connect(filter.frequency);
+ frequencyScaler.output.connect(osc.frequency);
+ osc.output.connect(filter.input);
+ filter.output.connect(ampEnv.amplitude);
+
+ addPort(amplitude = osc.amplitude, "Amplitude");
+ addPort(frequency = frequencyScaler.inputA, "Frequency");
+ addPort(pitchModulation = frequencyScaler.inputB, "PitchMod");
+ addPort(cutoff = cutoffAdder.inputB, "Cutoff");
+ addPort(cutoffRange = filterEnv.amplitude, "CutoffRange");
+ addPort(Q = filter.Q);
+
+ ampEnv.export(this, "Amp");
+ filterEnv.export(this, "Filter");
+
+ frequency.setup(osc.frequency);
+ pitchModulation.setup(0.2, 1.0, 4.0);
+ cutoff.setup(filter.frequency);
+ cutoffRange.setup(filter.frequency);
+
+ // Make the circuit turn off when the envelope finishes to reduce CPU load.
+ ampEnv.setupAutoDisable(this);
+
+ usePreset(0);
+ }
+
+ @Override
+ public void noteOff(TimeStamp timeStamp) {
+ ampEnv.input.off(timeStamp);
+ filterEnv.input.off(timeStamp);
+ }
+
+ @Override
+ public void noteOn(double freq, double ampl, TimeStamp timeStamp) {
+ frequency.set(freq, timeStamp);
+ amplitude.set(ampl, timeStamp);
+
+ ampEnv.input.on(timeStamp);
+ filterEnv.input.on(timeStamp);
+ }
+
+ @Override
+ public UnitOutputPort getOutput() {
+ return ampEnv.output;
+ }
+
+ @Override
+ public void usePreset(int presetIndex) {
+ int n = presetIndex % presetNames.length;
+ switch (n) {
+ case 0:
+ ampEnv.attack.set(0.01);
+ ampEnv.decay.set(0.2);
+ ampEnv.release.set(1.0);
+ cutoff.set(500.0);
+ cutoffRange.set(500.0);
+ filter.Q.set(1.0);
+ break;
+ case 1:
+ ampEnv.attack.set(0.5);
+ ampEnv.decay.set(0.3);
+ ampEnv.release.set(0.2);
+ cutoff.set(500.0);
+ cutoffRange.set(500.0);
+ filter.Q.set(3.0);
+ break;
+ default:
+ ampEnv.attack.set(0.1);
+ ampEnv.decay.set(0.3);
+ ampEnv.release.set(0.5);
+ cutoff.set(2000.0);
+ cutoffRange.set(500.0);
+ filter.Q.set(2.0);
+ break;
+ }
+ }
+
+ static String[] presetNames = {
+ "FastSaw", "SlowSaw", "BrightSaw", ""
+ };
+
+ static class MyVoiceDescription extends VoiceDescription {
+ String[] tags = {
+ "electronic", "filter", "clean"
+ };
+
+ public MyVoiceDescription() {
+ super("SubtractiveSynth", presetNames);
+ }
+
+ @Override
+ public UnitVoice createUnitVoice() {
+ return new SubtractiveSynthVoice();
+ }
+
+ @Override
+ public String[] getTags(int presetIndex) {
+ return tags;
+ }
+
+ @Override
+ public String getVoiceClassName() {
+ return SubtractiveSynthVoice.class.getName();
+ }
+ }
+
+ public static VoiceDescription getVoiceDescription() {
+ return new MyVoiceDescription();
+ }
+
+}
diff --git a/src/com/jsyn/instruments/WaveShapingVoice.java b/src/com/jsyn/instruments/WaveShapingVoice.java
new file mode 100644
index 0000000..5044f21
--- /dev/null
+++ b/src/com/jsyn/instruments/WaveShapingVoice.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright 2011 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.instruments;
+
+import com.jsyn.data.DoubleTable;
+import com.jsyn.ports.UnitFunctionPort;
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.ports.UnitOutputPort;
+import com.jsyn.unitgen.Circuit;
+import com.jsyn.unitgen.EnvelopeDAHDSR;
+import com.jsyn.unitgen.FunctionEvaluator;
+import com.jsyn.unitgen.Multiply;
+import com.jsyn.unitgen.SineOscillator;
+import com.jsyn.unitgen.UnitOscillator;
+import com.jsyn.unitgen.UnitVoice;
+import com.jsyn.util.VoiceDescription;
+import com.softsynth.math.ChebyshevPolynomial;
+import com.softsynth.math.PolynomialTableData;
+import com.softsynth.shared.time.TimeStamp;
+
+/**
+ * Waveshaping oscillator with envelopes.
+ *
+ * @author Phil Burk (C) 2011 Mobileer Inc
+ */
+public class WaveShapingVoice extends Circuit implements UnitVoice {
+ private static final long serialVersionUID = -2704222221111608377L;
+ private static final int NUM_PRESETS = 3;
+ private UnitOscillator osc;
+ private FunctionEvaluator waveShaper;
+ private EnvelopeDAHDSR ampEnv;
+ private EnvelopeDAHDSR rangeEnv;
+ private Multiply frequencyScaler;
+
+ public UnitInputPort range;
+ public UnitInputPort frequency;
+ public UnitInputPort amplitude;
+ public UnitFunctionPort function;
+ public UnitInputPort pitchModulation;
+
+ // default Chebyshev polynomial table to share.
+ private static DoubleTable chebyshevTable;
+ private final static int CHEBYSHEV_ORDER = 11;
+
+ static {
+ // Make table with Chebyshev polynomial to share among voices
+ PolynomialTableData chebData = new PolynomialTableData(
+ ChebyshevPolynomial.T(CHEBYSHEV_ORDER), 1024);
+ chebyshevTable = new DoubleTable(chebData.getData());
+ }
+
+ public WaveShapingVoice() {
+ add(frequencyScaler = new Multiply());
+ add(osc = new SineOscillator());
+ add(waveShaper = new FunctionEvaluator());
+ add(rangeEnv = new EnvelopeDAHDSR());
+ add(ampEnv = new EnvelopeDAHDSR());
+
+ addPort(amplitude = ampEnv.amplitude);
+ addPort(range = osc.amplitude, "Range");
+ addPort(function = waveShaper.function);
+ addPort(frequency = frequencyScaler.inputA, "Frequency");
+ addPort(pitchModulation = frequencyScaler.inputB, "PitchMod");
+
+ ampEnv.export(this, "Amp");
+ rangeEnv.export(this, "Range");
+
+ function.set(chebyshevTable);
+
+ // Connect units.
+ osc.output.connect(rangeEnv.amplitude);
+ rangeEnv.output.connect(waveShaper.input);
+ ampEnv.output.connect(waveShaper.amplitude);
+ frequencyScaler.output.connect(osc.frequency);
+
+ // Set reasonable defaults for the ports.
+ pitchModulation.setup(0.1, 1.0, 10.0);
+ range.setup(0.1, 0.8, 1.0);
+ frequency.setup(osc.frequency);
+ amplitude.setup(0.0, 0.5, 1.0);
+
+ // Make the circuit turn off when the envelope finishes to reduce CPU load.
+ ampEnv.setupAutoDisable(this);
+
+ usePreset(2);
+ }
+
+ @Override
+ public UnitOutputPort getOutput() {
+ return waveShaper.output;
+ }
+
+ @Override
+ public void noteOn(double freq, double amp, TimeStamp timeStamp) {
+ frequency.set(freq, timeStamp);
+ amplitude.set(amp, timeStamp);
+ ampEnv.input.on(timeStamp);
+ rangeEnv.input.on(timeStamp);
+ }
+
+ @Override
+ public void noteOff(TimeStamp timeStamp) {
+ ampEnv.input.off(timeStamp);
+ rangeEnv.input.off(timeStamp);
+ }
+
+ @Override
+ public void usePreset(int presetIndex) {
+ int n = presetIndex % NUM_PRESETS;
+ switch (n) {
+ case 0:
+ ampEnv.attack.set(0.01);
+ ampEnv.decay.set(0.2);
+ ampEnv.release.set(1.0);
+ rangeEnv.attack.set(0.01);
+ rangeEnv.decay.set(0.2);
+ rangeEnv.sustain.set(0.4);
+ rangeEnv.release.set(1.0);
+ break;
+ case 1:
+ ampEnv.attack.set(0.5);
+ ampEnv.decay.set(0.3);
+ ampEnv.release.set(0.2);
+ rangeEnv.attack.set(0.03);
+ rangeEnv.decay.set(0.2);
+ rangeEnv.sustain.set(0.5);
+ rangeEnv.release.set(1.0);
+ break;
+ default:
+ ampEnv.attack.set(0.1);
+ ampEnv.decay.set(0.3);
+ ampEnv.release.set(0.5);
+ rangeEnv.attack.set(0.01);
+ rangeEnv.decay.set(0.2);
+ rangeEnv.sustain.set(0.9);
+ rangeEnv.release.set(1.0);
+ break;
+ }
+ }
+
+ static class MyVoiceDescription extends VoiceDescription {
+ static String[] presetNames = {
+ "FastChebyshev", "SlowChebyshev", "BrightChebyshev"
+ };
+ static String[] tags = {
+ "electronic", "waveshaping", "clean"
+ };
+
+ public MyVoiceDescription() {
+ super("Waveshaping", presetNames);
+ }
+
+ @Override
+ public UnitVoice createUnitVoice() {
+ return new WaveShapingVoice();
+ }
+
+ @Override
+ public String[] getTags(int presetIndex) {
+ return tags;
+ }
+
+ @Override
+ public String getVoiceClassName() {
+ return WaveShapingVoice.class.getName();
+ }
+ }
+
+ public static VoiceDescription getVoiceDescription() {
+ return new MyVoiceDescription();
+ }
+
+}
diff --git a/src/com/jsyn/io/AudioFifo.java b/src/com/jsyn/io/AudioFifo.java
new file mode 100644
index 0000000..43f16d3
--- /dev/null
+++ b/src/com/jsyn/io/AudioFifo.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.io;
+
+/**
+ * FIFO that implements AudioInputStream, AudioOutputStream interfaces. This can be used to send
+ * audio data between different threads. The reads or writes may or may not wait based on flags.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class AudioFifo implements AudioInputStream, AudioOutputStream {
+ // These indices run double the FIFO size so that we can tell empty from full.
+ private volatile int readIndex;
+ private volatile int writeIndex;
+ private volatile double[] buffer;
+ // Used to mask the index into range when accessing the buffer array.
+ private int accessMask;
+ // Used to mask the index so it wraps around.
+ private int sizeMask;
+ private boolean writeWaitEnabled = true;
+ private boolean readWaitEnabled = true;
+ private Object writeSemaphore = new Object();
+ private Object readSemaphore = new Object();
+
+ /**
+ * @param size Number of doubles in the FIFO. Must be a power of 2. Eg. 1024.
+ */
+ public void allocate(int size) {
+ if (!isPowerOfTwo(size)) {
+ throw new IllegalArgumentException("Size must be a power of two.");
+ }
+ buffer = new double[size];
+ accessMask = size - 1;
+ sizeMask = (size * 2) - 1;
+ }
+
+ public int size() {
+ return buffer.length;
+ }
+
+ public static boolean isPowerOfTwo(int size) {
+ return ((size & (size - 1)) == 0);
+ }
+
+ /** How many samples are available for reading without blocking? */
+ @Override
+ public int available() {
+ return (writeIndex - readIndex) & sizeMask;
+ }
+
+ @Override
+ public void close() {
+ // TODO Maybe we should tell any thread that is waiting that the FIFO is closed.
+ }
+
+ @Override
+ public double read() {
+ double value = Double.NaN;
+ if (readWaitEnabled) {
+ try {
+ while (available() < 1) {
+ synchronized (writeSemaphore) {
+ writeSemaphore.wait();
+ }
+ }
+ value = readOneInternal();
+ } catch (InterruptedException e) {
+ }
+ } else {
+ if (readIndex != writeIndex) {
+ value = readOneInternal();
+ }
+ }
+ return value;
+ }
+
+ private double readOneInternal() {
+ double value = buffer[readIndex & accessMask];
+ readIndex = (readIndex + 1) & sizeMask;
+ if (writeWaitEnabled) {
+ synchronized (readSemaphore) {
+ readSemaphore.notify();
+ }
+ }
+ return value;
+ }
+
+ @Override
+ public void write(double value) {
+ if (writeWaitEnabled) {
+ try {
+ while (available() == buffer.length) {
+ synchronized (readSemaphore) {
+ readSemaphore.wait();
+ }
+ }
+ writeOneInternal(value);
+ } catch (InterruptedException e) {
+ }
+ } else {
+ if (available() != buffer.length) {
+ writeOneInternal(value);
+ }
+ }
+ }
+
+ private void writeOneInternal(double value) {
+ buffer[writeIndex & accessMask] = value;
+ writeIndex = (writeIndex + 1) & sizeMask;
+ if (readWaitEnabled) {
+ synchronized (writeSemaphore) {
+ writeSemaphore.notify();
+ }
+ }
+ }
+
+ @Override
+ public int read(double[] buffer) {
+ return read(buffer, 0, buffer.length);
+ }
+
+ @Override
+ public int read(double[] buffer, int start, int count) {
+ if (readWaitEnabled) {
+ for (int i = 0; i < count; i++) {
+ buffer[i + start] = read();
+ }
+ } else {
+ if (available() < count) {
+ count = available();
+ } else {
+ for (int i = 0; i < count; i++) {
+ buffer[i + start] = read();
+ }
+ }
+ }
+ return count;
+ }
+
+ @Override
+ public void write(double[] buffer) {
+ write(buffer, 0, buffer.length);
+ }
+
+ @Override
+ public void write(double[] buffer, int start, int count) {
+ for (int i = 0; i < count; i++) {
+ write(buffer[i + start]);
+ }
+ }
+
+ /** If true then a subsequent write call will wait if there is no room to write. */
+ public void setWriteWaitEnabled(boolean enabled) {
+ writeWaitEnabled = enabled;
+
+ }
+
+ /** If true then a subsequent read call will wait if there is no data to read. */
+ public void setReadWaitEnabled(boolean enabled) {
+ readWaitEnabled = enabled;
+
+ }
+
+ public boolean isWriteWaitEnabled() {
+ return writeWaitEnabled;
+ }
+
+ public boolean isReadWaitEnabled() {
+ return readWaitEnabled;
+ }
+
+}
diff --git a/src/com/jsyn/io/AudioInputStream.java b/src/com/jsyn/io/AudioInputStream.java
new file mode 100644
index 0000000..f233ff1
--- /dev/null
+++ b/src/com/jsyn/io/AudioInputStream.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.io;
+
+public interface AudioInputStream {
+ public double read();
+
+ /**
+ * Try to fill the entire buffer.
+ *
+ * @param buffer
+ * @return number of samples read
+ */
+ public int read(double[] buffer);
+
+ /**
+ * Read from the stream. Block until some data is available.
+ *
+ * @param buffer
+ * @param start index of first sample in buffer
+ * @param count number of samples to read, for example count=8 for 4 stereo frames
+ * @return number of samples read
+ */
+ public int read(double[] buffer, int start, int count);
+
+ public void close();
+
+ /**
+ * @return number of samples currently available to read without blocking
+ */
+ public int available();
+}
diff --git a/src/com/jsyn/io/AudioOutputStream.java b/src/com/jsyn/io/AudioOutputStream.java
new file mode 100644
index 0000000..dada577
--- /dev/null
+++ b/src/com/jsyn/io/AudioOutputStream.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.io;
+
+import java.io.IOException;
+
+public interface AudioOutputStream {
+ public void write(double value) throws IOException;
+
+ public void write(double[] buffer) throws IOException;
+
+ public void write(double[] buffer, int start, int count) throws IOException;
+
+ public void close() throws IOException;
+}
diff --git a/src/com/jsyn/midi/MessageParser.java b/src/com/jsyn/midi/MessageParser.java
new file mode 100644
index 0000000..43d10c8
--- /dev/null
+++ b/src/com/jsyn/midi/MessageParser.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.midi;
+
+/**
+ * Parse the message and call the appropriate method to handle it.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class MessageParser {
+ public void parse(byte[] message) {
+ int status = message[0];
+ int command = status & 0xF0;
+ int channel = status & 0x0F;
+
+ switch (command) {
+ case MidiConstants.NOTE_ON:
+ int velocity = message[2];
+ if (velocity == 0) {
+ noteOff(channel, message[1], velocity);
+ } else {
+ noteOn(channel, message[1], velocity);
+ }
+ break;
+
+ case MidiConstants.NOTE_OFF:
+ noteOff(channel, message[1], message[2]);
+ break;
+
+ case MidiConstants.CONTROL_CHANGE:
+ controlChange(channel, message[1], message[2]);
+ break;
+
+ case MidiConstants.PITCH_BEND:
+ int bend = (((message[2]) & 0x007F) << 7) + ((message[1]) & 0x007F);
+ pitchBend(channel, bend);
+ break;
+ }
+
+ }
+
+ public void pitchBend(int channel, int bend) {
+ }
+
+ public void controlChange(int channel, int index, int value) {
+ }
+
+ public void noteOn(int channel, int pitch, int velocity) {
+ }
+
+ public void noteOff(int channel, int pitch, int velocity) {
+ }
+}
diff --git a/src/com/jsyn/midi/MidiConstants.java b/src/com/jsyn/midi/MidiConstants.java
new file mode 100644
index 0000000..dae9390
--- /dev/null
+++ b/src/com/jsyn/midi/MidiConstants.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.midi;
+
+/**
+ * Constants that define the MIDI standard.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class MidiConstants {
+ // Basic commands.
+ public static final int NOTE_OFF = 0x80;
+ public static final int NOTE_ON = 0x90;
+ public static final int POLYPHONIC_AFTERTOUCH = 0xA0;
+ public static final int CONTROL_CHANGE = 0xB0;
+ public static final int PROGRAM_CHANGE = 0xC0;
+ public static final int CHANNEL_AFTERTOUCH = 0xD0;
+ public static final int PITCH_BEND = 0xE0;
+ public static final int SYSTEM_COMMON = 0xF0;
+
+ public static final int PITCH_BEND_CENTER = 8192;
+
+ public static final String PITCH_NAMES[] = {
+ "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"
+ };
+
+ /**
+ * Calculate frequency in Hertz based on MIDI pitch. Middle C is 60.0. You can use fractional
+ * pitches so 60.5 would give you a pitch half way between C and C#.
+ */
+ static final double CONCERT_A_FREQUENCY = 440.0;
+ static final double CONCERT_A_PITCH = 69.0;
+
+ public static double convertPitchToFrequency(double pitch) {
+ return CONCERT_A_FREQUENCY * Math.pow(2.0, ((pitch - CONCERT_A_PITCH) / 12.0));
+ }
+
+ /**
+ * Calculate MIDI pitch based on frequency in Hertz. Middle C is 60.0.
+ */
+ public static double convertFrequencyToPitch(double frequency) {
+ return CONCERT_A_PITCH + (12 * Math.log(frequency / CONCERT_A_FREQUENCY) / Math.log(2.0));
+ }
+
+}
diff --git a/src/com/jsyn/package.html b/src/com/jsyn/package.html
new file mode 100644
index 0000000..cd73832
--- /dev/null
+++ b/src/com/jsyn/package.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+<title>JSyn Package</title>
+</head>
+<body>
+JSyn is a music and audio synthesis API for Java. The basic sequence of operations is:
+<ul>
+<li>Use the JSyn class to create a synthesizer.</li>
+<li>Create unit generators and add them to the synthesizer.</li>
+<li>Connect unit generators so that audio signals can flow between them.</li>
+<li>Start an output generator. It will pull data from the connected units.</li>
+<li>Set port values and queue sample and envelope data to change the sound.</li>
+</ul>
+</body>
+</html> \ No newline at end of file
diff --git a/src/com/jsyn/ports/ConnectableInput.java b/src/com/jsyn/ports/ConnectableInput.java
new file mode 100644
index 0000000..3dae876
--- /dev/null
+++ b/src/com/jsyn/ports/ConnectableInput.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2011 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.ports;
+
+/**
+ * This interface lets you pass either an input port, or a single part of an input port.
+ *
+ * @author Phil Burk (C) 2011 Mobileer Inc
+ */
+public interface ConnectableInput {
+ public void connect(ConnectableOutput other);
+
+ public void disconnect(ConnectableOutput other);
+
+ /**
+ * This is used internally by PortBlockPart to make a connection between specific parts of a
+ * port.
+ *
+ * @return
+ */
+ public PortBlockPart getPortBlockPart();
+
+ public void pullData(long frameCount, int start, int limit);
+}
diff --git a/src/com/jsyn/ports/ConnectableOutput.java b/src/com/jsyn/ports/ConnectableOutput.java
new file mode 100644
index 0000000..f42a799
--- /dev/null
+++ b/src/com/jsyn/ports/ConnectableOutput.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.ports;
+
+public interface ConnectableOutput {
+ public void connect(ConnectableInput other);
+
+ public void disconnect(ConnectableInput other);
+}
diff --git a/src/com/jsyn/ports/GettablePort.java b/src/com/jsyn/ports/GettablePort.java
new file mode 100644
index 0000000..aabf5ca
--- /dev/null
+++ b/src/com/jsyn/ports/GettablePort.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.ports;
+
+public interface GettablePort {
+ String getName();
+
+ int getNumParts();
+
+ double getValue(int partNum);
+
+ Object getUnitGenerator();
+}
diff --git a/src/com/jsyn/ports/InputMixingBlockPart.java b/src/com/jsyn/ports/InputMixingBlockPart.java
new file mode 100644
index 0000000..3211342
--- /dev/null
+++ b/src/com/jsyn/ports/InputMixingBlockPart.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.ports;
+
+import java.io.PrintStream;
+
+import com.jsyn.Synthesizer;
+import com.jsyn.unitgen.UnitGenerator;
+
+/**
+ * A UnitInputPort has an array of these, one for each part.
+ *
+ * @author Phil Burk 2009 Mobileer Inc
+ */
+
+public class InputMixingBlockPart extends PortBlockPart {
+ private double[] mixer = new double[Synthesizer.FRAMES_PER_BLOCK];
+ private double current;
+
+ InputMixingBlockPart(UnitBlockPort unitBlockPort, double defaultValue) {
+ super(unitBlockPort, defaultValue);
+ }
+
+ @Override
+ public double getValue() {
+ return current;
+ }
+
+ @Override
+ protected void setValue(double value) {
+ current = value;
+ super.setValue(value);
+ }
+
+ @Override
+ public double[] getValues() {
+ double[] result;
+ int numConnections = getConnectionCount();
+ // System.out.println("numConnection = " + numConnections + " for " +
+ // this );
+ if (numConnections == 0)
+ // No connection so just use our own data.
+ {
+ result = super.getValues();
+ } else if (numConnections == 1)
+ // Grab values from one connected port.
+ {
+ PortBlockPart otherPart = getConnection(0);
+ result = otherPart.getValues();
+ } else
+ // Mix all of the inputs.
+ {
+ PortBlockPart otherPart = getConnection(0);
+ double[] inputs = otherPart.getValues();
+ for (int i = 0; i < mixer.length; i++) {
+ mixer[i] = inputs[i]; // set directly instead of zeroing first
+ }
+ // Now mix in the remaining inputs.
+ for (int jCon = 1; jCon < numConnections; jCon++) {
+ otherPart = getConnection(jCon);
+
+ inputs = otherPart.getValues();
+ for (int i = 0; i < mixer.length; i++) {
+ mixer[i] += inputs[i]; // mix with previous inputs
+ }
+ }
+ result = mixer;
+ }
+ current = result[0];
+ return result;
+ }
+
+ private void printIndentation(PrintStream out, int level) {
+ for (int i = 0; i < level; i++) {
+ out.print(" ");
+ }
+ }
+
+ private String portToString(UnitBlockPort port) {
+ UnitGenerator ugen = port.getUnitGenerator();
+ return ugen.getClass().getSimpleName() + "." + port.getName();
+ }
+
+ public void printConnections(PrintStream out, int level) {
+ for (int i = 0; i < getConnectionCount(); i++) {
+ PortBlockPart part = getConnection(i);
+
+ printIndentation(out, level);
+ out.println(portToString(getPort()) + " <--- " + portToString(part.getPort()));
+
+ part.getPort().getUnitGenerator().printConnections(out, level + 1);
+ }
+ }
+}
diff --git a/src/com/jsyn/ports/PortBlockPart.java b/src/com/jsyn/ports/PortBlockPart.java
new file mode 100644
index 0000000..26bbae0
--- /dev/null
+++ b/src/com/jsyn/ports/PortBlockPart.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.ports;
+
+import java.util.ArrayList;
+
+import com.jsyn.Synthesizer;
+import com.jsyn.engine.SynthesisEngine;
+import com.softsynth.shared.time.ScheduledCommand;
+import com.softsynth.shared.time.TimeStamp;
+
+/**
+ * Part of a multi-part port, for example, the left side of a stereo port.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ */
+public class PortBlockPart implements ConnectableOutput, ConnectableInput {
+ private double[] values = new double[Synthesizer.FRAMES_PER_BLOCK];
+ private ArrayList<PortBlockPart> connections = new ArrayList<PortBlockPart>();
+ private UnitBlockPort unitBlockPort;
+
+ protected PortBlockPart(UnitBlockPort unitBlockPort, double defaultValue) {
+ this.unitBlockPort = unitBlockPort;
+ setValue(defaultValue);
+ }
+
+ public double[] getValues() {
+ return values;
+ }
+
+ public double getValue() {
+ return values[0];
+ }
+
+ public double get() {
+ return values[0];
+ }
+
+ protected void setValue(double value) {
+ for (int i = 0; i < values.length; i++) {
+ values[i] = value;
+ }
+ }
+
+ protected boolean isConnected() {
+ return (connections.size() > 0);
+ }
+
+ private void addConnection(PortBlockPart otherPart) {
+ // System.out.println("addConnection from " + this + " to " + otherPart
+ // );
+ if (connections.contains(otherPart)) {
+ System.out.println("addConnection alreacy had connection from " + this + " to "
+ + otherPart);
+ } else {
+ connections.add(otherPart);
+ }
+ }
+
+ private void removeConnection(PortBlockPart otherPart) {
+ // System.out.println("removeConnection from " + this + " to " +
+ // otherPart );
+ connections.remove(otherPart);
+ }
+
+ private void connectNow(PortBlockPart otherPart) {
+ addConnection(otherPart);
+ otherPart.addConnection(this);
+ }
+
+ private void disconnectNow(PortBlockPart otherPart) {
+ removeConnection(otherPart);
+ otherPart.removeConnection(this);
+ }
+
+ private void disconnectAllNow() {
+ for (PortBlockPart part : connections) {
+ part.removeConnection(this);
+ }
+ connections.clear();
+ }
+
+ public PortBlockPart getConnection(int i) {
+ return connections.get(i);
+ }
+
+ public int getConnectionCount() {
+ return connections.size();
+ }
+
+ /** Set all values to the last value. */
+ protected void flatten() {
+ double lastValue = values[values.length - 1];
+ for (int i = 0; i < values.length - 1; i++) {
+ values[i] = lastValue;
+ }
+ }
+
+ protected UnitBlockPort getPort() {
+ return unitBlockPort;
+ }
+
+ private void checkConnection(PortBlockPart destination) {
+ SynthesisEngine sourceSynth = unitBlockPort.getSynthesisEngine();
+ SynthesisEngine destSynth = destination.unitBlockPort.getSynthesisEngine();
+ if ((sourceSynth != destSynth) && (sourceSynth != null) && (destSynth != null)) {
+ throw new RuntimeException("Connection between units on different synths.");
+ }
+ }
+
+ protected void connect(final PortBlockPart destination) {
+ checkConnection(destination);
+ unitBlockPort.queueCommand(new ScheduledCommand() {
+ @Override
+ public void run() {
+ connectNow(destination);
+ }
+ });
+ }
+
+ protected void connect(final PortBlockPart destination, TimeStamp timeStamp) {
+ unitBlockPort.scheduleCommand(timeStamp, new ScheduledCommand() {
+ @Override
+ public void run() {
+ connectNow(destination);
+ }
+ });
+ }
+
+ protected void disconnect(final PortBlockPart destination) {
+ unitBlockPort.queueCommand(new ScheduledCommand() {
+ @Override
+ public void run() {
+ disconnectNow(destination);
+ }
+ });
+ }
+
+ protected void disconnect(final PortBlockPart destination, TimeStamp timeStamp) {
+ unitBlockPort.scheduleCommand(timeStamp, new ScheduledCommand() {
+ @Override
+ public void run() {
+ disconnectNow(destination);
+ }
+ });
+ }
+
+ protected void disconnectAll() {
+ unitBlockPort.queueCommand(new ScheduledCommand() {
+ @Override
+ public void run() {
+ disconnectAllNow();
+ }
+ });
+ }
+
+ @Override
+ public void connect(ConnectableInput other) {
+ connect(other.getPortBlockPart());
+ }
+
+ @Override
+ public void connect(ConnectableOutput other) {
+ other.connect(this);
+ }
+
+ @Override
+ public void disconnect(ConnectableOutput other) {
+ other.disconnect(this);
+ }
+
+ @Override
+ public void disconnect(ConnectableInput other) {
+ disconnect(other.getPortBlockPart());
+ }
+
+ /** To implement ConnectableInput */
+ @Override
+ public PortBlockPart getPortBlockPart() {
+ return this;
+ }
+
+ @Override
+ public void pullData(long frameCount, int start, int limit) {
+ for (int i = 0; i < getConnectionCount(); i++) {
+ PortBlockPart part = getConnection(i);
+ part.getPort().getUnitGenerator().pullData(frameCount, start, limit);
+ }
+ }
+
+}
diff --git a/src/com/jsyn/ports/QueueDataCommand.java b/src/com/jsyn/ports/QueueDataCommand.java
new file mode 100644
index 0000000..52025ae
--- /dev/null
+++ b/src/com/jsyn/ports/QueueDataCommand.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.ports;
+
+import com.jsyn.data.SequentialData;
+import com.softsynth.shared.time.ScheduledCommand;
+
+/**
+ * A command that can be used to queue SequentialData to a UnitDataQueuePort. Here is an example of
+ * queuing data with a callback using this command.
+ *
+ * <pre>
+ * <code>
+ * // Queue an envelope with a completion callback.
+ * QueueDataCommand command = envelopePlayer.dataQueue.createQueueDataCommand( envelope, 0,
+ * envelope.getNumFrames() );
+ * // Create an object to be called when the queued data is done.
+ * TestQueueCallback callback = new TestQueueCallback();
+ * command.setCallback( callback );
+ * command.setNumLoops( 2 );
+ * envelopePlayer.rate.set( 0.2 );
+ * synth.queueCommand( command );
+ * </code>
+ * </pre>
+ *
+ * The callback will be passed QueueDataEvents.
+ *
+ * <pre>
+ * <code>
+ * class TestQueueCallback implements UnitDataQueueCallback
+ * {
+ * public void started( QueueDataEvent event )
+ * {
+ * System.out.println("CALLBACK: Envelope started.");
+ * }
+ *
+ * public void looped( QueueDataEvent event )
+ * {
+ * System.out.println("CALLBACK: Envelope looped.");
+ * }
+ *
+ * public void finished( QueueDataEvent event )
+ * {
+ * System.out.println("CALLBACK: Envelope finished.");
+ * }
+ * }
+ * </code>
+ * </pre>
+ *
+ * @author Phil Burk 2009 Mobileer Inc
+ */
+public abstract class QueueDataCommand extends QueueDataEvent implements ScheduledCommand {
+
+ protected SequentialDataCrossfade crossfadeData;
+ protected SequentialData currentData;
+
+ private static final long serialVersionUID = -1185274459972359536L;
+ private UnitDataQueueCallback callback;
+
+ public QueueDataCommand(UnitDataQueuePort port, SequentialData sequentialData, int startFrame,
+ int numFrames) {
+ super(port);
+
+ assert ((startFrame + numFrames) <= sequentialData.getNumFrames());
+ this.sequentialData = sequentialData;
+ this.currentData = sequentialData;
+ crossfadeData = new SequentialDataCrossfade();
+ this.startFrame = startFrame;
+ this.numFrames = numFrames;
+ }
+
+ @Override
+ public abstract void run();
+
+ /**
+ * If true then this item will be skipped if other items are queued after it. This flag allows
+ * you to queue lots of small pieces of sound without making the queue very long.
+ *
+ * @param skipIfOthers
+ */
+ public void setSkipIfOthers(boolean skipIfOthers) {
+ this.skipIfOthers = skipIfOthers;
+ }
+
+ /**
+ * If true then the queue will be cleared and this item will be started immediately. It is
+ * better to use this flag than to clear the queue from the application because there could be a
+ * gap before the next item is available. This is most useful when combined with
+ * setCrossFadeIn().
+ *
+ * @param immediate
+ */
+ public void setImmediate(boolean immediate) {
+ this.immediate = immediate;
+ }
+
+ public UnitDataQueueCallback getCallback() {
+ return callback;
+ }
+
+ public void setCallback(UnitDataQueueCallback callback) {
+ this.callback = callback;
+ }
+
+ public SequentialDataCrossfade getCrossfadeData() {
+ return crossfadeData;
+ }
+
+ public void setCrossfadeData(SequentialDataCrossfade crossfadeData) {
+ this.crossfadeData = crossfadeData;
+ }
+
+ public SequentialData getCurrentData() {
+ return currentData;
+ }
+
+ public void setCurrentData(SequentialData currentData) {
+ this.currentData = currentData;
+ }
+
+ /**
+ * Stop the unit that contains this port after this command has finished.
+ *
+ * @param autoStop
+ */
+ public void setAutoStop(boolean autoStop) {
+ this.autoStop = autoStop;
+ }
+
+ /**
+ * Set how many time the block should be repeated after the first time. For example, if you set
+ * numLoops to zero the block will only be played once. If you set numLoops to one the block
+ * will be played twice.
+ *
+ * @param numLoops number of times to loop back
+ */
+ public void setNumLoops(int numLoops) {
+ this.numLoops = numLoops;
+ }
+
+ /**
+ * Number of frames to cross fade from the previous block to this block. This can be used to
+ * avoid pops when making abrupt transitions. There must be frames available after the end of
+ * the previous block to use for crossfading. The crossfade is linear.
+ *
+ * @param size
+ */
+ public void setCrossFadeIn(int size) {
+ this.crossFadeIn = size;
+ }
+
+}
diff --git a/src/com/jsyn/ports/QueueDataEvent.java b/src/com/jsyn/ports/QueueDataEvent.java
new file mode 100644
index 0000000..2b93fab
--- /dev/null
+++ b/src/com/jsyn/ports/QueueDataEvent.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.ports;
+
+import java.util.EventObject;
+
+import com.jsyn.data.SequentialData;
+
+/**
+ * An event that is passed to a UnitDataQueueCallback when the element in the queue is played..
+ *
+ * @author Phil Burk 2009 Mobileer Inc
+ */
+public class QueueDataEvent extends EventObject {
+ private static final long serialVersionUID = 176846633064538053L;
+ protected SequentialData sequentialData;
+ protected int startFrame;
+ protected int numFrames;
+ protected int numLoops;
+ protected int loopsLeft;
+ protected int crossFadeIn;
+ protected boolean skipIfOthers;
+ protected boolean autoStop;
+ protected boolean immediate;
+
+ public QueueDataEvent(Object arg0) {
+ super(arg0);
+ }
+
+ public boolean isSkipIfOthers() {
+ return skipIfOthers;
+ }
+
+ public boolean isImmediate() {
+ return immediate;
+ }
+
+ public SequentialData getSequentialData() {
+ return sequentialData;
+ }
+
+ public int getCrossFadeIn() {
+ return crossFadeIn;
+ }
+
+ public int getStartFrame() {
+ return startFrame;
+ }
+
+ public int getNumFrames() {
+ return numFrames;
+ }
+
+ public int getNumLoops() {
+ return numLoops;
+ }
+
+ public int getLoopsLeft() {
+ return loopsLeft;
+ }
+
+ public boolean isAutoStop() {
+ return autoStop;
+ }
+
+}
diff --git a/src/com/jsyn/ports/SequentialDataCrossfade.java b/src/com/jsyn/ports/SequentialDataCrossfade.java
new file mode 100644
index 0000000..25e1fd9
--- /dev/null
+++ b/src/com/jsyn/ports/SequentialDataCrossfade.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.ports;
+
+import com.jsyn.data.SequentialData;
+import com.jsyn.data.SequentialDataCommon;
+
+/**
+ * A SequentialData object that will crossfade between two other SequentialData objects. The
+ * crossfade is linear. This could, for example, be used to create a smooth transition between two
+ * samples, or between two arbitrary regions in one sample. As an example, consider a sample that
+ * has a length of 200000 frames. You could specify a sample loop that started arbitrarily at frame
+ * 50000 and with a size of 30000 frames. Unless you got lucky with the zero crossings, it is likely
+ * that you will hear a pop when this sample loops. To prevent the pop you could crossfade the
+ * beginning of the loop with the region immediately after the end of the loop. To crossfade with
+ * 5000 samples after the loop:
+ *
+ * <pre>
+ * SequentialDataCrossfade xfade = new SequentialDataCrossfade(sample, (50000 + 30000), 5000, sample,
+ * 50000, 30000);
+ * </pre>
+ *
+ * After the crossfade you will hear the rest of the target at full volume. There are two regions
+ * that determine what is returned from readDouble()
+ * <ol>
+ * <li>Crossfade region with size crossFadeFrames. It fades smoothly from source to target.</li>
+ * <li>Steady region that is simply the target values with size (numFrames-crossFadeFrames).</li>
+ * </ol>
+ *
+ * <pre>
+ * "Crossfade Region" "Steady Region"
+ * |-- source fading out --|
+ * |-- target fading in --|-- remainder of target at original volume --|
+ * </pre>
+ *
+ * @author Phil Burk
+ */
+class SequentialDataCrossfade extends SequentialDataCommon {
+ private SequentialData source;
+ private int sourceStartIndex;
+
+ private SequentialData target;
+ private int targetStartIndex;
+
+ private int crossFadeFrames;
+ private double frameScaler;
+
+ /**
+ * @param source SequentialData that will be at full volume at the beginning of the crossfade
+ * region.
+ * @param sourceStartFrame Frame in source to begin the crossfade.
+ * @param crossFadeFrames Number of frames in the crossfaded region.
+ * @param target SequentialData that will be at full volume at the end of the crossfade region.
+ * @param targetStartFrame Frame in target to begin the crossfade.
+ * @param numFrames total number of frames in this data object.
+ */
+ public void setup(SequentialData source, int sourceStartFrame, int crossFadeFrames,
+ SequentialData target, int targetStartFrame, int numFrames) {
+
+ assert ((sourceStartFrame + crossFadeFrames) <= source.getNumFrames());
+ assert ((targetStartFrame + numFrames) <= target.getNumFrames());
+
+ // System.out.println( "WARNING! sourceStartFrame = " + sourceStartFrame
+ // + ", crossFadeFrames = " + crossFadeFrames + ", maxFrame = "
+ // + source.getNumFrames() + ", source = " + source );
+ // System.out.println( " targetStartFrame = " + targetStartFrame
+ // + ", numFrames = " + numFrames + ", maxFrame = "
+ // + target.getNumFrames() + ", target = " + target );
+
+ // There is a danger that we might nest SequentialDataCrossfades deeply
+ // as source. If past crossfade region then pull out the target.
+ if (source instanceof SequentialDataCrossfade) {
+ SequentialDataCrossfade crossfade = (SequentialDataCrossfade) source;
+ // If we are starting past the crossfade region then just use the
+ // target.
+ if (sourceStartFrame >= crossfade.crossFadeFrames) {
+ source = crossfade.target;
+ sourceStartFrame += crossfade.targetStartIndex / source.getChannelsPerFrame();
+ }
+ }
+
+ if (target instanceof SequentialDataCrossfade) {
+ SequentialDataCrossfade crossfade = (SequentialDataCrossfade) target;
+ target = crossfade.target;
+ targetStartFrame += crossfade.targetStartIndex / target.getChannelsPerFrame();
+ }
+
+ this.source = source;
+ this.target = target;
+ this.sourceStartIndex = sourceStartFrame * source.getChannelsPerFrame();
+ this.crossFadeFrames = crossFadeFrames;
+ this.targetStartIndex = targetStartFrame * target.getChannelsPerFrame();
+
+ frameScaler = (crossFadeFrames == 0) ? 1.0 : (1.0 / crossFadeFrames);
+ this.numFrames = numFrames;
+ }
+
+ @Override
+ public void writeDouble(int index, double value) {
+ }
+
+ @Override
+ public double readDouble(int index) {
+ int frame = index / source.getChannelsPerFrame();
+ if (frame < crossFadeFrames) {
+ double factor = frame * frameScaler;
+ double value = (1.0 - factor) * source.readDouble(index + sourceStartIndex);
+ value += (factor * target.readDouble(index + targetStartIndex));
+ return value;
+ } else {
+ return target.readDouble(index + targetStartIndex);
+ }
+ }
+
+ @Override
+ public double getRateScaler(int index, double synthesisRate) {
+ return target.getRateScaler(index, synthesisRate);
+ }
+
+ @Override
+ public int getChannelsPerFrame() {
+ return target.getChannelsPerFrame();
+ }
+
+}
diff --git a/src/com/jsyn/ports/SettablePort.java b/src/com/jsyn/ports/SettablePort.java
new file mode 100644
index 0000000..e0db05c
--- /dev/null
+++ b/src/com/jsyn/ports/SettablePort.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.ports;
+
+import com.softsynth.shared.time.TimeStamp;
+
+/**
+ * Port whose parts can be set.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ */
+public interface SettablePort extends GettablePort {
+ void set(int partNum, double value, TimeStamp timeStamp);
+}
diff --git a/src/com/jsyn/ports/UnitBlockPort.java b/src/com/jsyn/ports/UnitBlockPort.java
new file mode 100644
index 0000000..d7fc82f
--- /dev/null
+++ b/src/com/jsyn/ports/UnitBlockPort.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.ports;
+
+/**
+ * A port that contains multiple parts with blocks of data.
+ *
+ * @author Phil Burk 2009 Mobileer Inc
+ */
+public class UnitBlockPort extends UnitPort {
+ PortBlockPart[] parts;
+
+ public UnitBlockPort(int numParts, String name, double defaultValue) {
+ super(name);
+ makeParts(numParts, defaultValue);
+ }
+
+ public UnitBlockPort(String name) {
+ this(1, name, 0.0);
+ }
+
+ protected void makeParts(int numParts, double defaultValue) {
+ parts = new PortBlockPart[numParts];
+ for (int i = 0; i < numParts; i++) {
+ parts[i] = new PortBlockPart(this, defaultValue);
+ }
+ }
+
+ @Override
+ public int getNumParts() {
+ return parts.length;
+ }
+
+ /**
+ * Convenience call to get(0).
+ *
+ * @return value of 0th part as set
+ */
+ public double get() {
+ return get(0);
+ }
+
+ public double getValue() {
+ return getValue(0);
+ }
+
+ /**
+ * This is used inside UnitGenerators to get the current values for a port. It works regardless
+ * of whether the port is connected or not.
+ *
+ * @return
+ */
+ public double[] getValues() {
+ return parts[0].getValues();
+ }
+
+ /** Only for use in the audio thread when implementing UnitGenerators. */
+ public double[] getValues(int partNum) {
+ return parts[partNum].getValues();
+ }
+
+ /** Get the immediate current value of the port. */
+ public double getValue(int partNum) {
+ return parts[partNum].getValue();
+ }
+
+ public double get(int partNum) {
+ return parts[partNum].get();
+ }
+
+ /** Only for use in the audio thread when implementing UnitGenerators. */
+ protected void setValueInternal(int partNum, double value) {
+ parts[partNum].setValue(value);
+ }
+
+ /** Only for use in the audio thread when implementing UnitGenerators. */
+ public void setValueInternal(double value) {
+ setValueInternal(0, value);
+ }
+
+ public boolean isConnected() {
+ return isConnected(0);
+ }
+
+ public boolean isConnected(int partNum) {
+ return parts[partNum].isConnected();
+ }
+
+ public void disconnectAll(int partNum) {
+ parts[partNum].disconnectAll();
+ }
+
+ public void disconnectAll() {
+ disconnectAll(0);
+ }
+}
diff --git a/src/com/jsyn/ports/UnitDataQueueCallback.java b/src/com/jsyn/ports/UnitDataQueueCallback.java
new file mode 100644
index 0000000..dca4adc
--- /dev/null
+++ b/src/com/jsyn/ports/UnitDataQueueCallback.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2011 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.ports;
+
+/**
+ * This is called when a block of data that is queued to a UnitDataQueuePort starts, loops, or
+ * finishes.
+ *
+ * @author Phil Burk (C) 2011 Mobileer Inc
+ */
+public interface UnitDataQueueCallback {
+ public void started(QueueDataEvent event);
+
+ public void looped(QueueDataEvent event);
+
+ public void finished(QueueDataEvent event);
+}
diff --git a/src/com/jsyn/ports/UnitDataQueuePort.java b/src/com/jsyn/ports/UnitDataQueuePort.java
new file mode 100644
index 0000000..0c88711
--- /dev/null
+++ b/src/com/jsyn/ports/UnitDataQueuePort.java
@@ -0,0 +1,468 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.ports;
+
+import java.util.LinkedList;
+
+import com.jsyn.data.SequentialData;
+import com.jsyn.exceptions.ChannelMismatchException;
+import com.softsynth.shared.time.ScheduledCommand;
+import com.softsynth.shared.time.TimeStamp;
+
+/**
+ * Queue for SequentialData, samples or envelopes
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ */
+public class UnitDataQueuePort extends UnitPort {
+ private LinkedList<QueuedBlock> blocks = new LinkedList<QueuedBlock>();
+ private QueueDataCommand currentBlock;
+ private int frameIndex;
+ private int numChannels = 1;
+ private double normalizedRate;
+ private long framesMoved;
+ private boolean autoStopPending;
+ private boolean targetValid;
+ private QueueDataCommand finishingBlock;
+ private QueueDataCommand loopingBlock;
+ public static final int LOOP_IF_LAST = -1;
+
+ public UnitDataQueuePort(String name) {
+ super(name);
+ }
+
+ /** Hold a reference to part of a sample. */
+ @SuppressWarnings("serial")
+ private class QueuedBlock extends QueueDataCommand {
+
+ public QueuedBlock(SequentialData queueableData, int startFrame, int numFrames) {
+ super(UnitDataQueuePort.this, queueableData, startFrame, numFrames);
+ }
+
+ @Override
+ public void run() {
+ synchronized (blocks) {
+ // Remove last block if it can be skipped.
+ if (blocks.size() > 0) {
+ QueueDataEvent lastBlock = blocks.getLast();
+ if (lastBlock.isSkipIfOthers()) {
+ blocks.removeLast();
+ }
+ }
+
+ // If we are crossfading then figure out where to crossfade
+ // from.
+ if (getCrossFadeIn() > 0) {
+ if (isImmediate()) {
+ // Queue will be cleared so fade in from current.
+ if (currentBlock != null) {
+ setupCrossFade(currentBlock, frameIndex, this);
+ }
+ // else nothing is playing so don't crossfade.
+ } else {
+ QueueDataCommand endBlock = getEndBlock();
+ if (endBlock != null) {
+ setupCrossFade(endBlock,
+ endBlock.getStartFrame() + endBlock.getNumFrames(), this);
+ }
+ }
+ }
+
+ if (isImmediate()) {
+ clearQueue();
+ }
+
+ blocks.add(this);
+ }
+ }
+ }
+
+ // FIXME - determine crossfade on any transition between blocks or when looping back.
+
+ protected void setupCrossFade(QueueDataCommand sourceCommand, int sourceStartIndex,
+ QueueDataCommand targetCommand) {
+ int crossFrames = targetCommand.getCrossFadeIn();
+ SequentialData sourceData = sourceCommand.getCurrentData();
+ SequentialData targetData = targetCommand.getCurrentData();
+ int remainingSource = sourceData.getNumFrames() - sourceStartIndex;
+ // clip to end of source
+ if (crossFrames > remainingSource)
+ crossFrames = remainingSource;
+ if (crossFrames > 0) {
+ // The SequentialDataCrossfade should continue to the end of the target
+ // so that we can crossfade from it to the target.
+ int remainingTarget = targetData.getNumFrames() - targetCommand.getStartFrame();
+ targetCommand.crossfadeData.setup(sourceData, sourceStartIndex, crossFrames,
+ targetData, targetCommand.getStartFrame(), remainingTarget);
+ targetCommand.currentData = targetCommand.crossfadeData;
+ targetCommand.startFrame = 0;
+ }
+ }
+
+ public QueueDataCommand createQueueDataCommand(SequentialData queueableData) {
+ return createQueueDataCommand(queueableData, 0, queueableData.getNumFrames());
+ }
+
+ public QueueDataCommand createQueueDataCommand(SequentialData queueableData, int startFrame,
+ int numFrames) {
+ if (queueableData.getChannelsPerFrame() != UnitDataQueuePort.this.numChannels) {
+ throw new ChannelMismatchException("Tried to queue "
+ + queueableData.getChannelsPerFrame() + " channel data to a " + numChannels
+ + " channel port.");
+ }
+ return new QueuedBlock(queueableData, startFrame, numFrames);
+ }
+
+ public QueueDataCommand getEndBlock() {
+ if (blocks.size() > 0) {
+ return blocks.getLast();
+ } else if (currentBlock != null) {
+ return currentBlock;
+ } else {
+ return null;
+ }
+ }
+
+ public void setCurrentBlock(QueueDataCommand currentBlock) {
+ this.currentBlock = currentBlock;
+ }
+
+ public void firePendingCallbacks() {
+ if (loopingBlock != null) {
+ if (loopingBlock.getCallback() != null) {
+ loopingBlock.getCallback().looped(currentBlock);
+ }
+ loopingBlock = null;
+ }
+ if (finishingBlock != null) {
+ if (finishingBlock.getCallback() != null) {
+ finishingBlock.getCallback().finished(currentBlock); // FIXME - Should this pass
+ // finishingBlock?!
+ }
+ finishingBlock = null;
+ }
+ }
+
+ public boolean hasMore() {
+ return (currentBlock != null) || (blocks.size() > 0);
+ }
+
+ private void checkBlock() {
+ if (currentBlock == null) {
+ synchronized (blocks) {
+ setCurrentBlock(blocks.remove());
+ frameIndex = currentBlock.getStartFrame();
+ currentBlock.loopsLeft = currentBlock.getNumLoops();
+ if (currentBlock.getCallback() != null) {
+ currentBlock.getCallback().started(currentBlock);
+ }
+ }
+ }
+ }
+
+ private void advanceFrameIndex() {
+ frameIndex += 1;
+ framesMoved += 1;
+ // Are we done with this block?
+ if (frameIndex >= (currentBlock.getStartFrame() + currentBlock.getNumFrames())) {
+ // Should we loop on this block based on a counter?
+ if (currentBlock.loopsLeft > 0) {
+ currentBlock.loopsLeft -= 1;
+ loopToStart();
+ }
+ // Should we loop forever on this block?
+ else if ((blocks.size() == 0) && (currentBlock.loopsLeft < 0)) {
+ loopToStart();
+ }
+ // We are done.
+ else {
+ if (currentBlock.isAutoStop()) {
+ autoStopPending = true;
+ }
+ finishingBlock = currentBlock;
+ setCurrentBlock(null);
+ // System.out.println("advanceFrameIndex: currentBlock set null");
+ }
+ }
+ }
+
+ private void loopToStart() {
+ if (currentBlock.getCrossFadeIn() > 0) {
+ setupCrossFade(currentBlock, frameIndex, currentBlock);
+ }
+ frameIndex = currentBlock.getStartFrame();
+ loopingBlock = currentBlock;
+ }
+
+ public double getNormalizedRate() {
+ return normalizedRate;
+ }
+
+ public double readCurrentChannelDouble(int channelIndex) {
+ return currentBlock.currentData.readDouble((frameIndex * numChannels) + channelIndex);
+ }
+
+ public void writeCurrentChannelDouble(int channelIndex, double value) {
+ currentBlock.currentData.writeDouble((frameIndex * numChannels) + channelIndex, value);
+ }
+
+ public void beginFrame(double synthesisPeriod) {
+ checkBlock();
+ normalizedRate = currentBlock.currentData.getRateScaler(frameIndex, synthesisPeriod);
+ }
+
+ public void endFrame() {
+ advanceFrameIndex();
+ targetValid = true;
+ }
+
+ public double readNextMonoDouble(double synthesisPeriod) {
+ beginFrame(synthesisPeriod);
+ double value = currentBlock.currentData.readDouble(frameIndex);
+ endFrame();
+ return value;
+ }
+
+ /** Write directly to the port queue. This is only called by unit tests! */
+ protected void addQueuedBlock(QueueDataEvent block) {
+ blocks.add((QueuedBlock) block);
+ }
+
+ /** Clear the queue. Internal use only. */
+ protected void clearQueue() {
+ synchronized (blocks) {
+ blocks.clear();
+ setCurrentBlock(null);
+ targetValid = false;
+ autoStopPending = false;
+ }
+ }
+
+ class ClearQueueCommand implements ScheduledCommand {
+ @Override
+ public void run() {
+ clearQueue();
+ }
+ }
+
+ /** Queue the data to the port at a future time. */
+ public void queue(SequentialData queueableData, int startFrame, int numFrames,
+ TimeStamp timeStamp) {
+ QueueDataCommand command = createQueueDataCommand(queueableData, startFrame, numFrames);
+ scheduleCommand(timeStamp, command);
+ }
+
+ /**
+ * Queue the data to the port at a future time. Command will clear the queue before executing.
+ */
+ public void queueImmediate(SequentialData queueableData, int startFrame, int numFrames,
+ TimeStamp timeStamp) {
+ QueueDataCommand command = createQueueDataCommand(queueableData, startFrame, numFrames);
+ command.setImmediate(true);
+ scheduleCommand(timeStamp, command);
+ }
+
+ /** Queue the data to the port at a future time. */
+ public void queueLoop(SequentialData queueableData, int startFrame, int numFrames,
+ TimeStamp timeStamp) {
+ queueLoop(queueableData, startFrame, numFrames, LOOP_IF_LAST, timeStamp);
+ }
+
+ /**
+ * Queue the data to the port at a future time with a specified number of loops.
+ */
+ public void queueLoop(SequentialData queueableData, int startFrame, int numFrames,
+ int numLoops, TimeStamp timeStamp) {
+ QueueDataCommand command = createQueueDataCommand(queueableData, startFrame, numFrames);
+ command.setNumLoops(numLoops);
+ scheduleCommand(timeStamp, command);
+ }
+
+ /** Queue the entire data object for looping. */
+ public void queueLoop(SequentialData queueableData) {
+ queueLoop(queueableData, 0, queueableData.getNumFrames());
+ }
+
+ /** Queue the data to the port for immediate use. */
+ public void queueLoop(SequentialData queueableData, int startFrame, int numFrames) {
+ queueLoop(queueableData, startFrame, numFrames, LOOP_IF_LAST);
+ }
+
+ /**
+ * Queue the data to the port for immediate use with a specified number of loops.
+ */
+ public void queueLoop(SequentialData queueableData, int startFrame, int numFrames, int numLoops) {
+ QueueDataCommand command = createQueueDataCommand(queueableData, startFrame, numFrames);
+ command.setNumLoops(numLoops);
+ queueCommand(command);
+ }
+
+ /**
+ * Queue the data to the port at a future time. Request that the unit stop when this block is
+ * finished.
+ */
+ public void queueStop(SequentialData queueableData, int startFrame, int numFrames,
+ TimeStamp timeStamp) {
+ QueueDataCommand command = createQueueDataCommand(queueableData, startFrame, numFrames);
+ command.setAutoStop(true);
+ scheduleCommand(timeStamp, command);
+ }
+
+ /** Queue the data to the port through the command queue ASAP. */
+ public void queue(SequentialData queueableData, int startFrame, int numFrames) {
+ QueueDataCommand command = createQueueDataCommand(queueableData, startFrame, numFrames);
+ queueCommand(command);
+ }
+
+ /**
+ * Queue entire amount of data with no options.
+ *
+ * @param queueableData
+ */
+ public void queue(SequentialData queueableData) {
+ queue(queueableData, 0, queueableData.getNumFrames());
+ }
+
+ /** Schedule queueOn now! */
+ public void queueOn(SequentialData queueableData) {
+ queueOn(queueableData, getSynthesisEngine().createTimeStamp());
+ }
+
+ /** Schedule queueOff now! */
+ public void queueOff(SequentialData queueableData) {
+ queueOff(queueableData, false);
+ }
+
+ /** Schedule queueOff now! */
+ public void queueOff(SequentialData queueableData, boolean ifStop) {
+ queueOff(queueableData, ifStop, getSynthesisEngine().createTimeStamp());
+ }
+
+ /**
+ * Convenience method that will queue the attack portion of a channelData and the sustain loop
+ * if it exists. This could be used to implement a NoteOn method.
+ */
+ public void queueOn(SequentialData queueableData, TimeStamp timeStamp) {
+
+ if (queueableData.getSustainBegin() < 0) {
+ // no sustain loop, handle release
+ if (queueableData.getReleaseBegin() < 0) {
+ queueImmediate(queueableData, 0, queueableData.getNumFrames(), timeStamp); /*
+ * No
+ * loops.
+ */
+ } else {
+ queueImmediate(queueableData, 0, queueableData.getReleaseEnd(), timeStamp);
+ int size = queueableData.getReleaseEnd() - queueableData.getReleaseBegin();
+ queueLoop(queueableData, queueableData.getReleaseBegin(), size, timeStamp);
+ }
+ } else {
+ // yes sustain loop
+ if (queueableData.getSustainEnd() > 0) {
+ int frontSize = queueableData.getSustainBegin();
+ int loopSize = queueableData.getSustainEnd() - queueableData.getSustainBegin();
+ // Is there an initial portion before the sustain loop?
+ if (frontSize > 0) {
+ queueImmediate(queueableData, 0, frontSize, timeStamp);
+ }
+ loopSize = queueableData.getSustainEnd() - queueableData.getSustainBegin();
+ if (loopSize > 0) {
+ queueLoop(queueableData, queueableData.getSustainBegin(), loopSize, timeStamp);
+ }
+ }
+
+ }
+ }
+
+ /**
+ * Convenience method that will queue the decay portion of a SequentialData object, or the gap
+ * and release loop portions if they exist. This could be used to implement a NoteOff method.
+ *
+ * @ifStop Will setAutostop(true) if release portion queued without a release loop. This will
+ * stop execution of the unit.
+ */
+ public void queueOff(SequentialData queueableData, boolean ifStop, TimeStamp timeStamp) {
+ if (queueableData.getSustainBegin() >= 0) /* Sustain loop? */
+ {
+ int relSize = queueableData.getReleaseEnd() - queueableData.getReleaseBegin();
+ if (queueableData.getReleaseBegin() < 0) { /* Sustain loop, no release loop. */
+ int susEnd = queueableData.getSustainEnd();
+ int size = queueableData.getNumFrames() - susEnd;
+ // System.out.println("queueOff: size = " + size );
+ if (size <= 0) {
+ // always queue something so that we can stop the loop
+ // 20001117
+ size = 1;
+ susEnd = queueableData.getNumFrames() - 1;
+ }
+ if (ifStop) {
+ queueStop(queueableData, susEnd, size, timeStamp);
+ } else {
+ queue(queueableData, susEnd, size, timeStamp);
+ }
+ } else if (queueableData.getReleaseBegin() > queueableData.getSustainEnd()) {
+ // Queue gap between sustain and release loop.
+ queue(queueableData, queueableData.getSustainEnd(), queueableData.getReleaseEnd()
+ - queueableData.getSustainEnd(), timeStamp);
+ if (relSize > 0)
+ queueLoop(queueableData, queueableData.getReleaseBegin(), relSize, timeStamp);
+ } else if (relSize > 0) {
+ // No gap between sustain and release.
+ queueLoop(queueableData, queueableData.getReleaseBegin(), relSize, timeStamp);
+ }
+ }
+ /* If no sustain loop, then nothing to do. */
+ }
+
+ public void clear(TimeStamp timeStamp) {
+ ScheduledCommand command = new ClearQueueCommand();
+ scheduleCommand(timeStamp, command);
+ }
+
+ public void clear() {
+ ScheduledCommand command = new ClearQueueCommand();
+ queueCommand(command);
+ }
+
+ public void writeNextDouble(double value) {
+ checkBlock();
+ currentBlock.currentData.writeDouble(frameIndex, value);
+ advanceFrameIndex();
+ }
+
+ public long getFrameCount() {
+ return framesMoved;
+ }
+
+ public boolean testAndClearAutoStop() {
+ boolean temp = autoStopPending;
+ autoStopPending = false;
+ return temp;
+ }
+
+ public boolean isTargetValid() {
+ return targetValid;
+ }
+
+ public void setNumChannels(int numChannels) {
+ this.numChannels = numChannels;
+ }
+
+ public int getNumChannels() {
+ return numChannels;
+ }
+}
diff --git a/src/com/jsyn/ports/UnitFunctionPort.java b/src/com/jsyn/ports/UnitFunctionPort.java
new file mode 100644
index 0000000..e45241a
--- /dev/null
+++ b/src/com/jsyn/ports/UnitFunctionPort.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.ports;
+
+import com.jsyn.data.Function;
+
+/**
+ * Port for holding a Function object.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ */
+public class UnitFunctionPort extends UnitPort {
+ private static NullFunction nullFunction = new NullFunction();
+ private Function function = nullFunction;
+
+ private static class NullFunction implements Function {
+ @Override
+ public double evaluate(double input) {
+ return 0.0;
+ }
+ }
+
+ public UnitFunctionPort(String name) {
+ super(name);
+ }
+
+ public void set(Function function) {
+ this.function = function;
+ }
+
+ public Function get() {
+ return function;
+ }
+}
diff --git a/src/com/jsyn/ports/UnitGatePort.java b/src/com/jsyn/ports/UnitGatePort.java
new file mode 100644
index 0000000..43d5e7f
--- /dev/null
+++ b/src/com/jsyn/ports/UnitGatePort.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.ports;
+
+import com.jsyn.unitgen.UnitGenerator;
+import com.softsynth.shared.time.ScheduledCommand;
+import com.softsynth.shared.time.TimeStamp;
+
+public class UnitGatePort extends UnitInputPort {
+ private boolean autoDisableEnabled = false;
+ private boolean triggered = false;
+ private boolean off = true;
+ private UnitGenerator gatedUnit;
+ public static final double THRESHOLD = 0.01;
+
+ public UnitGatePort(String name) {
+ super(name);
+ }
+
+ public void on() {
+ setOn(true);
+ }
+
+ public void off() {
+ setOn(false);
+ }
+
+ public void off(TimeStamp timeStamp) {
+ setOn(false, timeStamp);
+ }
+
+ public void on(TimeStamp timeStamp) {
+ setOn(true, timeStamp);
+ }
+
+ private void setOn(final boolean on) {
+ queueCommand(new ScheduledCommand() {
+ @Override
+ public void run() {
+ setOnInternal(on);
+ }
+ });
+ }
+
+ private void setOn(final boolean on, TimeStamp timeStamp) {
+ scheduleCommand(timeStamp, new ScheduledCommand() {
+ @Override
+ public void run() {
+ setOnInternal(on);
+ }
+ });
+ }
+
+ private void setOnInternal(boolean on) {
+ if (on) {
+ triggerInternal();
+ }
+ setValueInternal(on ? 1.0 : 0.0);
+ }
+
+ private void triggerInternal() {
+ getGatedUnit().setEnabled(true);
+ triggered = true;
+ }
+
+ public void trigger() {
+ queueCommand(new ScheduledCommand() {
+ @Override
+ public void run() {
+ triggerInternal();
+ }
+ });
+ }
+
+ public void trigger(TimeStamp timeStamp) {
+ scheduleCommand(timeStamp, new ScheduledCommand() {
+ @Override
+ public void run() {
+ triggerInternal();
+ }
+ });
+ }
+
+ /**
+ * This is called by UnitGenerators. It sets the off value that can be tested using isOff().
+ *
+ * @param i
+ * @return true if triggered by a positive edge.
+ */
+ public boolean checkGate(int i) {
+ double[] inputs = getValues();
+ boolean result = triggered;
+ triggered = false;
+ if (off) {
+ if (inputs[i] >= THRESHOLD) {
+ result = true;
+ off = false;
+ }
+ } else {
+ if (inputs[i] < THRESHOLD) {
+ off = true;
+ }
+ }
+ return result;
+ }
+
+ public boolean isOff() {
+ return off;
+ }
+
+ public boolean isAutoDisableEnabled() {
+ return autoDisableEnabled;
+ }
+
+ /**
+ * Request the containing UnitGenerator be disabled when checkAutoDisabled() is called. This can
+ * be used to reduce CPU load.
+ *
+ * @param autoDisableEnabled
+ */
+ public void setAutoDisableEnabled(boolean autoDisableEnabled) {
+ this.autoDisableEnabled = autoDisableEnabled;
+ }
+
+ /**
+ * Called by UnitGenerator when an envelope reaches the end of its contour.
+ */
+ public void checkAutoDisable() {
+ if (autoDisableEnabled) {
+ getGatedUnit().setEnabled(false);
+ }
+ }
+
+ private UnitGenerator getGatedUnit() {
+ return (gatedUnit == null) ? getUnitGenerator() : gatedUnit;
+ }
+
+ public void setupAutoDisable(UnitGenerator unit) {
+ gatedUnit = unit;
+ setAutoDisableEnabled(true);
+ // Start off disabled so we don't immediately swamp the CPU.
+ gatedUnit.setEnabled(false);
+ }
+}
diff --git a/src/com/jsyn/ports/UnitInputPort.java b/src/com/jsyn/ports/UnitInputPort.java
new file mode 100644
index 0000000..93a7f7a
--- /dev/null
+++ b/src/com/jsyn/ports/UnitInputPort.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.ports;
+
+import java.io.PrintStream;
+
+import com.softsynth.shared.time.ScheduledCommand;
+import com.softsynth.shared.time.TimeStamp;
+
+/**
+ * A port that is used to pass values into a UnitGenerator.
+ *
+ * @author Phil Burk 2009 Mobileer Inc
+ */
+public class UnitInputPort extends UnitBlockPort implements ConnectableInput, SettablePort {
+ private double minimum = 0.0;
+ private double maximum = 1.0;
+ private double defaultValue = 0.0;
+
+ private double[] setValues;
+
+ /**
+ * @param numParts typically 1, use 2 for stereo ports
+ * @param name name that may be used in GUIs
+ * @param defaultValue
+ */
+ public UnitInputPort(int numParts, String name, double defaultValue) {
+ super(numParts, name, defaultValue);
+ setDefault(defaultValue);
+ setValues = new double[numParts];
+ for (int i = 0; i < numParts; i++) {
+ setValues[i] = defaultValue;
+ }
+ }
+
+ public UnitInputPort(String name, double defaultValue) {
+ this(1, name, defaultValue);
+ }
+
+ public UnitInputPort(String name) {
+ this(1, name, 0.0);
+ }
+
+ public UnitInputPort(int numParts, String name) {
+ this(numParts, name, 0.0);
+ }
+
+ @Override
+ protected void makeParts(int numParts, double defaultValue) {
+ parts = new InputMixingBlockPart[numParts];
+ for (int i = 0; i < numParts; i++) {
+ parts[i] = new InputMixingBlockPart(this, defaultValue);
+ }
+ }
+
+ /**
+ * This is used internally by the SynthesisEngine to execute units based on their connections.
+ *
+ * @param frameCount
+ * @param start
+ * @param limit
+ */
+ @Override
+ public void pullData(long frameCount, int start, int limit) {
+ for (PortBlockPart part : parts) {
+ ((InputMixingBlockPart) part).pullData(frameCount, start, limit);
+ }
+ }
+
+ @Override
+ protected void setValueInternal(int partNum, double value) {
+ super.setValueInternal(partNum, value);
+ setValues[partNum] = value;
+ }
+
+ public void set(double value) {
+ set(0, value);
+ }
+
+ public void set(final int partNum, final double value) {
+ // Trigger exception now if out of range.
+ setValues[partNum] = value;
+ queueCommand(new ScheduledCommand() {
+ @Override
+ public void run() {
+ setValueInternal(partNum, value);
+ }
+ });
+ }
+
+ public void set(double value, TimeStamp time) {
+ set(0, value, time);
+ }
+
+ public void set(double value, double time) {
+ set(0, value, time);
+ }
+
+ public void set(int partNum, double value, double time) {
+ set(partNum, value, new TimeStamp(time));
+ }
+
+ @Override
+ public void set(final int partNum, final double value, TimeStamp timeStamp) {
+ // Trigger exception now if out of range.
+ getValue(partNum);
+ scheduleCommand(timeStamp, new ScheduledCommand() {
+ @Override
+ public void run() {
+ setValueInternal(partNum, value);
+ }
+ });
+ }
+
+ /**
+ * Value of a port based on the set() calls. Not affected by connected ports.
+ *
+ * @param partNum
+ * @return value as set
+ */
+ @Override
+ public double get(int partNum) {
+ return setValues[partNum];
+ }
+
+ public double getMaximum() {
+ return maximum;
+ }
+
+ /**
+ * The minimum and maximum are only used when setting up knobs or other control systems. The
+ * internal values are not clipped to this range.
+ *
+ * @param maximum
+ */
+ public void setMaximum(double maximum) {
+ this.maximum = maximum;
+ }
+
+ public double getMinimum() {
+ return minimum;
+ }
+
+ public void setMinimum(double minimum) {
+ this.minimum = minimum;
+ }
+
+ public double getDefault() {
+ return defaultValue;
+ }
+
+ public void setDefault(double defaultValue) {
+ this.defaultValue = defaultValue;
+ }
+
+ /**
+ * Convenience function for setting limits on a port. These limits are recommended values when
+ * setting up a GUI. It is possible to set a port to a value outside these limits.
+ *
+ * @param minimum
+ * @param value default value, will be clipped to min/max
+ * @param maximum
+ */
+ public void setup(double minimum, double value, double maximum) {
+ setMinimum(minimum);
+ setMaximum(maximum);
+ setDefault(value);
+ set(value);
+ }
+
+ // Grab min, max, default from another port.
+ public void setup(UnitInputPort other) {
+ setup(other.getMinimum(), other.getDefault(), other.getMaximum());
+ }
+
+ public void connect(int thisPartNum, UnitOutputPort otherPort, int otherPartNum,
+ TimeStamp timeStamp) {
+ otherPort.connect(otherPartNum, this, thisPartNum, timeStamp);
+ }
+
+ /** Connect an input to an output port. */
+ public void connect(int thisPartNum, UnitOutputPort otherPort, int otherPartNum) {
+ // Typically connections are made from output to input because it is
+ // more intuitive.
+ otherPort.connect(otherPartNum, this, thisPartNum);
+ }
+
+ public void connect(UnitOutputPort otherPort) {
+ connect(0, otherPort, 0);
+ }
+
+ @Override
+ public void connect(ConnectableOutput other) {
+ other.connect(this);
+ }
+
+ public void disconnect(int thisPartNum, UnitOutputPort otherPort, int otherPartNum) {
+ otherPort.disconnect(otherPartNum, this, thisPartNum);
+ }
+
+ @Override
+ public PortBlockPart getPortBlockPart() {
+ return parts[0];
+ }
+
+ public ConnectableInput getConnectablePart(int i) {
+ return parts[i];
+ }
+
+ @Override
+ public void disconnect(ConnectableOutput other) {
+ other.disconnect(this);
+ }
+
+ public void printConnections(PrintStream out, int level) {
+ for (PortBlockPart part : parts) {
+ ((InputMixingBlockPart) part).printConnections(out, level);
+ }
+ }
+
+}
diff --git a/src/com/jsyn/ports/UnitOutputPort.java b/src/com/jsyn/ports/UnitOutputPort.java
new file mode 100644
index 0000000..6fcd758
--- /dev/null
+++ b/src/com/jsyn/ports/UnitOutputPort.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.ports;
+
+import com.jsyn.unitgen.UnitSink;
+import com.softsynth.shared.time.TimeStamp;
+
+/**
+ * Units write to their output port blocks. Other multiple connected input ports read from them.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ */
+
+public class UnitOutputPort extends UnitBlockPort implements ConnectableOutput, GettablePort {
+ public UnitOutputPort() {
+ this("Output");
+ }
+
+ public UnitOutputPort(String name) {
+ this(1, name, 0.0);
+ }
+
+ public UnitOutputPort(int numParts, String name) {
+ this(numParts, name, 0.0);
+ }
+
+ public UnitOutputPort(int numParts, String name, double defaultValue) {
+ super(numParts, name, defaultValue);
+ }
+
+ public void flatten() {
+ for (PortBlockPart part : parts) {
+ part.flatten();
+ }
+ }
+
+ public void connect(int thisPartNum, UnitInputPort otherPort, int otherPartNum) {
+ PortBlockPart source = parts[thisPartNum];
+ PortBlockPart destination = otherPort.parts[otherPartNum];
+ source.connect(destination);
+ }
+
+ public void connect(int thisPartNum, UnitInputPort otherPort, int otherPartNum,
+ TimeStamp timeStamp) {
+ PortBlockPart source = parts[thisPartNum];
+ PortBlockPart destination = otherPort.parts[otherPartNum];
+ source.connect(destination, timeStamp);
+ }
+
+ public void connect(UnitInputPort input) {
+ connect(0, input, 0);
+ }
+
+ @Override
+ public void connect(ConnectableInput input) {
+ parts[0].connect(input);
+ }
+
+ public void connect(UnitSink sink) {
+ connect(0, sink.getInput(), 0);
+ }
+
+ public void disconnect(int thisPartNum, UnitInputPort otherPort, int otherPartNum) {
+ PortBlockPart source = parts[thisPartNum];
+ PortBlockPart destination = otherPort.parts[otherPartNum];
+ source.disconnect(destination);
+ }
+
+ public void disconnect(int thisPartNum, UnitInputPort otherPort, int otherPartNum,
+ TimeStamp timeStamp) {
+ PortBlockPart source = parts[thisPartNum];
+ PortBlockPart destination = otherPort.parts[otherPartNum];
+ source.disconnect(destination, timeStamp);
+ }
+
+ public void disconnect(UnitInputPort otherPort) {
+ disconnect(0, otherPort, 0);
+ }
+
+ @Override
+ public void disconnect(ConnectableInput input) {
+ parts[0].disconnect(input);
+ }
+
+ public ConnectableOutput getConnectablePart(int i) {
+ return parts[i];
+ }
+
+}
diff --git a/src/com/jsyn/ports/UnitPort.java b/src/com/jsyn/ports/UnitPort.java
new file mode 100644
index 0000000..a652e68
--- /dev/null
+++ b/src/com/jsyn/ports/UnitPort.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.ports;
+
+import com.jsyn.engine.SynthesisEngine;
+import com.jsyn.unitgen.UnitGenerator;
+import com.softsynth.shared.time.ScheduledCommand;
+import com.softsynth.shared.time.TimeStamp;
+
+/**
+ * Basic audio port for JSyn unit generators.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ */
+public class UnitPort {
+ private String name;
+ private UnitGenerator unit;
+
+ public UnitPort(String name) {
+ this.name = name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setUnitGenerator(UnitGenerator unit) {
+ // If a port is in a circuit then we want to just use the lower level
+ // unit that instantiated the circuit.
+ if (this.unit == null) {
+ this.unit = unit;
+ }
+ }
+
+ public UnitGenerator getUnitGenerator() {
+ return unit;
+ }
+
+ SynthesisEngine getSynthesisEngine() {
+ if (unit == null) {
+ return null;
+ }
+ return unit.getSynthesisEngine();
+ }
+
+ public int getNumParts() {
+ return 1;
+ }
+
+ public void scheduleCommand(TimeStamp timeStamp, ScheduledCommand scheduledCommand) {
+ if (getSynthesisEngine() == null) {
+ scheduledCommand.run();
+ } else {
+ getSynthesisEngine().scheduleCommand(timeStamp, scheduledCommand);
+ }
+ }
+
+ public void queueCommand(ScheduledCommand scheduledCommand) {
+ if (getSynthesisEngine() == null) {
+ scheduledCommand.run();
+ } else {
+ getSynthesisEngine().scheduleCommand(getSynthesisEngine().createTimeStamp(),
+ scheduledCommand);
+ }
+ }
+
+}
diff --git a/src/com/jsyn/ports/UnitSpectralInputPort.java b/src/com/jsyn/ports/UnitSpectralInputPort.java
new file mode 100644
index 0000000..bdf0ff5
--- /dev/null
+++ b/src/com/jsyn/ports/UnitSpectralInputPort.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.ports;
+
+import com.jsyn.data.Spectrum;
+
+public class UnitSpectralInputPort extends UnitPort implements ConnectableInput {
+ private UnitSpectralOutputPort other;
+
+ private Spectrum spectrum;
+
+ public UnitSpectralInputPort() {
+ this("Output");
+ }
+
+ public UnitSpectralInputPort(String name) {
+ super(name);
+ }
+
+ public void setSpectrum(Spectrum spectrum) {
+ this.spectrum = spectrum;
+ }
+
+ public Spectrum getSpectrum() {
+ if (other == null) {
+ return spectrum;
+ } else {
+ return other.getSpectrum();
+ }
+ }
+
+ @Override
+ public void connect(ConnectableOutput other) {
+ if (other instanceof UnitSpectralOutputPort) {
+ this.other = (UnitSpectralOutputPort) other;
+ } else {
+ throw new RuntimeException(
+ "Can only connect UnitSpectralOutputPort to UnitSpectralInputPort!");
+ }
+ }
+
+ @Override
+ public void disconnect(ConnectableOutput other) {
+ if (this.other == other) {
+ this.other = null;
+ }
+ }
+
+ @Override
+ public PortBlockPart getPortBlockPart() {
+ return null;
+ }
+
+ @Override
+ public void pullData(long frameCount, int start, int limit) {
+ if (other != null) {
+ other.getUnitGenerator().pullData(frameCount, start, limit);
+ }
+ }
+
+ public boolean isAvailable() {
+ if (other != null) {
+ return other.isAvailable();
+ } else {
+ return (spectrum != null);
+ }
+ }
+
+}
diff --git a/src/com/jsyn/ports/UnitSpectralOutputPort.java b/src/com/jsyn/ports/UnitSpectralOutputPort.java
new file mode 100644
index 0000000..51633ce
--- /dev/null
+++ b/src/com/jsyn/ports/UnitSpectralOutputPort.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.ports;
+
+import com.jsyn.data.Spectrum;
+
+public class UnitSpectralOutputPort extends UnitPort implements ConnectableOutput {
+ private Spectrum spectrum;
+ private boolean available;
+
+ public UnitSpectralOutputPort() {
+ this("Output");
+ }
+
+ public UnitSpectralOutputPort(int size) {
+ this("Output", size);
+ }
+
+ public UnitSpectralOutputPort(String name) {
+ super(name);
+ spectrum = new Spectrum();
+ }
+
+ public UnitSpectralOutputPort(String name, int size) {
+ super(name);
+ spectrum = new Spectrum(size);
+ }
+
+ public void setSize(int size) {
+ spectrum.setSize(size);
+ }
+
+ public Spectrum getSpectrum() {
+ return spectrum;
+ }
+
+ public void advance() {
+ available = true;
+ }
+
+ @Override
+ public void connect(ConnectableInput other) {
+ other.connect(this);
+ }
+
+ @Override
+ public void disconnect(ConnectableInput other) {
+ other.disconnect(this);
+ }
+
+ public boolean isAvailable() {
+ return available;
+ }
+
+}
diff --git a/src/com/jsyn/ports/UnitVariablePort.java b/src/com/jsyn/ports/UnitVariablePort.java
new file mode 100644
index 0000000..60b64fd
--- /dev/null
+++ b/src/com/jsyn/ports/UnitVariablePort.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.ports;
+
+import com.softsynth.shared.time.ScheduledCommand;
+import com.softsynth.shared.time.TimeStamp;
+
+public class UnitVariablePort extends UnitPort implements SettablePort {
+ private double value;
+
+ public UnitVariablePort(String name, double defaultValue) {
+ super(name);
+ value = defaultValue;
+ }
+
+ public UnitVariablePort(String name) {
+ super(name);
+ }
+
+ public void setValue(double value) {
+ this.value = value;
+ }
+
+ public void set(double value) {
+ this.value = value;
+ }
+
+ public double get() {
+ return value;
+ }
+
+ public double getValue() {
+ return value;
+ }
+
+ @Override
+ public double getValue(int partNum) {
+ return value;
+ }
+
+ @Override
+ public void set(int partNum, final double value, TimeStamp timeStamp) {
+ scheduleCommand(timeStamp, new ScheduledCommand() {
+ @Override
+ public void run() {
+ set(value);
+ }
+ });
+ }
+}
diff --git a/src/com/jsyn/ports/package.html b/src/com/jsyn/ports/package.html
new file mode 100644
index 0000000..3547618
--- /dev/null
+++ b/src/com/jsyn/ports/package.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+<title>JSyn Ports</title>
+</head>
+<body>
+<p>Ports are used to pass audio data in and out of UnitGenerators.
+They can also be used to connect UnitGenerators together so that signals can flow between them.
+The UnitDataQueuePort contains a FIFO that will accept envelope and sample data.
+</p>
+</body>
+</html> \ No newline at end of file
diff --git a/src/com/jsyn/scope/AudioScope.java b/src/com/jsyn/scope/AudioScope.java
new file mode 100644
index 0000000..32268cd
--- /dev/null
+++ b/src/com/jsyn/scope/AudioScope.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.scope;
+
+import com.jsyn.Synthesizer;
+import com.jsyn.ports.UnitOutputPort;
+import com.jsyn.scope.swing.AudioScopeView;
+
+// TODO Auto and Manual triggers.
+// TODO Auto scaling of vertical.
+// TODO Fixed size Y scale knobs.
+// TODO Pan back and forth around trigger.
+// TODO Continuous capture
+/**
+ * Digital oscilloscope for JSyn.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class AudioScope {
+ public enum TriggerMode {
+ AUTO, NORMAL // , MANUAL
+ }
+
+ public enum ViewMode {
+ WAVEFORM, SPECTRUM
+ }
+
+ private AudioScopeView audioScopeView = null;
+ private AudioScopeModel audioScopeModel;
+
+ public AudioScope(Synthesizer synth) {
+ audioScopeModel = new AudioScopeModel(synth);
+ }
+
+ public AudioScopeProbe addProbe(UnitOutputPort output) {
+ return addProbe(output, 0);
+ }
+
+ public AudioScopeProbe addProbe(UnitOutputPort output, int partIndex) {
+ return audioScopeModel.addProbe(output, partIndex);
+ }
+
+ public void start() {
+ audioScopeModel.start();
+ }
+
+ public void stop() {
+ audioScopeModel.stop();
+ }
+
+ public AudioScopeModel getModel() {
+ return audioScopeModel;
+ }
+
+ public AudioScopeView getView() {
+ if (audioScopeView == null) {
+ audioScopeView = new AudioScopeView();
+ audioScopeView.setModel(audioScopeModel);
+ }
+ return audioScopeView;
+ }
+
+ public void setTriggerMode(TriggerMode triggerMode) {
+ audioScopeModel.setTriggerMode(triggerMode);
+ }
+
+ public void setTriggerSource(AudioScopeProbe probe) {
+ audioScopeModel.setTriggerSource(probe);
+ }
+
+ public void setTriggerLevel(double level) {
+ getModel().getTriggerModel().getLevelModel().setDoubleValue(level);
+ }
+
+ public double getTriggerLevel() {
+ return getModel().getTriggerModel().getLevelModel().getDoubleValue();
+ }
+
+ public void setViewMode(ViewMode waveform) {
+ // TODO Auto-generated method stub
+
+ }
+
+}
diff --git a/src/com/jsyn/scope/AudioScopeModel.java b/src/com/jsyn/scope/AudioScopeModel.java
new file mode 100644
index 0000000..85c4413
--- /dev/null
+++ b/src/com/jsyn/scope/AudioScopeModel.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.scope;
+
+import java.util.ArrayList;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import com.jsyn.Synthesizer;
+import com.jsyn.ports.UnitOutputPort;
+import com.jsyn.scope.AudioScope.TriggerMode;
+
+public class AudioScopeModel implements Runnable {
+ private static final int PRE_TRIGGER_SIZE = 32;
+ private Synthesizer synthesisEngine;
+ private ArrayList<AudioScopeProbe> probes = new ArrayList<AudioScopeProbe>();
+ private CopyOnWriteArrayList<ChangeListener> changeListeners = new CopyOnWriteArrayList<ChangeListener>();
+ private MultiChannelScopeProbeUnit probeUnit;
+ private double timeToArm;
+ private double period = 0.2;
+ private TriggerModel triggerModel;
+
+ public AudioScopeModel(Synthesizer synth) {
+ this.synthesisEngine = synth;
+ triggerModel = new TriggerModel();
+ }
+
+ public AudioScopeProbe addProbe(UnitOutputPort output, int partIndex) {
+ AudioScopeProbe probe = new AudioScopeProbe(this, output, partIndex);
+ DefaultWaveTraceModel waveTraceModel = new DefaultWaveTraceModel(this, probes.size());
+ probe.setWaveTraceModel(waveTraceModel);
+ probes.add(probe);
+ if (triggerModel.getSource() == null) {
+ triggerModel.setSource(probe);
+ }
+ return probe;
+ }
+
+ public void start() {
+ stop();
+ probeUnit = new MultiChannelScopeProbeUnit(probes.size(), triggerModel);
+ synthesisEngine.add(probeUnit);
+ for (int i = 0; i < probes.size(); i++) {
+ AudioScopeProbe probe = probes.get(i);
+ probe.getSource().connect(probe.getPartIndex(), probeUnit.input, i);
+ }
+ // Connect trigger signal to input of probe.
+ triggerModel.getSource().getSource()
+ .connect(triggerModel.getSource().getPartIndex(), probeUnit.trigger, 0);
+ probeUnit.start();
+
+ // Get synthesizer time in seconds.
+ timeToArm = synthesisEngine.getCurrentTime();
+ probeUnit.arm(timeToArm, this);
+ }
+
+ public void stop() {
+ if (probeUnit != null) {
+ for (int i = 0; i < probes.size(); i++) {
+ probeUnit.input.disconnectAll(i);
+ }
+ probeUnit.trigger.disconnectAll();
+ probeUnit.stop();
+ synthesisEngine.remove(probeUnit);
+ probeUnit = null;
+ }
+ }
+
+ public AudioScopeProbe[] getProbes() {
+ return probes.toArray(new AudioScopeProbe[0]);
+ }
+
+ public Synthesizer getSynthesizer() {
+ return synthesisEngine;
+ }
+
+ @Override
+ public void run() {
+ fireChangeListeners();
+ timeToArm = synthesisEngine.getCurrentTime();
+ timeToArm += period;
+ probeUnit.arm(timeToArm, this);
+ }
+
+ private void fireChangeListeners() {
+ ChangeEvent changeEvent = new ChangeEvent(this);
+ for (ChangeListener listener : changeListeners) {
+ listener.stateChanged(changeEvent);
+ }
+ // debug();
+ }
+
+ public void addChangeListener(ChangeListener changeListener) {
+ changeListeners.add(changeListener);
+ }
+
+ public void removeChangeListener(ChangeListener changeListener) {
+ changeListeners.remove(changeListener);
+ }
+
+ public void setTriggerMode(TriggerMode triggerMode) {
+ triggerModel.getModeModel().setSelectedItem(triggerMode);
+ }
+
+ public void setTriggerSource(AudioScopeProbe probe) {
+ triggerModel.setSource(probe);
+ }
+
+ public double getSample(int bufferIndex, int i) {
+ return probeUnit.getSample(bufferIndex, i);
+ }
+
+ public int getFramesPerBuffer() {
+ return probeUnit.getFramesPerBuffer();
+ }
+
+ public int getFramesCaptured() {
+ return probeUnit.getFramesCaptured();
+ }
+
+ public int getVisibleSize() {
+ int size = 0;
+ if (probeUnit != null) {
+ size = probeUnit.getPostTriggerSize() + PRE_TRIGGER_SIZE;
+ if (size > getFramesCaptured()) {
+ size = getFramesCaptured();
+ }
+ }
+ return size;
+ }
+
+ public int getStartIndex() {
+ // TODO Add pan support here.
+ return getFramesCaptured() - getVisibleSize();
+ }
+
+ public TriggerModel getTriggerModel() {
+ return triggerModel;
+ }
+
+}
diff --git a/src/com/jsyn/scope/AudioScopeProbe.java b/src/com/jsyn/scope/AudioScopeProbe.java
new file mode 100644
index 0000000..f1aad65
--- /dev/null
+++ b/src/com/jsyn/scope/AudioScopeProbe.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.scope;
+
+import java.awt.Color;
+
+import javax.swing.JToggleButton.ToggleButtonModel;
+
+import com.jsyn.ports.UnitOutputPort;
+import com.jsyn.swing.ExponentialRangeModel;
+
+/**
+ * Collect data from the source and make it available to the scope.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class AudioScopeProbe {
+ // private UnitOutputPort output;
+ private WaveTraceModel waveTraceModel;
+ private AudioScopeModel audioScopeModel;
+ private UnitOutputPort source;
+ private int partIndex;
+ private Color color;
+ private ExponentialRangeModel verticalScaleModel;
+ private ToggleButtonModel autoScaleButtonModel;
+ private double MIN_RANGE = 0.01;
+ private double MAX_RANGE = 100.0;
+
+ public AudioScopeProbe(AudioScopeModel audioScopeModel, UnitOutputPort source, int partIndex) {
+ this.audioScopeModel = audioScopeModel;
+ this.source = source;
+ this.partIndex = partIndex;
+
+ verticalScaleModel = new ExponentialRangeModel("VScale", 1000, MIN_RANGE, MAX_RANGE,
+ MIN_RANGE);
+ autoScaleButtonModel = new ToggleButtonModel();
+ autoScaleButtonModel.setSelected(true);
+ }
+
+ public WaveTraceModel getWaveTraceModel() {
+ return waveTraceModel;
+ }
+
+ public void setWaveTraceModel(WaveTraceModel waveTraceModel) {
+ this.waveTraceModel = waveTraceModel;
+ }
+
+ public UnitOutputPort getSource() {
+ return source;
+ }
+
+ public int getPartIndex() {
+ return partIndex;
+ }
+
+ public Color getColor() {
+ return color;
+ }
+
+ public void setColor(Color color) {
+ this.color = color;
+ }
+
+ public void setAutoScaleEnabled(boolean enabled) {
+ autoScaleButtonModel.setSelected(enabled);
+ }
+
+ public void setVerticalScale(double max) {
+ verticalScaleModel.setDoubleValue(max);
+ }
+
+ public ExponentialRangeModel getVerticalScaleModel() {
+ return verticalScaleModel;
+ }
+
+ public ToggleButtonModel getAutoScaleButtonModel() {
+ return autoScaleButtonModel;
+ }
+
+}
diff --git a/src/com/jsyn/scope/DefaultWaveTraceModel.java b/src/com/jsyn/scope/DefaultWaveTraceModel.java
new file mode 100644
index 0000000..a123c0b
--- /dev/null
+++ b/src/com/jsyn/scope/DefaultWaveTraceModel.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.scope;
+
+public class DefaultWaveTraceModel implements WaveTraceModel {
+ private AudioScopeModel audioScopeModel;
+ private int bufferIndex;
+
+ public DefaultWaveTraceModel(AudioScopeModel audioScopeModel, int bufferIndex) {
+ this.audioScopeModel = audioScopeModel;
+ this.bufferIndex = bufferIndex;
+ }
+
+ @Override
+ public double getSample(int i) {
+ return audioScopeModel.getSample(bufferIndex, i);
+ }
+
+ @Override
+ public int getSize() {
+ return audioScopeModel.getFramesCaptured();
+ }
+
+ @Override
+ public int getStartIndex() {
+ return audioScopeModel.getStartIndex();
+ }
+
+ @Override
+ public int getVisibleSize() {
+ return audioScopeModel.getVisibleSize();
+ }
+
+}
diff --git a/src/com/jsyn/scope/MultiChannelScopeProbeUnit.java b/src/com/jsyn/scope/MultiChannelScopeProbeUnit.java
new file mode 100644
index 0000000..5be19f0
--- /dev/null
+++ b/src/com/jsyn/scope/MultiChannelScopeProbeUnit.java
@@ -0,0 +1,246 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.scope;
+
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.scope.AudioScope.TriggerMode;
+import com.jsyn.unitgen.UnitGenerator;
+import com.softsynth.shared.time.ScheduledCommand;
+
+/**
+ * Multi-channel scope probe with an independent trigger input.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ */
+public class MultiChannelScopeProbeUnit extends UnitGenerator {
+ // Signal that is captured.
+ public UnitInputPort input;
+ // Signal that triggers the probe.
+ public UnitInputPort trigger;
+
+ // I am using ints instead of an enum for performance reasons.
+ private static final int STATE_IDLE = 0;
+ private static final int STATE_ARMED = 1;
+ private static final int STATE_READY = 2;
+ private static final int STATE_TRIGGERED = 3;
+ private int state = STATE_IDLE;
+
+ private int numChannels;
+ private double[][] inputValues;
+ private static final int FRAMES_PER_BUFFER = 4096; // must be power of two
+ private static final int FRAMES_PER_BUFFER_MASK = FRAMES_PER_BUFFER - 1;
+ private Runnable callback;
+
+ private TriggerModel triggerModel;
+ private int autoCountdown;
+ private int countdown;
+ private int postTriggerSize = 512;
+ SignalBuffer captureBuffer;
+ SignalBuffer displayBuffer;
+
+ // Use double buffers. One for capture, one for display.
+ class SignalBuffer {
+ float[][] buffers;
+ private int writeCursor;
+ private int triggerIndex;
+ private int framesCaptured;
+
+ SignalBuffer(int numChannels) {
+ buffers = new float[numChannels][];
+ for (int j = 0; j < numChannels; j++) {
+ buffers[j] = new float[FRAMES_PER_BUFFER];
+ }
+ }
+
+ void reset() {
+ writeCursor = 0;
+ triggerIndex = 0;
+ framesCaptured = 0;
+ }
+
+ public void saveChannelValue(int j, float value) {
+ buffers[j][writeCursor] = value;
+ }
+
+ public void markTrigger() {
+ triggerIndex = writeCursor;
+ }
+
+ public void bumpCursor() {
+ writeCursor = (writeCursor + 1) & FRAMES_PER_BUFFER_MASK;
+ if (writeCursor >= FRAMES_PER_BUFFER) {
+ writeCursor = 0;
+ }
+ if (framesCaptured < FRAMES_PER_BUFFER) {
+ framesCaptured += 1;
+ }
+ }
+
+ private int convertInternalToExternalIndex(int internalIndex) {
+ if (framesCaptured < FRAMES_PER_BUFFER) {
+ return internalIndex;
+ } else {
+ return (internalIndex - writeCursor) & (FRAMES_PER_BUFFER_MASK);
+ }
+ }
+
+ private int convertExternalToInternalIndex(int externalIndex) {
+ if (framesCaptured < FRAMES_PER_BUFFER) {
+ return externalIndex;
+ } else {
+ return (externalIndex + writeCursor) & (FRAMES_PER_BUFFER_MASK);
+ }
+ }
+
+ public int getTriggerIndex() {
+ return convertInternalToExternalIndex(triggerIndex);
+ }
+
+ public int getFramesCaptured() {
+ return framesCaptured;
+ }
+
+ public float getSample(int bufferIndex, int sampleIndex) {
+ int index = convertExternalToInternalIndex(sampleIndex);
+ return buffers[bufferIndex][index];
+ }
+ }
+
+ public MultiChannelScopeProbeUnit(int numChannels, TriggerModel triggerModel) {
+ this.numChannels = numChannels;
+ captureBuffer = new SignalBuffer(numChannels);
+ displayBuffer = new SignalBuffer(numChannels);
+ this.triggerModel = triggerModel;
+ addPort(trigger = new UnitInputPort(numChannels, "Trigger"));
+ addPort(input = new UnitInputPort(numChannels, "Input"));
+ inputValues = new double[numChannels][];
+ }
+
+ private synchronized void switchBuffers() {
+ SignalBuffer temp = captureBuffer;
+ captureBuffer = displayBuffer;
+ displayBuffer = temp;
+ }
+
+ private void internalArm(Runnable callback) {
+ this.callback = callback;
+ state = STATE_ARMED;
+ captureBuffer.reset();
+ }
+
+ class ScheduledArm implements ScheduledCommand {
+ private Runnable callback;
+
+ ScheduledArm(Runnable callback) {
+ this.callback = callback;
+ }
+
+ @Override
+ public void run() {
+ internalArm(this.callback);
+ }
+ }
+
+ /** Arm the probe at a future time. */
+ public void arm(double time, Runnable callback) {
+ ScheduledArm command = new ScheduledArm(callback);
+ getSynthesisEngine().scheduleCommand(time, command);
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ if (state != STATE_IDLE) {
+ TriggerMode triggerMode = triggerModel.getMode();
+ double triggerLevel = triggerModel.getTriggerLevel();
+ double[] triggerValues = trigger.getValues();
+
+ for (int j = 0; j < numChannels; j++) {
+ inputValues[j] = input.getValues(j);
+ }
+
+ for (int i = start; i < limit; i++) {
+ // Capture one sample from each channel.
+ for (int j = 0; j < numChannels; j++) {
+ captureBuffer.saveChannelValue(j, (float) inputValues[j][i]);
+ }
+ captureBuffer.bumpCursor();
+
+ switch (state) {
+ case STATE_ARMED:
+ if (triggerValues[i] <= triggerLevel) {
+ state = STATE_READY;
+ autoCountdown = 44100;
+ }
+ break;
+
+ case STATE_READY: {
+ boolean triggered = false;
+ if (triggerValues[i] > triggerLevel) {
+ triggered = true;
+ } else if (triggerMode.equals(TriggerMode.AUTO)) {
+ if (--autoCountdown == 0) {
+ triggered = true;
+ }
+ }
+ if (triggered) {
+ captureBuffer.markTrigger();
+ state = STATE_TRIGGERED;
+ countdown = postTriggerSize;
+ }
+ }
+ break;
+
+ case STATE_TRIGGERED:
+ countdown -= 1;
+ if (countdown <= 0) {
+ state = STATE_IDLE;
+ switchBuffers();
+ fireCallback();
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ private void fireCallback() {
+ if (callback != null) {
+ callback.run();
+ }
+ }
+
+ public float getSample(int bufferIndex, int sampleIndex) {
+ return displayBuffer.getSample(bufferIndex, sampleIndex);
+ }
+
+ public int getTriggerIndex() {
+ return displayBuffer.getTriggerIndex();
+ }
+
+ public int getFramesCaptured() {
+ return displayBuffer.getFramesCaptured();
+ }
+
+ public int getFramesPerBuffer() {
+ return FRAMES_PER_BUFFER;
+ }
+
+ public int getPostTriggerSize() {
+ return postTriggerSize;
+ }
+
+}
diff --git a/src/com/jsyn/scope/TriggerModel.java b/src/com/jsyn/scope/TriggerModel.java
new file mode 100644
index 0000000..0ded1fa
--- /dev/null
+++ b/src/com/jsyn/scope/TriggerModel.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.scope;
+
+import javax.swing.DefaultComboBoxModel;
+
+import com.jsyn.scope.AudioScope.TriggerMode;
+import com.jsyn.swing.ExponentialRangeModel;
+
+public class TriggerModel {
+ private ExponentialRangeModel levelModel;
+ private DefaultComboBoxModel<AudioScope.TriggerMode> modeModel;
+ private AudioScopeProbe source;
+
+ public TriggerModel() {
+ modeModel = new DefaultComboBoxModel<AudioScope.TriggerMode>();
+ modeModel.addElement(TriggerMode.AUTO);
+ modeModel.addElement(TriggerMode.NORMAL);
+ levelModel = new ExponentialRangeModel("TriggerLevel", 1000, 0.01, 2.0, 0.04);
+ }
+
+ public AudioScopeProbe getSource() {
+ return source;
+ }
+
+ public void setSource(AudioScopeProbe source) {
+ this.source = source;
+ }
+
+ public ExponentialRangeModel getLevelModel() {
+ return levelModel;
+ }
+
+ public void setLevelModel(ExponentialRangeModel levelModel) {
+ this.levelModel = levelModel;
+ }
+
+ public DefaultComboBoxModel<TriggerMode> getModeModel() {
+ return modeModel;
+ }
+
+ public void setModeModel(DefaultComboBoxModel<TriggerMode> modeModel) {
+ this.modeModel = modeModel;
+ }
+
+ public double getTriggerLevel() {
+ return levelModel.getDoubleValue();
+ }
+
+ public TriggerMode getMode() {
+ return (TriggerMode) modeModel.getSelectedItem();
+ }
+}
diff --git a/src/com/jsyn/scope/WaveTraceModel.java b/src/com/jsyn/scope/WaveTraceModel.java
new file mode 100644
index 0000000..e9d8bf9
--- /dev/null
+++ b/src/com/jsyn/scope/WaveTraceModel.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.scope;
+
+public interface WaveTraceModel {
+ int getSize();
+
+ int getVisibleSize();
+
+ int getStartIndex();
+
+ double getSample(int i);
+}
diff --git a/src/com/jsyn/scope/swing/AudioScopeProbeView.java b/src/com/jsyn/scope/swing/AudioScopeProbeView.java
new file mode 100644
index 0000000..59526e1
--- /dev/null
+++ b/src/com/jsyn/scope/swing/AudioScopeProbeView.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.scope.swing;
+
+import com.jsyn.scope.AudioScopeProbe;
+
+/**
+ * Wave display associated with a probe.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class AudioScopeProbeView {
+ private AudioScopeProbe probeModel;
+ private WaveTraceView waveTrace;
+
+ public AudioScopeProbeView(AudioScopeProbe probeModel) {
+ this.probeModel = probeModel;
+ waveTrace = new WaveTraceView(probeModel.getAutoScaleButtonModel(),
+ probeModel.getVerticalScaleModel());
+ waveTrace.setModel(probeModel.getWaveTraceModel());
+ }
+
+ public WaveTraceView getWaveTraceView() {
+ return waveTrace;
+ }
+
+ public AudioScopeProbe getModel() {
+ return probeModel;
+ }
+
+}
diff --git a/src/com/jsyn/scope/swing/AudioScopeView.java b/src/com/jsyn/scope/swing/AudioScopeView.java
new file mode 100644
index 0000000..31f1264
--- /dev/null
+++ b/src/com/jsyn/scope/swing/AudioScopeView.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.scope.swing;
+
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Dimension;
+import java.util.ArrayList;
+
+import javax.swing.JPanel;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import com.jsyn.scope.AudioScopeModel;
+import com.jsyn.scope.AudioScopeProbe;
+
+public class AudioScopeView extends JPanel {
+ private static final long serialVersionUID = -7507986850757860853L;
+ private AudioScopeModel audioScopeModel;
+ private ArrayList<AudioScopeProbeView> probeViews = new ArrayList<AudioScopeProbeView>();
+ private MultipleWaveDisplay multipleWaveDisplay;
+ private boolean showControls = false;
+ private ScopeControlPanel controlPanel = null;
+
+ public AudioScopeView() {
+ setBackground(Color.GREEN);
+ }
+
+ public void setModel(AudioScopeModel audioScopeModel) {
+ this.audioScopeModel = audioScopeModel;
+ // Create a view for each probe.
+ probeViews.clear();
+ for (AudioScopeProbe probeModel : audioScopeModel.getProbes()) {
+ AudioScopeProbeView audioScopeProbeView = new AudioScopeProbeView(probeModel);
+ probeViews.add(audioScopeProbeView);
+ }
+ setupGUI();
+
+ // Listener for signal change events.
+ audioScopeModel.addChangeListener(new ChangeListener() {
+ @Override
+ public void stateChanged(ChangeEvent e) {
+ multipleWaveDisplay.repaint();
+ }
+ });
+
+ }
+
+ private void setupGUI() {
+ removeAll();
+ setLayout(new BorderLayout());
+ multipleWaveDisplay = new MultipleWaveDisplay();
+
+ for (AudioScopeProbeView probeView : probeViews) {
+ multipleWaveDisplay.addWaveTrace(probeView.getWaveTraceView());
+ probeView.getModel().setColor(probeView.getWaveTraceView().getColor());
+ }
+
+ add(multipleWaveDisplay, BorderLayout.CENTER);
+
+ setMinimumSize(new Dimension(400, 200));
+ setPreferredSize(new Dimension(600, 250));
+ setMaximumSize(new Dimension(1200, 300));
+ }
+
+ /** @deprecated */
+ @Deprecated
+ public void setShowControls(boolean show) {
+ setControlsVisible(show);
+ }
+
+ public void setControlsVisible(boolean show) {
+ if (this.showControls) {
+ if (!show && (controlPanel != null)) {
+ remove(controlPanel);
+ }
+ } else {
+ if (show) {
+ if (controlPanel == null) {
+ controlPanel = new ScopeControlPanel(this);
+ }
+ add(controlPanel, BorderLayout.EAST);
+ validate();
+ }
+ }
+
+ this.showControls = show;
+ }
+
+ public AudioScopeModel getModel() {
+ return audioScopeModel;
+ }
+
+ public AudioScopeProbeView[] getProbeViews() {
+ return probeViews.toArray(new AudioScopeProbeView[0]);
+ }
+
+}
diff --git a/src/com/jsyn/scope/swing/MultipleWaveDisplay.java b/src/com/jsyn/scope/swing/MultipleWaveDisplay.java
new file mode 100644
index 0000000..0259850
--- /dev/null
+++ b/src/com/jsyn/scope/swing/MultipleWaveDisplay.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2011 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.scope.swing;
+
+import java.awt.Color;
+import java.awt.Graphics;
+import java.util.ArrayList;
+
+import javax.swing.JPanel;
+
+/**
+ * Display multiple waveforms together in different colors.
+ *
+ * @author Phil Burk (C) 2011 Mobileer Inc
+ */
+public class MultipleWaveDisplay extends JPanel {
+ private static final long serialVersionUID = -5157397030540800373L;
+
+ private ArrayList<WaveTraceView> waveTraceViews = new ArrayList<WaveTraceView>();
+ private Color[] defaultColors = {
+ Color.BLUE, Color.RED, Color.BLACK, Color.MAGENTA, Color.GREEN, Color.ORANGE
+ };
+
+ public MultipleWaveDisplay() {
+ setBackground(Color.WHITE);
+ }
+
+ public void addWaveTrace(WaveTraceView waveTraceView) {
+ if (waveTraceView.getColor() == null) {
+ waveTraceView.setColor(defaultColors[waveTraceViews.size() % defaultColors.length]);
+ }
+ waveTraceViews.add(waveTraceView);
+ }
+
+ @Override
+ public void paintComponent(Graphics g) {
+ super.paintComponent(g);
+ int width = getWidth();
+ int height = getHeight();
+ for (WaveTraceView waveTraceView : waveTraceViews.toArray(new WaveTraceView[0])) {
+ waveTraceView.drawWave(g, width, height);
+ }
+ }
+}
diff --git a/src/com/jsyn/scope/swing/ScopeControlPanel.java b/src/com/jsyn/scope/swing/ScopeControlPanel.java
new file mode 100644
index 0000000..7f3a026
--- /dev/null
+++ b/src/com/jsyn/scope/swing/ScopeControlPanel.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.scope.swing;
+
+import java.awt.GridLayout;
+
+import javax.swing.JPanel;
+
+import com.jsyn.scope.AudioScopeModel;
+
+public class ScopeControlPanel extends JPanel {
+ private static final long serialVersionUID = 7738305116057614812L;
+ private AudioScopeModel audioScopeModel;
+ private ScopeTriggerPanel triggerPanel;
+ private JPanel probeRows;
+
+ public ScopeControlPanel(AudioScopeView audioScopeView) {
+ setLayout(new GridLayout(0, 1));
+ this.audioScopeModel = audioScopeView.getModel();
+ triggerPanel = new ScopeTriggerPanel(audioScopeModel);
+ add(triggerPanel);
+
+ probeRows = new JPanel();
+ probeRows.setLayout(new GridLayout(1, 0));
+ add(probeRows);
+ for (AudioScopeProbeView probeView : audioScopeView.getProbeViews()) {
+ ScopeProbePanel probePanel = new ScopeProbePanel(probeView);
+ probeRows.add(probePanel);
+ }
+ }
+
+}
diff --git a/src/com/jsyn/scope/swing/ScopeProbePanel.java b/src/com/jsyn/scope/swing/ScopeProbePanel.java
new file mode 100644
index 0000000..9cb82af
--- /dev/null
+++ b/src/com/jsyn/scope/swing/ScopeProbePanel.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.scope.swing;
+
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+import javax.swing.BorderFactory;
+import javax.swing.JCheckBox;
+import javax.swing.JPanel;
+import javax.swing.JToggleButton.ToggleButtonModel;
+
+import com.jsyn.scope.AudioScopeProbe;
+import com.jsyn.swing.RotaryTextController;
+
+public class ScopeProbePanel extends JPanel {
+ private static final long serialVersionUID = 4511589171299298548L;
+ private AudioScopeProbeView audioScopeProbeView;
+ private AudioScopeProbe audioScopeProbe;
+ private RotaryTextController verticalScaleKnob;
+ private JCheckBox autoBox;
+ private ToggleButtonModel autoScaleModel;
+
+ public ScopeProbePanel(AudioScopeProbeView probeView) {
+ this.audioScopeProbeView = probeView;
+ setLayout(new BorderLayout());
+
+ setBorder(BorderFactory.createLineBorder(Color.GRAY, 3));
+
+ // Add a colored box to match the waveform color.
+ JPanel colorPanel = new JPanel();
+ colorPanel.setMinimumSize(new Dimension(40, 40));
+ audioScopeProbe = probeView.getModel();
+ colorPanel.setBackground(audioScopeProbe.getColor());
+ add(colorPanel, BorderLayout.NORTH);
+
+ // Knob for tweaking vertical range.
+ verticalScaleKnob = new RotaryTextController(audioScopeProbeView.getWaveTraceView()
+ .getVerticalRangeModel(), 5);
+ add(verticalScaleKnob, BorderLayout.CENTER);
+ verticalScaleKnob.setTitle("YScale");
+
+ // Auto ranging checkbox.
+ autoBox = new JCheckBox("Auto");
+ autoScaleModel = audioScopeProbeView.getWaveTraceView().getAutoButtonModel();
+ autoScaleModel.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ ToggleButtonModel model = (ToggleButtonModel) e.getSource();
+ boolean enabled = !model.isSelected();
+ System.out.println("Knob enabled = " + enabled);
+ verticalScaleKnob.setEnabled(!model.isSelected());
+ }
+ });
+ autoBox.setModel(autoScaleModel);
+ add(autoBox, BorderLayout.SOUTH);
+
+ verticalScaleKnob.setEnabled(!autoScaleModel.isSelected());
+
+ setMinimumSize(new Dimension(80, 100));
+ setPreferredSize(new Dimension(80, 150));
+ setMaximumSize(new Dimension(120, 200));
+ }
+
+}
diff --git a/src/com/jsyn/scope/swing/ScopeTriggerPanel.java b/src/com/jsyn/scope/swing/ScopeTriggerPanel.java
new file mode 100644
index 0000000..e466c05
--- /dev/null
+++ b/src/com/jsyn/scope/swing/ScopeTriggerPanel.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.scope.swing;
+
+import java.awt.BorderLayout;
+
+import javax.swing.JComboBox;
+import javax.swing.JPanel;
+
+import com.jsyn.scope.AudioScopeModel;
+import com.jsyn.scope.TriggerModel;
+import com.jsyn.swing.RotaryTextController;
+
+public class ScopeTriggerPanel extends JPanel {
+ private static final long serialVersionUID = 4511589171299298548L;
+ private JComboBox triggerModeComboBox;
+ private RotaryTextController triggerLevelKnob;
+
+ public ScopeTriggerPanel(AudioScopeModel audioScopeModel) {
+ setLayout(new BorderLayout());
+ TriggerModel triggerModel = audioScopeModel.getTriggerModel();
+ triggerModeComboBox = new JComboBox(triggerModel.getModeModel());
+ add(triggerModeComboBox, BorderLayout.NORTH);
+
+ triggerLevelKnob = new RotaryTextController(triggerModel.getLevelModel(), 5);
+
+ add(triggerLevelKnob, BorderLayout.CENTER);
+ triggerLevelKnob.setTitle("Trigger Level");
+ }
+
+}
diff --git a/src/com/jsyn/scope/swing/WaveTraceView.java b/src/com/jsyn/scope/swing/WaveTraceView.java
new file mode 100644
index 0000000..849a6f4
--- /dev/null
+++ b/src/com/jsyn/scope/swing/WaveTraceView.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.scope.swing;
+
+import java.awt.Color;
+import java.awt.Graphics;
+
+import javax.swing.JToggleButton.ToggleButtonModel;
+
+import com.jsyn.scope.WaveTraceModel;
+import com.jsyn.swing.ExponentialRangeModel;
+
+public class WaveTraceView {
+ private static final double AUTO_DECAY = 0.95;
+ private WaveTraceModel waveTraceModel;
+ private Color color;
+ private ExponentialRangeModel verticalScaleModel;
+ private ToggleButtonModel autoScaleButtonModel;
+
+ private double xScaler;
+ private double yScalar;
+ private int centerY;
+
+ public WaveTraceView(ToggleButtonModel autoButtonModel, ExponentialRangeModel verticalRangeModel) {
+ this.verticalScaleModel = verticalRangeModel;
+ this.autoScaleButtonModel = autoButtonModel;
+ }
+
+ public Color getColor() {
+ return color;
+ }
+
+ public void setColor(Color color) {
+ this.color = color;
+ }
+
+ public ExponentialRangeModel getVerticalRangeModel() {
+ return verticalScaleModel;
+ }
+
+ public ToggleButtonModel getAutoButtonModel() {
+ return autoScaleButtonModel;
+ }
+
+ public void setModel(WaveTraceModel waveTraceModel) {
+ this.waveTraceModel = waveTraceModel;
+ }
+
+ public int convertRealToY(double r) {
+ return centerY - (int) (yScalar * r);
+ }
+
+ public void drawWave(Graphics g, int width, int height) {
+ double sampleMax = 0.0;
+ double sampleMin = 0.0;
+ g.setColor(color);
+ int numSamples = waveTraceModel.getVisibleSize();
+ if (numSamples > 0) {
+ xScaler = (double) width / numSamples;
+ // Scale by 0.5 because it is bipolar.
+ yScalar = 0.5 * height / verticalScaleModel.getDoubleValue();
+ centerY = height / 2;
+
+ // Calculate position of first point.
+ int x1 = 0;
+ int offset = waveTraceModel.getStartIndex();
+ double value = waveTraceModel.getSample(offset);
+ int y1 = convertRealToY(value);
+
+ // Draw lines to remaining points.
+ for (int i = 1; i < numSamples; i++) {
+ int x2 = (int) (i * xScaler);
+ value = waveTraceModel.getSample(offset + i);
+ int y2 = convertRealToY(value);
+ g.drawLine(x1, y1, x2, y2);
+ x1 = x2;
+ y1 = y2;
+ // measure min and max for auto
+ if (value > sampleMax) {
+ sampleMax = value;
+ } else if (value < sampleMin) {
+ sampleMin = value;
+ }
+ }
+
+ autoScaleRange(sampleMax);
+ }
+ }
+
+ // Autoscale the vertical range.
+ private void autoScaleRange(double sampleMax) {
+ if (autoScaleButtonModel.isSelected()) {
+ double scaledMax = sampleMax * 1.1;
+ double current = verticalScaleModel.getDoubleValue();
+ if (scaledMax > current) {
+ verticalScaleModel.setDoubleValue(scaledMax);
+ } else {
+ double decayed = current * AUTO_DECAY;
+ if (decayed > verticalScaleModel.getMinimum()) {
+ if (scaledMax < decayed) {
+ verticalScaleModel.setDoubleValue(decayed);
+ }
+ }
+ }
+ }
+ }
+
+}
diff --git a/src/com/jsyn/swing/ASCIIMusicKeyboard.java b/src/com/jsyn/swing/ASCIIMusicKeyboard.java
new file mode 100644
index 0000000..40f7792
--- /dev/null
+++ b/src/com/jsyn/swing/ASCIIMusicKeyboard.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright 2012 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.swing;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.KeyEvent;
+import java.awt.event.KeyListener;
+import java.util.HashSet;
+
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+
+/**
+ * Support for playing musical scales on the ASCII keyboard of a computer. Has a Sustain checkbox
+ * that simulates a sustain pedal. Auto-repeat keys are detected and suppressed.
+ *
+ * @author Phil Burk (C) 2012 Mobileer Inc
+ */
+@SuppressWarnings("serial")
+public abstract class ASCIIMusicKeyboard extends JPanel {
+ private JCheckBox sustainBox;
+ private JButton focusButton;
+ public static final String PENTATONIC_KEYS = "zxcvbasdfgqwert12345";
+ public static final String SEPTATONIC_KEYS = "zxcvbnmasdfghjqwertyu1234567890";
+ private String keyboardLayout = SEPTATONIC_KEYS; /* default music keyboard layout */
+ private int basePitch = 48;
+ private KeyListener keyListener;
+ private JLabel countLabel;
+ private int onCount;
+ private int offCount;
+ private int pressedCount;
+ private int releasedCount;
+ private HashSet<Integer> pressedKeys = new HashSet<Integer>();
+ private HashSet<Integer> onKeys = new HashSet<Integer>();
+
+ public ASCIIMusicKeyboard() {
+ focusButton = new JButton("Click here to play ASCII keys.");
+ focusButton.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent arg0) {
+ }
+ });
+ keyListener = new KeyListener() {
+
+ @Override
+ public void keyPressed(KeyEvent e) {
+ int key = e.getKeyChar();
+ int idx = keyboardLayout.indexOf(key);
+ System.out.println("keyPressed " + idx);
+ if (idx >= 0) {
+ if (!pressedKeys.contains(idx)) {
+ keyOn(convertIndexToPitch(idx));
+ onCount++;
+ pressedKeys.add(idx);
+ onKeys.add(idx);
+ }
+ }
+ pressedCount++;
+ updateCountLabel();
+ }
+
+ @Override
+ public void keyReleased(KeyEvent e) {
+ int key = e.getKeyChar();
+ int idx = keyboardLayout.indexOf(key);
+ System.out.println("keyReleased " + idx);
+ if (idx >= 0) {
+ if (!sustainBox.isSelected()) {
+ noteOffInternal(idx);
+ onKeys.remove(idx);
+ }
+ pressedKeys.remove(idx);
+ }
+ releasedCount++;
+ updateCountLabel();
+ }
+
+ @Override
+ public void keyTyped(KeyEvent arg0) {
+ }
+ };
+ focusButton.addKeyListener(keyListener);
+ add(focusButton);
+
+ sustainBox = new JCheckBox("sustain");
+ sustainBox.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent arg0) {
+ if (!sustainBox.isSelected()) {
+ for (Integer noteIndex : onKeys) {
+ noteOffInternal(noteIndex);
+ }
+ onKeys.clear();
+ }
+ }
+ });
+ add(sustainBox);
+ sustainBox.addKeyListener(keyListener);
+
+ countLabel = new JLabel("0");
+ add(countLabel);
+ }
+
+ private void noteOffInternal(int idx) {
+ keyOff(convertIndexToPitch(idx));
+ offCount++;
+ }
+
+ protected void updateCountLabel() {
+ countLabel.setText(onCount + "/" + offCount + ", " + pressedCount + "/" + releasedCount);
+ }
+
+ /**
+ * Convert index to a MIDI noteNumber in a major scale. Result will be offset by the basePitch.
+ */
+ public int convertIndexToPitch(int keyIndex) {
+ int scale[] = {
+ 0, 2, 4, 5, 7, 9, 11
+ };
+ int octave = keyIndex / scale.length;
+ int idx = keyIndex % scale.length;
+ int pitch = (octave * 12) + scale[idx];
+ return pitch + basePitch;
+ }
+
+ /**
+ * This will be called when a key is released. It may also be called for sustaining notes when
+ * the Sustain check box is turned off.
+ *
+ * @param pitch
+ */
+ public abstract void keyOff(int keyIndex);
+
+ /**
+ * This will be called when a key is pressed.
+ *
+ * @param pitch
+ */
+ public abstract void keyOn(int keyIndex);
+
+ public String getKeyboardLayout() {
+ return keyboardLayout;
+ }
+
+ /**
+ * Specify the keys that will be active for music. If the first character in the layout is
+ * pressed then keyOn() will be called with 0. Default is SEPTATONIC_KEYS.
+ *
+ * @param keyboardLayout defines order of playable keys
+ */
+ public void setKeyboardLayout(String keyboardLayout) {
+ this.keyboardLayout = keyboardLayout;
+ }
+
+ public int getBasePitch() {
+ return basePitch;
+ }
+
+ /**
+ * Define offset used by convertIndexToPitch().
+ *
+ * @param basePitch
+ */
+ public void setBasePitch(int basePitch) {
+ this.basePitch = basePitch;
+ }
+
+ /**
+ * @return
+ */
+ public KeyListener getKeyListener() {
+ return keyListener;
+ }
+}
diff --git a/src/com/jsyn/swing/DoubleBoundedRangeModel.java b/src/com/jsyn/swing/DoubleBoundedRangeModel.java
new file mode 100644
index 0000000..647e8da
--- /dev/null
+++ b/src/com/jsyn/swing/DoubleBoundedRangeModel.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2002 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.swing;
+
+import javax.swing.DefaultBoundedRangeModel;
+
+/**
+ * Double precision data model for sliders and knobs. Maps integer range info to a double value.
+ *
+ * @author Phil Burk, (C) 2002 SoftSynth.com, PROPRIETARY and CONFIDENTIAL
+ */
+public class DoubleBoundedRangeModel extends DefaultBoundedRangeModel {
+ private static final long serialVersionUID = 284361767102120148L;
+ protected String name;
+ private double dmin;
+ private double dmax;
+
+ public DoubleBoundedRangeModel(String name, int resolution, double dmin, double dmax,
+ double dval) {
+ this.name = name;
+ this.dmin = dmin;
+ this.dmax = dmax;
+ setMinimum(0);
+ setMaximum(resolution);
+ setDoubleValue(dval);
+ }
+
+ public boolean equivalentTo(Object other) {
+ if (!(other instanceof DoubleBoundedRangeModel))
+ return false;
+ DoubleBoundedRangeModel otherModel = (DoubleBoundedRangeModel) other;
+ return (getValue() == otherModel.getValue());
+ }
+
+ /** Set name of value. This may be used in labels or when saving the value. */
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public double getDoubleMinimum() {
+ return dmin;
+ }
+
+ public double getDoubleMaximum() {
+ return dmax;
+ }
+
+ public double sliderToDouble(int sliderValue) {
+ double doubleMin = getDoubleMinimum();
+ return doubleMin + ((getDoubleMaximum() - doubleMin) * sliderValue / getMaximum());
+ }
+
+ public int doubleToSlider(double dval) {
+ double doubleMin = getDoubleMinimum();
+ // TODO consider using Math.floor() instead of (int) if not too slow.
+ return (int) Math.round(getMaximum() * (dval - doubleMin)
+ / (getDoubleMaximum() - doubleMin));
+ }
+
+ public double getDoubleValue() {
+ return sliderToDouble(getValue());
+ }
+
+ public void setDoubleValue(double dval) {
+ setValue(doubleToSlider(dval));
+ }
+
+}
diff --git a/src/com/jsyn/swing/DoubleBoundedRangeSlider.java b/src/com/jsyn/swing/DoubleBoundedRangeSlider.java
new file mode 100644
index 0000000..c67a4cf
--- /dev/null
+++ b/src/com/jsyn/swing/DoubleBoundedRangeSlider.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2002 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.swing;
+
+import java.util.Hashtable;
+
+import javax.swing.BorderFactory;
+import javax.swing.JLabel;
+import javax.swing.JSlider;
+import javax.swing.border.TitledBorder;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import com.softsynth.util.NumericOutput;
+
+/**
+ * Slider that takes a DoubleBoundedRangeModel. It displays the current value in a titled border.
+ *
+ * @author Phil Burk, (C) 2002 SoftSynth.com, PROPRIETARY and CONFIDENTIAL
+ */
+
+public class DoubleBoundedRangeSlider extends JSlider {
+ /**
+ *
+ */
+ private static final long serialVersionUID = -440390322602838998L;
+ /** Places after decimal point for display. */
+ private int places;
+
+ public DoubleBoundedRangeSlider(DoubleBoundedRangeModel model) {
+ this(model, 5);
+ }
+
+ public DoubleBoundedRangeSlider(DoubleBoundedRangeModel model, int places) {
+ super(model);
+ this.places = places;
+ setBorder(BorderFactory.createTitledBorder(generateTitleText()));
+ model.addChangeListener(new ChangeListener() {
+ @Override
+ public void stateChanged(ChangeEvent e) {
+ updateTitle();
+ }
+ });
+ }
+
+ protected void updateTitle() {
+ TitledBorder border = (TitledBorder) getBorder();
+ if (border != null) {
+ border.setTitle(generateTitleText());
+ repaint();
+ }
+ }
+
+ String generateTitleText() {
+ DoubleBoundedRangeModel model = (DoubleBoundedRangeModel) getModel();
+ double val = model.getDoubleValue();
+ String valText = NumericOutput.doubleToString(val, 0, places);
+ return model.getName() + " = " + valText;
+ }
+
+ public void makeStandardLabels(int labelSpacing) {
+ setMajorTickSpacing(labelSpacing / 2);
+ setLabelTable(createStandardLabels(labelSpacing));
+ setPaintTicks(true);
+ setPaintLabels(true);
+ }
+
+ public double nextLabelValue(double current, double delta) {
+ return current + delta;
+ }
+
+ public void makeLabels(double start, double delta, int places) {
+ DoubleBoundedRangeModel model = (DoubleBoundedRangeModel) getModel();
+ // Create the label table
+ Hashtable<Integer, JLabel> labelTable = new Hashtable<Integer, JLabel>();
+ double dval = start;
+ while (dval <= model.getDoubleMaximum()) {
+ int sliderValue = model.doubleToSlider(dval);
+ String text = NumericOutput.doubleToString(dval, 0, places);
+ labelTable.put(new Integer(sliderValue), new JLabel(text));
+ dval = nextLabelValue(dval, delta);
+ }
+ setLabelTable(labelTable);
+ setPaintLabels(true);
+ }
+
+}
diff --git a/src/com/jsyn/swing/DoubleBoundedTextField.java b/src/com/jsyn/swing/DoubleBoundedTextField.java
new file mode 100644
index 0000000..9605ae1
--- /dev/null
+++ b/src/com/jsyn/swing/DoubleBoundedTextField.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2000 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.swing;
+
+import java.awt.Color;
+import java.awt.event.KeyAdapter;
+import java.awt.event.KeyEvent;
+
+import javax.swing.JTextField;
+import javax.swing.SwingConstants;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import com.softsynth.util.NumericOutput;
+
+/**
+ * TextField that turns pink when modified, and white when the value is entered.
+ *
+ * @author (C) 2000-2010 Phil Burk, Mobileer Inc
+ * @version 16
+ */
+
+public class DoubleBoundedTextField extends JTextField {
+ private static final long serialVersionUID = 6882779668177620812L;
+ boolean modified = false;
+ int numCharacters;
+ private DoubleBoundedRangeModel model;
+
+ public DoubleBoundedTextField(DoubleBoundedRangeModel pModel, int numCharacters) {
+ super(numCharacters);
+ this.model = pModel;
+ this.numCharacters = numCharacters;
+ setHorizontalAlignment(SwingConstants.LEADING);
+ setValue(model.getDoubleValue());
+ addKeyListener(new KeyAdapter() {
+ @Override
+ public void keyTyped(KeyEvent e) {
+ if (e.getKeyChar() == '\n') {
+ model.setDoubleValue(getValue());
+ } else {
+ markDirty();
+ }
+ }
+ });
+ model.addChangeListener(new ChangeListener() {
+ @Override
+ public void stateChanged(ChangeEvent e) {
+ setValue(model.getDoubleValue());
+ }
+ });
+ }
+
+ private void markDirty() {
+ modified = true;
+ setBackground(Color.pink);
+ repaint();
+ }
+
+ private void markClean() {
+ modified = false;
+ setBackground(Color.white);
+ setCaretPosition(0);
+ repaint();
+ }
+
+ @Override
+ public void setText(String text) {
+ markDirty();
+ super.setText(text);
+ }
+
+ private double getValue() throws NumberFormatException {
+ double val = Double.valueOf(getText()).doubleValue();
+ markClean();
+ return val;
+ }
+
+ private void setValue(double value) {
+ super.setText(NumericOutput.doubleToString(value, 1, 4));
+ markClean();
+ }
+}
diff --git a/src/com/jsyn/swing/EnvelopeEditorBox.java b/src/com/jsyn/swing/EnvelopeEditorBox.java
new file mode 100644
index 0000000..6394fce
--- /dev/null
+++ b/src/com/jsyn/swing/EnvelopeEditorBox.java
@@ -0,0 +1,570 @@
+/*
+ * Copyright 1997 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.swing;
+
+import java.awt.Color;
+import java.awt.Graphics;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
+import java.awt.event.MouseMotionListener;
+import java.util.ArrayList;
+
+import com.softsynth.util.NumericOutput;
+
+/**
+ * Edit a list of ordered duration,value pairs suitable for use with a SegmentedEnvelope.
+ *
+ * @author (C) 1997-2013 Phil Burk, SoftSynth.com
+ * @see SynthEnvelope
+ */
+
+/* ========================================================================== */
+public class EnvelopeEditorBox extends XYController implements MouseListener, MouseMotionListener {
+ EnvelopePoints points;
+ ArrayList<EditListener> listeners = new ArrayList<EditListener>();
+ int dragIndex = -1;
+ double dragLowLimit;
+ double dragHighLimit;
+ double draggedPoint[];
+ double xBefore; // WX value before point
+ double xPicked; // WX value of picked point
+ double dragWX;
+ double dragWY;
+ int maxPoints = Integer.MAX_VALUE;
+ int radius = 4;
+ double verticalBarSpacing = 1.0;
+ boolean verticalBarsEnabled = false;
+ double maximumXRange = Double.MAX_VALUE;
+ double minimumXRange = 0.1;
+ int rangeStart = -1; // gx coordinates
+ int rangeEnd = -1;
+ int mode = EDIT_POINTS;
+ public final static int EDIT_POINTS = 0;
+ public final static int SELECT_SUSTAIN = 1;
+ public final static int SELECT_RELEASE = 2;
+
+ Color rangeColor = Color.RED;
+ Color sustainColor = Color.BLUE;
+ Color releaseColor = Color.YELLOW;
+ Color overlapColor = Color.GREEN;
+ Color firstLineColor = Color.GRAY;
+
+ public interface EditListener {
+ public void objectEdited(Object editor, Object edited);
+ }
+
+ public EnvelopeEditorBox() {
+ addMouseListener(this);
+ addMouseMotionListener(this);
+ }
+
+ public void setMaximumXRange(double maxXRange) {
+ maximumXRange = maxXRange;
+ }
+
+ public double getMaximumXRange() {
+ return maximumXRange;
+ }
+
+ public void setMinimumXRange(double minXRange) {
+ minimumXRange = minXRange;
+ }
+
+ public double getMinimumXRange() {
+ return minimumXRange;
+ }
+
+ public void setSelection(int start, int end) {
+ switch (mode) {
+ case SELECT_SUSTAIN:
+ points.setSustainLoop(start, end);
+ break;
+ case SELECT_RELEASE:
+ points.setReleaseLoop(start, end);
+ break;
+ }
+ // System.out.println("start = " + start + ", end = " + end );
+ }
+
+ /** Set mode to either EDIT_POINTS or SELECT_SUSTAIN, SELECT_RELEASE; */
+ public void setMode(int mode) {
+ this.mode = mode;
+ }
+
+ public int getMode() {
+ return mode;
+ }
+
+ /**
+ * Add a listener to receive edit events. Listener will be passed the editor object and the
+ * edited object.
+ */
+ public void addEditListener(EditListener listener) {
+ listeners.add(listener);
+ }
+
+ public void removeEditListener(EditListener listener) {
+ listeners.remove(listener);
+ }
+
+ /** Send event to every subscribed listener. */
+ public void fireObjectEdited() {
+ for (EditListener listener : listeners) {
+ listener.objectEdited(this, points);
+ }
+ }
+
+ public void setMaxPoints(int maxPoints) {
+ this.maxPoints = maxPoints;
+ }
+
+ public int getMaxPoints() {
+ return maxPoints;
+ }
+
+ public int getNumPoints() {
+ return points.size();
+ }
+
+ public void setPoints(EnvelopePoints points) {
+ this.points = points;
+ setMaxWorldY(points.getMaximumValue());
+ }
+
+ public EnvelopePoints getPoints() {
+ return points;
+ }
+
+ /**
+ * Return index of point before this X position.
+ */
+ private int findPointBefore(double wx) {
+ int pnt = -1;
+ double px = 0.0;
+ xBefore = 0.0;
+ for (int i = 0; i < points.size(); i++) {
+ px += points.getDuration(i);
+ if (px > wx)
+ break;
+ pnt = i;
+ xBefore = px;
+ }
+ return pnt;
+ }
+
+ private int pickPoint(double wx, double wxAperture, double wy, double wyAperture) {
+ double px = 0.0;
+ double wxLow = wx - wxAperture;
+ double wxHigh = wx + wxAperture;
+ // System.out.println("wxLow = " + wxLow + ", wxHigh = " + wxHigh );
+ double wyLow = wy - wyAperture;
+ double wyHigh = wy + wyAperture;
+ // System.out.println("wyLow = " + wyLow + ", wyHigh = " + wyHigh );
+ double wxScale = 1.0 / wxAperture; // only divide once, then multiply
+ double wyScale = 1.0 / wyAperture;
+ int bestPoint = -1;
+ double bestDistance = Double.MAX_VALUE;
+ for (int i = 0; i < points.size(); i++) {
+ double dar[] = points.getPoint(i);
+ px += dar[0];
+ double py = dar[1];
+ // System.out.println("px = " + px + ", py = " + py );
+ if ((px > wxLow) && (px < wxHigh) && (py > wyLow) && (py < wyHigh)) {
+ /* Inside pick range. Calculate distance squared. */
+ double ndx = (px - wx) * wxScale;
+ double ndy = (py - wy) * wyScale;
+ double dist = (ndx * ndx) + (ndy * ndy);
+ // System.out.println("dist = " + dist );
+ if (dist < bestDistance) {
+ bestPoint = i;
+ bestDistance = dist;
+ xPicked = px;
+ }
+ }
+ }
+ return bestPoint;
+ }
+
+ private void clickDownRange(boolean shiftDown, int gx, int gy) {
+ setSelection(-1, -1);
+ rangeStart = rangeEnd = gx;
+ repaint();
+ }
+
+ private void dragRange(int gx, int gy) {
+ rangeEnd = gx;
+ repaint();
+ }
+
+ private void clickUpRange(int gx, int gy) {
+ dragRange(gx, gy);
+ if (rangeEnd < rangeStart) {
+ int temp = rangeEnd;
+ rangeEnd = rangeStart;
+ rangeStart = temp;
+ }
+ // System.out.println("clickUpRange: gx = " + gx + ", rangeStart = " +
+ // rangeStart );
+ double wx = convertGXtoWX(rangeStart);
+ int i0 = findPointBefore(wx);
+ wx = convertGXtoWX(rangeEnd);
+ int i1 = findPointBefore(wx);
+
+ if (i1 == i0) {
+ // set single point at zero so there is nothing played for queueOn()
+ if (gx < 0) {
+ setSelection(0, 0);
+ }
+ // else clear any existing loop
+ } else if (i1 == (i0 + 1)) {
+ setSelection(i1 + 1, i1 + 1); // set to a single point
+ } else if (i1 > (i0 + 1)) {
+ setSelection(i0 + 1, i1 + 1); // set to a range of two or more
+ }
+
+ rangeStart = -1;
+ rangeEnd = -1;
+ fireObjectEdited();
+ }
+
+ private void clickDownPoints(boolean shiftDown, int gx, int gy) {
+ dragIndex = -1;
+ double wx = convertGXtoWX(gx);
+ double wy = convertGYtoWY(gy);
+ // calculate world values for aperture
+ double wxAp = convertGXtoWX(radius + 2) - convertGXtoWX(0);
+ // System.out.println("wxAp = " + wxAp );
+ double wyAp = convertGYtoWY(0) - convertGYtoWY(radius + 2);
+ // System.out.println("wyAp = " + wyAp );
+ int pnt = pickPoint(wx, wxAp, wy, wyAp);
+ // System.out.println("pickPoint = " + pnt);
+ if (shiftDown) {
+ if (pnt >= 0) {
+ points.removePoint(pnt);
+ repaint();
+ }
+ } else {
+ if (pnt < 0) // didn't hit one so look for point to left of click
+ {
+ if (points.size() < maxPoints) // add if room
+ {
+ pnt = findPointBefore(wx);
+ // System.out.println("pointBefore = " + pnt);
+ dragIndex = pnt + 1;
+ if (pnt == (points.size() - 1)) {
+ points.add(wx - xBefore, wy);
+ } else {
+ points.insert(dragIndex, wx - xBefore, wy);
+ }
+ dragLowLimit = xBefore;
+ dragHighLimit = wx + (maximumXRange - points.getTotalDuration());
+ repaint();
+ }
+ } else
+ // hit one so drag it
+ {
+ dragIndex = pnt;
+ if (dragIndex <= 0)
+ dragLowLimit = 0.0; // FIXME
+ else
+ dragLowLimit = xPicked - points.getPoint(dragIndex)[0];
+ dragHighLimit = xPicked + (maximumXRange - points.getTotalDuration());
+ // System.out.println("dragLowLimit = " + dragLowLimit );
+ }
+ }
+ // Set up drag point if we are dragging.
+ if (dragIndex >= 0) {
+ draggedPoint = points.getPoint(dragIndex);
+ }
+
+ }
+
+ private void dragPoint(int gx, int gy) {
+ if (dragIndex < 0)
+ return;
+
+ double wx = convertGXtoWX(gx);
+ if (wx < dragLowLimit)
+ wx = dragLowLimit;
+ else if (wx > dragHighLimit)
+ wx = dragHighLimit;
+ draggedPoint[0] = wx - dragLowLimit; // duration
+
+ double wy = convertGYtoWY(gy);
+ wy = clipWorldY(wy);
+ draggedPoint[1] = wy;
+ dragWY = wy;
+ dragWX = wx;
+ points.setDirty(true);
+ repaint();
+ }
+
+ private void clickUpPoints(int gx, int gy) {
+ dragPoint(gx, gy);
+ fireObjectEdited();
+ dragIndex = -1;
+ }
+
+ // Implement the MouseMotionListener interface for AWT 1.1
+ @Override
+ public void mouseDragged(MouseEvent e) {
+ int x = e.getX();
+ int y = e.getY();
+ if (points == null)
+ return;
+ if (mode == EDIT_POINTS) {
+ dragPoint(x, y);
+ } else {
+ dragRange(x, y);
+ }
+ }
+
+ @Override
+ public void mouseMoved(MouseEvent e) {
+ }
+
+ // Implement the MouseListener interface for AWT 1.1
+ @Override
+ public void mousePressed(MouseEvent e) {
+ int x = e.getX();
+ int y = e.getY();
+ if (points == null)
+ return;
+ if (mode == EDIT_POINTS) {
+ clickDownPoints(e.isShiftDown(), x, y);
+ } else {
+ clickDownRange(e.isShiftDown(), x, y);
+ }
+ }
+
+ @Override
+ public void mouseClicked(MouseEvent e) {
+ }
+
+ @Override
+ public void mouseReleased(MouseEvent e) {
+ int x = e.getX();
+ int y = e.getY();
+ if (points == null)
+ return;
+ if (mode == EDIT_POINTS) {
+ clickUpPoints(x, y);
+ } else {
+ clickUpRange(x, y);
+ }
+ }
+
+ @Override
+ public void mouseEntered(MouseEvent e) {
+ }
+
+ @Override
+ public void mouseExited(MouseEvent e) {
+ }
+
+ /**
+ * Draw selected range.
+ */
+ private void drawRange(Graphics g) {
+ if (rangeStart >= 0) {
+ int height = bounds().height;
+ int gx0 = 0, gx1 = 0;
+
+ if (rangeEnd < rangeStart) {
+ gx0 = rangeEnd;
+ gx1 = rangeStart;
+ } else {
+ gx0 = rangeStart;
+ gx1 = rangeEnd;
+ }
+ g.setColor(rangeColor);
+ g.fillRect(gx0, 0, gx1 - gx0, height);
+ }
+ }
+
+ private void drawUnderSelection(Graphics g, int start, int end) {
+ if (start >= 0) {
+ int height = getHeight();
+ int gx0 = 0, gx1 = radius;
+ double wx = 0.0;
+ for (int i = 0; i <= (end - 1); i++) {
+ double dar[] = (double[]) points.elementAt(i);
+ wx += dar[0];
+ if (start == (i + 1)) {
+ gx0 = convertWXtoGX(wx) + radius;
+ }
+ if (end == (i + 1)) {
+ gx1 = convertWXtoGX(wx) + radius;
+ }
+ }
+ if (gx0 == gx1)
+ gx0 = gx0 - radius;
+ g.fillRect(gx0, 0, gx1 - gx0, height);
+ }
+ }
+
+ private void drawSelections(Graphics g) {
+ int sus0 = points.getSustainBegin();
+ int sus1 = points.getSustainEnd();
+ int rel0 = points.getReleaseBegin();
+ int rel1 = points.getReleaseEnd();
+
+ g.setColor(sustainColor);
+ drawUnderSelection(g, sus0, sus1);
+ g.setColor(releaseColor);
+ drawUnderSelection(g, rel0, rel1);
+ // draw overlapping sustain and release region
+ if (sus1 >= rel0) {
+ int sel1 = (rel1 < sus1) ? rel1 : sus1;
+ g.setColor(overlapColor);
+ drawUnderSelection(g, rel0, sel1);
+ }
+ }
+
+ /**
+ * Override this to draw a grid or other stuff under the envelope.
+ */
+ public void drawUnderlay(Graphics g) {
+ if (dragIndex < 0) {
+ drawSelections(g);
+ drawRange(g);
+ }
+ if (verticalBarsEnabled)
+ drawVerticalBars(g);
+ }
+
+ public void setVerticalBarsEnabled(boolean flag) {
+ verticalBarsEnabled = flag;
+ }
+
+ public boolean areVerticalBarsEnabled() {
+ return verticalBarsEnabled;
+ }
+
+ /**
+ * Set spacing in world coordinates.
+ */
+ public void setVerticalBarSpacing(double spacing) {
+ verticalBarSpacing = spacing;
+ }
+
+ public double getVerticalBarSpacing() {
+ return verticalBarSpacing;
+ }
+
+ /**
+ * Draw vertical lines.
+ */
+ private void drawVerticalBars(Graphics g) {
+ int width = getWidth();
+ int height = getHeight();
+ double wx = verticalBarSpacing;
+ int gx;
+
+ // g.setColor( getBackground().darker() );
+ g.setColor(Color.lightGray);
+ while (true) {
+ gx = convertWXtoGX(wx);
+ if (gx > width)
+ break;
+ g.drawLine(gx, 0, gx, height);
+ wx += verticalBarSpacing;
+ }
+ }
+
+ public void drawPoints(Graphics g, Color lineColor) {
+ double wx = 0.0;
+ int gx1 = 0;
+ int gy1 = getHeight();
+ for (int i = 0; i < points.size(); i++) {
+ double dar[] = (double[]) points.elementAt(i);
+ wx += dar[0];
+ double wy = dar[1];
+ int gx2 = convertWXtoGX(wx);
+ int gy2 = convertWYtoGY(wy);
+ if (i == 0) {
+ g.setColor(isEnabled() ? firstLineColor : firstLineColor.darker());
+ g.drawLine(gx1, gy1, gx2, gy2);
+ g.setColor(isEnabled() ? lineColor : lineColor.darker());
+ } else if (i > 0) {
+ g.drawLine(gx1, gy1, gx2, gy2);
+ }
+ int diameter = (2 * radius) + 1;
+ g.fillOval(gx2 - radius, gy2 - radius, diameter, diameter);
+ gx1 = gx2;
+ gy1 = gy2;
+ }
+ }
+
+ public void drawAllPoints(Graphics g) {
+ drawPoints(g, getForeground());
+ }
+
+ /* Override default paint action. */
+ @Override
+ public void paint(Graphics g) {
+ double wx = 0.0;
+ int width = getWidth();
+ int height = getHeight();
+
+ // draw background and erase all values
+ g.setColor(isEnabled() ? getBackground() : getBackground().darker());
+ g.fillRect(0, 0, width, height);
+
+ if (points == null) {
+ g.setColor(getForeground());
+ g.drawString("No EnvelopePoints", 10, 30);
+ return;
+ }
+
+ // Determine total duration.
+ if (points.size() > 0) {
+ wx = points.getTotalDuration();
+ // Adjust max X so that we see entire circle of last point.
+ double radiusWX = this.convertGXtoWX(radius) - this.getMinWorldX();
+ double wxFar = wx + radiusWX;
+ if (wxFar > getMaxWorldX()) {
+ if (wx > maximumXRange)
+ wxFar = maximumXRange;
+ setMaxWorldX(wxFar);
+ } else if (wx < (getMaxWorldX() * 0.7)) {
+ double newMax = wx / 0.7001; // make slightly larger to prevent
+ // endless jitter, FIXME - still
+ // needed after repaint()
+ // removed from setMaxWorldX?
+ // System.out.println("newMax = " + newMax );
+ if (newMax < minimumXRange)
+ newMax = minimumXRange;
+ setMaxWorldX(newMax);
+ }
+ }
+ // System.out.println("total X = " + wx );
+
+ drawUnderlay(g);
+
+ drawAllPoints(g);
+
+ /* Show X,Y,TotalX as text. */
+ g.drawString(points.getName() + ", len=" + NumericOutput.doubleToString(wx, 7, 3), 5, 15);
+ if ((draggedPoint != null) && (dragIndex >= 0)) {
+ String s = "i=" + dragIndex + ", dur="
+ + NumericOutput.doubleToString(draggedPoint[0], 7, 3) + ", y = "
+ + NumericOutput.doubleToString(draggedPoint[1], 8, 4);
+ g.drawString(s, 5, 30);
+ }
+ }
+}
diff --git a/src/com/jsyn/swing/EnvelopeEditorPanel.java b/src/com/jsyn/swing/EnvelopeEditorPanel.java
new file mode 100644
index 0000000..dc9f2cd
--- /dev/null
+++ b/src/com/jsyn/swing/EnvelopeEditorPanel.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.swing;
+
+import java.awt.BorderLayout;
+import java.awt.Button;
+import java.awt.Checkbox;
+import java.awt.CheckboxGroup;
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Label;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.ItemEvent;
+import java.awt.event.ItemListener;
+
+import javax.swing.JPanel;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+public class EnvelopeEditorPanel extends JPanel {
+ EnvelopeEditorBox editor;
+ Checkbox pointsBox;
+ Checkbox sustainBox;
+ Checkbox releaseBox;
+ Checkbox autoBox;
+ Button onButton;
+ Button offButton;
+ Button clearButton;
+ Button yUpButton;
+ Button yDownButton;
+ DoubleBoundedTextField zoomField;
+
+ public EnvelopeEditorPanel(EnvelopePoints points, int maxFrames) {
+ setSize(600, 300);
+
+ setLayout(new BorderLayout());
+ editor = new EnvelopeEditorBox();
+ editor.setMaxPoints(maxFrames);
+ editor.setBackground(Color.cyan);
+ editor.setPoints(points);
+ editor.setMinimumSize(new Dimension(500, 300));
+
+ add(editor, "Center");
+
+ JPanel buttonPanel = new JPanel();
+ add(buttonPanel, "South");
+
+ CheckboxGroup cbg = new CheckboxGroup();
+ pointsBox = new Checkbox("points", cbg, true);
+ pointsBox.addItemListener(new ItemListener() {
+ @Override
+ public void itemStateChanged(ItemEvent e) {
+ editor.setMode(EnvelopeEditorBox.EDIT_POINTS);
+ }
+ });
+ buttonPanel.add(pointsBox);
+
+ sustainBox = new Checkbox("onLoop", cbg, false);
+ sustainBox.addItemListener(new ItemListener() {
+ @Override
+ public void itemStateChanged(ItemEvent e) {
+ editor.setMode(EnvelopeEditorBox.SELECT_SUSTAIN);
+ }
+ });
+ buttonPanel.add(sustainBox);
+
+ releaseBox = new Checkbox("offLoop", cbg, false);
+ releaseBox.addItemListener(new ItemListener() {
+ @Override
+ public void itemStateChanged(ItemEvent e) {
+ editor.setMode(EnvelopeEditorBox.SELECT_RELEASE);
+ }
+ });
+ buttonPanel.add(releaseBox);
+
+ autoBox = new Checkbox("AutoStop", false);
+ /*
+ * buttonPanel.add( onButton = new Button( "On" ) ); onButton.addActionListener( module );
+ * buttonPanel.add( offButton = new Button( "Off" ) ); offButton.addActionListener( module
+ * ); buttonPanel.add( clearButton = new Button( "Clear" ) ); clearButton.addActionListener(
+ * module );
+ */
+ buttonPanel.add(yUpButton = new Button("Y*2"));
+ yUpButton.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ scaleEnvelopeValues(2.0);
+ }
+ });
+
+ buttonPanel.add(yDownButton = new Button("Y/2"));
+ yDownButton.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ scaleEnvelopeValues(0.5);
+ }
+ });
+
+ /* Add a TextField for setting the Y scale. */
+ double max = getMaxEnvelopeValue(editor.getPoints());
+ editor.setMaxWorldY(max);
+ buttonPanel.add(new Label("YMax ="));
+ final DoubleBoundedRangeModel model = new DoubleBoundedRangeModel("YMax", 100000, 1.0,
+ 100001.0, 1.0);
+ buttonPanel.add(zoomField = new DoubleBoundedTextField(model, 8));
+ model.addChangeListener(new ChangeListener() {
+ @Override
+ public void stateChanged(ChangeEvent e) {
+ try {
+ double val = model.getDoubleValue();
+ editor.setMaxWorldY(val);
+ editor.repaint();
+ } catch (NumberFormatException exp) {
+ zoomField.setText("ERROR");
+ zoomField.selectAll();
+ }
+ }
+ });
+
+ validate();
+ }
+
+ /**
+ * Multiply all the values in the envelope by scalar.
+ */
+ double getMaxEnvelopeValue(EnvelopePoints points) {
+ double max = 1.0;
+ for (int i = 0; i < points.size(); i++) {
+ double value = points.getValue(i);
+ if (value > max) {
+ max = value;
+ }
+ }
+ return max;
+ }
+
+ /**
+ * Multiply all the values in the envelope by scalar.
+ */
+ void scaleEnvelopeValues(double scalar) {
+ EnvelopePoints points = editor.getPoints();
+ for (int i = 0; i < points.size(); i++) {
+ double[] dar = points.getPoint(i);
+ dar[1] = dar[1] * scalar; // scale value
+ }
+ points.setDirty(true);
+ editor.repaint();
+ }
+}
diff --git a/src/com/jsyn/swing/EnvelopePoints.java b/src/com/jsyn/swing/EnvelopePoints.java
new file mode 100644
index 0000000..ab4ed03
--- /dev/null
+++ b/src/com/jsyn/swing/EnvelopePoints.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright 1997 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.swing;
+
+import java.util.Vector;
+
+import com.jsyn.data.SegmentedEnvelope;
+
+/**
+ * Vector that contains duration,value pairs. Used by EnvelopeEditor
+ *
+ * @author (C) 1997 Phil Burk, SoftSynth.com
+ */
+
+/* ========================================================================== */
+public class EnvelopePoints extends Vector {
+ private String name = "";
+ private double maximumValue = 1.0;
+ private int sustainBegin = -1;
+ private int sustainEnd = -1;
+ private int releaseBegin = -1;
+ private int releaseEnd = -1;
+ private boolean dirty = false;
+
+ /**
+ * Update only if points or loops were modified.
+ */
+ public void updateEnvelopeIfDirty(SegmentedEnvelope envelope) {
+ if (dirty) {
+ updateEnvelope(envelope);
+ }
+ }
+
+ /**
+ * The editor works on a vector of points, not a real envelope. The data must be written to a
+ * real SynthEnvelope in order to use it.
+ */
+ public void updateEnvelope(SegmentedEnvelope envelope) {
+ int numFrames = size();
+ for (int i = 0; i < numFrames; i++) {
+ envelope.write(i, getPoint(i), 0, 1);
+ }
+ envelope.setSustainBegin(getSustainBegin());
+ envelope.setSustainEnd(getSustainEnd());
+ envelope.setReleaseBegin(getReleaseBegin());
+ envelope.setReleaseEnd(getReleaseEnd());
+ envelope.setNumFrames(numFrames);
+ dirty = false;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setMaximumValue(double maximumValue) {
+ this.maximumValue = maximumValue;
+ }
+
+ public double getMaximumValue() {
+ return maximumValue;
+ }
+
+ public void add(double dur, double value) {
+ double dar[] = {
+ dur, value
+ };
+ addElement(dar);
+ dirty = true;
+ }
+
+ /**
+ * Insert point without changing total duration by reducing next points duration.
+ */
+ public void insert(int index, double dur, double y) {
+ double dar[] = {
+ dur, y
+ };
+ if (index < size()) {
+ ((double[]) elementAt(index))[0] -= dur;
+ }
+ insertElementAt(dar, index);
+
+ if (index <= sustainBegin)
+ sustainBegin += 1;
+ if (index <= sustainEnd)
+ sustainEnd += 1;
+ if (index <= releaseBegin)
+ releaseBegin += 1;
+ if (index <= releaseEnd)
+ releaseEnd += 1;
+ dirty = true;
+ }
+
+ /**
+ * Remove indexed point and update sustain and release loops if necessary. Did not name this
+ * "remove()" because of conflicts with new JDK 1.3 method with the same name.
+ */
+ public void removePoint(int index) {
+ super.removeElementAt(index);
+ // move down loop if points below or inside loop removed
+ if (index < sustainBegin)
+ sustainBegin -= 1;
+ if (index <= sustainEnd)
+ sustainEnd -= 1;
+ if (index < releaseBegin)
+ releaseBegin -= 1;
+ if (index <= releaseEnd)
+ releaseEnd -= 1;
+
+ // was entire loop removed?
+ if (sustainBegin > sustainEnd) {
+ sustainBegin = -1;
+ sustainEnd = -1;
+ }
+ // was entire loop removed?
+ if (releaseBegin > releaseEnd) {
+ releaseBegin = -1;
+ releaseEnd = -1;
+ }
+ dirty = true;
+ }
+
+ public double getDuration(int index) {
+ return ((double[]) elementAt(index))[0];
+ }
+
+ public double getValue(int index) {
+ return ((double[]) elementAt(index))[1];
+ }
+
+ public double[] getPoint(int index) {
+ return (double[]) elementAt(index);
+ }
+
+ public double getTotalDuration() {
+ double sum = 0.0;
+ for (int i = 0; i < size(); i++) {
+ double dar[] = (double[]) elementAt(i);
+ sum += dar[0];
+ }
+ return sum;
+ }
+
+ /**
+ * Set location of Sustain Loop in units of Frames. Set SustainBegin to -1 if no Sustain Loop.
+ * SustainEnd value is the frame index of the frame just past the end of the loop. The number of
+ * frames included in the loop is (SustainEnd - SustainBegin).
+ */
+ public void setSustainLoop(int startFrame, int endFrame) {
+ this.sustainBegin = startFrame;
+ this.sustainEnd = endFrame;
+ dirty = true;
+ }
+
+ /***
+ * @return Beginning of sustain loop or -1 if no loop.
+ */
+ public int getSustainBegin() {
+ return this.sustainBegin;
+ }
+
+ /***
+ * @return End of sustain loop or -1 if no loop.
+ */
+ public int getSustainEnd() {
+ return this.sustainEnd;
+ }
+
+ /***
+ * @return Size of sustain loop in frames, 0 if no loop.
+ */
+ public int getSustainSize() {
+ return (this.sustainEnd - this.sustainBegin);
+ }
+
+ /**
+ * Set location of Release Loop in units of Frames. Set ReleaseBegin to -1 if no ReleaseLoop.
+ * ReleaseEnd value is the frame index of the frame just past the end of the loop. The number of
+ * frames included in the loop is (ReleaseEnd - ReleaseBegin).
+ */
+ public void setReleaseLoop(int startFrame, int endFrame) {
+ this.releaseBegin = startFrame;
+ this.releaseEnd = endFrame;
+ dirty = true;
+ }
+
+ /***
+ * @return Beginning of release loop or -1 if no loop.
+ */
+ public int getReleaseBegin() {
+ return this.releaseBegin;
+ }
+
+ /***
+ * @return End of release loop or -1 if no loop.
+ */
+ public int getReleaseEnd() {
+ return this.releaseEnd;
+ }
+
+ /***
+ * @return Size of release loop in frames, 0 if no loop.
+ */
+ public int getReleaseSize() {
+ return (this.releaseEnd - this.releaseBegin);
+ }
+
+ public boolean isDirty() {
+ return dirty;
+ }
+
+ public void setDirty(boolean b) {
+ dirty = b;
+ }
+
+}
diff --git a/src/com/jsyn/swing/ExponentialRangeModel.java b/src/com/jsyn/swing/ExponentialRangeModel.java
new file mode 100644
index 0000000..4411947
--- /dev/null
+++ b/src/com/jsyn/swing/ExponentialRangeModel.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2011 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.swing;
+
+/**
+ * Maps integer range info to a double value along an exponential scale.
+ *
+ * <pre>
+ *
+ * x = ival / resolution
+ * f(x) = a*(root&circ;cx) + b
+ * f(0.0) = dmin
+ * f(1.0) = dmax
+ * b = dmin - a
+ * a = (dmax - dmin) / (root&circ;c - 1)
+ *
+ * Inverse function:
+ * x = log( (y-b)/a ) / log(root)
+ *
+ * </pre>
+ *
+ * @author Phil Burk, (C) 2011 Mobileer Inc
+ */
+public class ExponentialRangeModel extends DoubleBoundedRangeModel {
+ private static final long serialVersionUID = -142785624892302160L;
+ double a = 1.0;
+ double b = -1.0;
+ double span = 1.0;
+ double root = 10.0;
+
+ /** Use default root of 10.0 and span of 1.0. */
+ public ExponentialRangeModel(String name, int resolution, double dmin, double dmax, double dval) {
+ this(name, resolution, dmin, dmax, dval, 1.0);
+ }
+
+ /** Set span before setting double value so it is translated correctly. */
+ ExponentialRangeModel(String name, int resolution, double dmin, double dmax, double dval,
+ double span) {
+ super(name, resolution, dmin, dmax, dval);
+ setRoot(10.0);
+ setSpan(span);
+ /* Set again after coefficients setup. */
+ setDoubleValue(dval);
+ }
+
+ private void updateCoefficients() {
+ a = (getDoubleMaximum() - getDoubleMinimum()) / (Math.pow(root, span) - 1.0);
+ b = getDoubleMinimum() - a;
+ }
+
+ private void setRoot(double w) {
+ root = w;
+ updateCoefficients();
+ }
+
+ public double getRoot() {
+ return root;
+ }
+
+ public void setSpan(double c) {
+ this.span = c;
+ updateCoefficients();
+ }
+
+ public double getSpan() {
+ return span;
+ }
+
+ @Override
+ public double sliderToDouble(int sliderValue) {
+ updateCoefficients(); // TODO optimize when we call this
+ double x = (double) sliderValue / getMaximum();
+ double y = (a * Math.pow(root, span * x)) + b;
+ return y;
+ }
+
+ @Override
+ public int doubleToSlider(double dval) {
+ updateCoefficients(); // TODO optimize when we call this
+ double z = (dval - b) / a;
+ double x = Math.log(z) / (span * Math.log(root));
+ return (int) Math.round(x * getMaximum());
+ }
+
+ public void test(int sliderValue) {
+ double dval = sliderToDouble(sliderValue);
+ int ival = doubleToSlider(dval);
+ System.out.println(sliderValue + " => " + dval + " => " + ival);
+ }
+
+}
diff --git a/src/com/jsyn/swing/InstrumentBrowser.java b/src/com/jsyn/swing/InstrumentBrowser.java
new file mode 100644
index 0000000..70328bf
--- /dev/null
+++ b/src/com/jsyn/swing/InstrumentBrowser.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2012 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.swing;
+
+import java.awt.Dimension;
+import java.awt.GridLayout;
+import java.util.ArrayList;
+
+import javax.swing.JList;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.ListSelectionModel;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+
+import com.jsyn.util.InstrumentLibrary;
+import com.jsyn.util.VoiceDescription;
+
+/**
+ * Display a list of VoiceDescriptions and their associated presets. Notify PresetSelectionListeners
+ * when a preset is selected.
+ *
+ * @author Phil Burk (C) 2012 Mobileer Inc
+ */
+@SuppressWarnings("serial")
+public class InstrumentBrowser extends JPanel {
+ private InstrumentLibrary library;
+ private JList instrumentList;
+ private JScrollPane listScroller2;
+ private VoiceDescription voiceDescription;
+ private ArrayList<PresetSelectionListener> listeners = new ArrayList<PresetSelectionListener>();
+
+ public InstrumentBrowser(InstrumentLibrary library) {
+ this.library = library;
+ JPanel horizontalPanel = new JPanel();
+ horizontalPanel.setLayout(new GridLayout(1, 2));
+
+ instrumentList = createList(library.getVoiceDescriptions());
+ instrumentList.addListSelectionListener(new ListSelectionListener() {
+ @Override
+ public void valueChanged(ListSelectionEvent e) {
+ if (e.getValueIsAdjusting() == false) {
+ int n = instrumentList.getSelectedIndex();
+ if (n >= 0) {
+ showPresetList(n);
+ }
+ }
+ }
+ });
+
+ JScrollPane listScroller1 = new JScrollPane(instrumentList);
+ listScroller1.setPreferredSize(new Dimension(250, 120));
+ add(listScroller1);
+
+ showPresetList(0);
+ }
+
+ public void addPresetSelectionListener(PresetSelectionListener listener) {
+ listeners.add(listener);
+ }
+
+ public void removePresetSelectionListener(PresetSelectionListener listener) {
+ listeners.remove(listener);
+ }
+
+ private void firePresetSelectionListeners(VoiceDescription voiceDescription, int presetIndex) {
+ for (PresetSelectionListener listener : listeners) {
+ listener.presetSelected(voiceDescription, presetIndex);
+ }
+ }
+
+ private void showPresetList(int n) {
+ if (listScroller2 != null) {
+ remove(listScroller2);
+ }
+ voiceDescription = library.getVoiceDescriptions()[n];
+ final JList presetList = createList(voiceDescription.getPresetNames());
+ presetList.addListSelectionListener(new ListSelectionListener() {
+ @Override
+ public void valueChanged(ListSelectionEvent e) {
+ if (e.getValueIsAdjusting() == false) {
+ int n = presetList.getSelectedIndex();
+ if (n >= 0) {
+ firePresetSelectionListeners(voiceDescription, n);
+ }
+ }
+ }
+ });
+
+ listScroller2 = new JScrollPane(presetList);
+ listScroller2.setPreferredSize(new Dimension(250, 120));
+ add(listScroller2);
+ presetList.setSelectedIndex(0);
+ validate();
+ }
+
+ private JList createList(Object[] data) {
+ JList list = new JList(data);
+ list.setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);
+ list.setLayoutOrientation(JList.VERTICAL);
+ list.setVisibleRowCount(-1);
+ return list;
+ }
+}
diff --git a/src/com/jsyn/swing/JAppletFrame.java b/src/com/jsyn/swing/JAppletFrame.java
new file mode 100644
index 0000000..53bd65b
--- /dev/null
+++ b/src/com/jsyn/swing/JAppletFrame.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 1997 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.swing;
+
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+
+import javax.swing.JApplet;
+import javax.swing.JFrame;
+
+/**
+ * Frame that allows a program to be run as either an Application or an Applet. Used by JSyn example
+ * programs.
+ *
+ * @author (C) 1997 Phil Burk, SoftSynth.com
+ */
+
+public class JAppletFrame extends JFrame {
+ private static final long serialVersionUID = -6047247494856379114L;
+ JApplet applet;
+
+ public JAppletFrame(String frameTitle, final JApplet pApplet) {
+ super(frameTitle);
+ this.applet = pApplet;
+ getContentPane().add(applet);
+ repaint();
+
+ addWindowListener(new WindowAdapter() {
+ @Override
+ public void windowClosing(WindowEvent e) {
+ applet.stop();
+ applet.destroy();
+ try {
+ System.exit(0);
+ } catch (SecurityException exc) {
+ System.err.println("System.exit(0) not allowed by Java VM.");
+ }
+ }
+
+ @Override
+ public void windowClosed(WindowEvent e) {
+ }
+ });
+ }
+
+ public void test() {
+ applet.init();
+ applet.start();
+ }
+
+}
diff --git a/src/com/jsyn/swing/PortBoundedRangeModel.java b/src/com/jsyn/swing/PortBoundedRangeModel.java
new file mode 100644
index 0000000..a5cf841
--- /dev/null
+++ b/src/com/jsyn/swing/PortBoundedRangeModel.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2011 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.swing;
+
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import com.jsyn.ports.UnitInputPort;
+
+/**
+ * A bounded range model that drives a UnitInputPort. The range of the model is set based on the min
+ * and max of the port.
+ *
+ * @author Phil Burk (C) 2011 Mobileer Inc
+ */
+public class PortBoundedRangeModel extends DoubleBoundedRangeModel {
+ private static final long serialVersionUID = -8011867146560305808L;
+ private UnitInputPort port;
+
+ public PortBoundedRangeModel(UnitInputPort pPort) {
+ super(pPort.getName(), 10000, pPort.getMinimum(), pPort.getMaximum(), pPort.getValue());
+ this.port = pPort;
+ addChangeListener(new ChangeListener() {
+ @Override
+ public void stateChanged(ChangeEvent e) {
+ port.set(getDoubleValue());
+ }
+ });
+ }
+
+}
diff --git a/src/com/jsyn/swing/PortControllerFactory.java b/src/com/jsyn/swing/PortControllerFactory.java
new file mode 100644
index 0000000..7387db4
--- /dev/null
+++ b/src/com/jsyn/swing/PortControllerFactory.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.swing;
+
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import com.jsyn.ports.UnitInputPort;
+
+/**
+ * Factory class for making various controllers for JSyn ports.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class PortControllerFactory {
+ private static final int RESOLUTION = 100000;
+
+ public static DoubleBoundedRangeSlider createPortSlider(final UnitInputPort port) {
+ DoubleBoundedRangeModel rangeModel = new DoubleBoundedRangeModel(port.getName(),
+ RESOLUTION, port.getMinimum(), port.getMaximum(), port.get());
+ rangeModel.addChangeListener(new ChangeListener() {
+ @Override
+ public void stateChanged(ChangeEvent e) {
+ DoubleBoundedRangeModel model = (DoubleBoundedRangeModel) e.getSource();
+ double value = model.getDoubleValue();
+ port.set(value);
+ }
+ });
+ return new DoubleBoundedRangeSlider(rangeModel, 3);
+ }
+
+ public static DoubleBoundedRangeSlider createExponentialPortSlider(final UnitInputPort port) {
+ ExponentialRangeModel rangeModel = new ExponentialRangeModel(port.getName(), RESOLUTION,
+ port.getMinimum(), port.getMaximum(), port.get());
+ rangeModel.addChangeListener(new ChangeListener() {
+ @Override
+ public void stateChanged(ChangeEvent e) {
+ ExponentialRangeModel model = (ExponentialRangeModel) e.getSource();
+ double value = model.getDoubleValue();
+ port.set(value);
+ }
+ });
+ return new DoubleBoundedRangeSlider(rangeModel, 3);
+ }
+
+}
diff --git a/src/com/jsyn/swing/PortModelFactory.java b/src/com/jsyn/swing/PortModelFactory.java
new file mode 100644
index 0000000..8bec76a
--- /dev/null
+++ b/src/com/jsyn/swing/PortModelFactory.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.swing;
+
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import com.jsyn.ports.UnitInputPort;
+
+public class PortModelFactory {
+ private static final int RESOLUTION = 1000000;
+
+ public static DoubleBoundedRangeModel createLinearModel(final UnitInputPort pPort) {
+ final DoubleBoundedRangeModel model = new DoubleBoundedRangeModel(pPort.getName(),
+ RESOLUTION, pPort.getMinimum(), pPort.getMaximum(), pPort.get());
+ model.addChangeListener(new ChangeListener() {
+ @Override
+ public void stateChanged(ChangeEvent e) {
+ pPort.set(model.getDoubleValue());
+ }
+ });
+ return model;
+ }
+
+ public static ExponentialRangeModel createExponentialModel(final UnitInputPort pPort) {
+ final ExponentialRangeModel model = new ExponentialRangeModel(pPort.getName(), RESOLUTION,
+ pPort.getMinimum(), pPort.getMaximum(), pPort.get());
+ model.addChangeListener(new ChangeListener() {
+ @Override
+ public void stateChanged(ChangeEvent e) {
+ pPort.set(model.getDoubleValue());
+ }
+ });
+ return model;
+ }
+
+ public static ExponentialRangeModel createExponentialModel(final int partNum,
+ final UnitInputPort pPort) {
+ final ExponentialRangeModel model = new ExponentialRangeModel(pPort.getName(), RESOLUTION,
+ pPort.getMinimum(), pPort.getMaximum(), pPort.get());
+ model.addChangeListener(new ChangeListener() {
+ @Override
+ public void stateChanged(ChangeEvent e) {
+ pPort.set(partNum, model.getDoubleValue());
+ }
+ });
+ return model;
+ }
+
+}
diff --git a/src/com/jsyn/swing/PresetSelectionListener.java b/src/com/jsyn/swing/PresetSelectionListener.java
new file mode 100644
index 0000000..daf0310
--- /dev/null
+++ b/src/com/jsyn/swing/PresetSelectionListener.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.swing;
+
+import com.jsyn.util.VoiceDescription;
+
+public interface PresetSelectionListener {
+ public void presetSelected(VoiceDescription voiceDescription, int presetIndex);
+}
diff --git a/src/com/jsyn/swing/RotaryController.java b/src/com/jsyn/swing/RotaryController.java
new file mode 100644
index 0000000..87f1d34
--- /dev/null
+++ b/src/com/jsyn/swing/RotaryController.java
@@ -0,0 +1,329 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.swing;
+
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.RenderingHints;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseMotionAdapter;
+
+import javax.swing.BoundedRangeModel;
+import javax.swing.JPanel;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+/**
+ * Rotary controller looks like a knob on a synthesizer. You control this knob by clicking on it and
+ * dragging <b>up</b> or down</b>. If you move the mouse to the <b>left</b> of the knob then you
+ * will have <b>coarse</b> control. If you move the mouse to the <b>right</b> of the knob then you
+ * will have <b>fine</b> control.
+ * <P>
+ *
+ * @author (C) 2010 Phil Burk, Mobileer Inc
+ * @version 16.1
+ */
+public class RotaryController extends JPanel {
+ private static final long serialVersionUID = 6681532871556659546L;
+ private static final double SENSITIVITY = 0.01;
+ private BoundedRangeModel model;
+
+ private double minAngle = 1.4 * Math.PI;
+ private double maxAngle = -0.4 * Math.PI;
+ private double unitIncrement = 0.01;
+ private int lastY;
+ private int startX;
+ private Color knobColor = Color.LIGHT_GRAY;
+ private Color lineColor = Color.RED;
+ private double baseValue;
+
+ public enum Style {
+ LINE, LINEDOT, ARROW, ARC
+ };
+
+ private Style style = Style.ARC;
+
+ public RotaryController(BoundedRangeModel model) {
+ this.model = model;
+ setMinimumSize(new Dimension(50, 50));
+ setPreferredSize(new Dimension(50, 50));
+ addMouseListener(new MouseHandler());
+ addMouseMotionListener(new MouseMotionHandler());
+ model.addChangeListener(new ChangeListener() {
+ @Override
+ public void stateChanged(ChangeEvent e) {
+ repaint();
+ }
+ });
+ }
+
+ public BoundedRangeModel getModel() {
+ return model;
+ }
+
+ private class MouseHandler extends MouseAdapter {
+
+ @Override
+ public void mousePressed(MouseEvent e) {
+ lastY = e.getY();
+ startX = e.getX();
+ }
+
+ @Override
+ public void mouseReleased(MouseEvent e) {
+ if (isEnabled()) {
+ setKnobByXY(e.getX(), e.getY());
+ }
+ }
+ }
+
+ private class MouseMotionHandler extends MouseMotionAdapter {
+ @Override
+ public void mouseDragged(MouseEvent e) {
+ if (isEnabled()) {
+ setKnobByXY(e.getX(), e.getY());
+ }
+ }
+ }
+
+ private int getModelRange() {
+ return (((model.getMaximum() - model.getExtent()) - model.getMinimum()));
+ }
+
+ /**
+ * A fractional value is useful for drawing.
+ *
+ * @return model value as a normalized fraction between 0.0 and 1.0
+ */
+ public double getFractionFromModel() {
+ double value = model.getValue();
+ return convertValueToFraction(value);
+ }
+
+ private double convertValueToFraction(double value) {
+ return (value - model.getMinimum()) / getModelRange();
+ }
+
+ private void setKnobByXY(int x, int y) {
+ // Scale increment by X position.
+ int xdiff = startX - x; // More to left causes bigger increments.
+ double power = xdiff * SENSITIVITY;
+ double perPixel = unitIncrement * Math.pow(2.0, power);
+
+ int ydiff = lastY - y;
+ double fractionalDelta = ydiff * perPixel;
+ // Only update the model if we actually change values.
+ // This is needed in case the range is small.
+ int valueDelta = (int) Math.round(fractionalDelta * getModelRange());
+ if (valueDelta != 0) {
+ model.setValue(model.getValue() + valueDelta);
+ lastY = y;
+ }
+ }
+
+ private double fractionToAngle(double fraction) {
+ return (fraction * (maxAngle - minAngle)) + minAngle;
+ }
+
+ private void drawLineIndicator(Graphics g, int x, int y, int radius, double angle,
+ boolean drawDot) {
+ double arrowSize = radius * 0.95;
+ int arrowX = (int) (arrowSize * Math.sin(angle));
+ int arrowY = (int) (arrowSize * Math.cos(angle));
+ g.setColor(lineColor);
+ g.drawLine(x, y, x + arrowX, y - arrowY);
+ if (drawDot) {
+ // draw little dot at end
+ double dotScale = 0.1;
+ int dotRadius = (int) (dotScale * arrowSize);
+ if (dotRadius > 1) {
+ int dotX = x + (int) ((0.99 - dotScale) * arrowX) - dotRadius;
+ int dotY = y - (int) ((0.99 - dotScale) * arrowY) - dotRadius;
+ g.fillOval(dotX, dotY, dotRadius * 2, dotRadius * 2);
+ }
+ }
+ }
+
+ private void drawArrowIndicator(Graphics g, int x0, int y0, int radius, double angle) {
+ int arrowSize = (int) (radius * 0.95);
+ int arrowWidth = (int) (radius * 0.2);
+ int xp[] = {
+ 0, arrowWidth, 0, -arrowWidth
+ };
+ int yp[] = {
+ arrowSize, -arrowSize / 2, 0, -arrowSize / 2
+ };
+ double sa = Math.sin(angle);
+ double ca = Math.cos(angle);
+ for (int i = 0; i < xp.length; i++) {
+ int x = xp[i];
+ int y = yp[i];
+ xp[i] = x0 - (int) ((x * ca) - (y * sa));
+ yp[i] = y0 - (int) ((x * sa) + (y * ca));
+ }
+ g.fillPolygon(xp, yp, xp.length);
+ }
+
+ private void drawArcIndicator(Graphics g, int x, int y, int radius, double angle) {
+ final double DEGREES_PER_RADIAN = 180.0 / Math.PI;
+ final int minAngleDegrees = (int) (minAngle * DEGREES_PER_RADIAN);
+ final int maxAngleDegrees = (int) (maxAngle * DEGREES_PER_RADIAN);
+
+ int zeroAngleDegrees = (int) (fractionToAngle(baseValue) * DEGREES_PER_RADIAN);
+
+ double arrowSize = radius * 0.95;
+ int arcX = x - radius;
+ int arcY = y - radius;
+ int arcAngle = (int) (angle * DEGREES_PER_RADIAN);
+ int arrowX = (int) (arrowSize * Math.cos(angle));
+ int arrowY = (int) (arrowSize * Math.sin(angle));
+
+ g.setColor(knobColor.darker().darker());
+ g.fillArc(arcX, arcY, 2 * radius, 2 * radius, minAngleDegrees, maxAngleDegrees
+ - minAngleDegrees);
+ g.setColor(Color.ORANGE);
+ g.fillArc(arcX, arcY, 2 * radius, 2 * radius, zeroAngleDegrees, arcAngle - zeroAngleDegrees);
+
+ // fill in middle
+ int arcWidth = radius / 4;
+ int diameter = ((radius - arcWidth) * 2);
+ g.setColor(knobColor);
+ g.fillOval(arcWidth + x - radius, arcWidth + y - radius, diameter, diameter);
+
+ g.setColor(lineColor);
+ g.drawLine(x, y, x + arrowX, y - arrowY);
+
+ }
+
+ /**
+ * Override this method if you want to draw your own line or dot on the knob.
+ */
+ public void drawIndicator(Graphics g, int x, int y, int radius, double angle) {
+ g.setColor(isEnabled() ? lineColor : lineColor.darker());
+ switch (style) {
+ case LINE:
+ drawLineIndicator(g, x, y, radius, angle, false);
+ break;
+ case LINEDOT:
+ drawLineIndicator(g, x, y, radius, angle, true);
+ break;
+ case ARROW:
+ drawArrowIndicator(g, x, y, radius, angle);
+ break;
+ case ARC:
+ drawArcIndicator(g, x, y, radius, angle);
+ break;
+ }
+ }
+
+ /**
+ * Override this method if you want to draw your own knob.
+ *
+ * @param g graphics context
+ * @param x position of center of knob
+ * @param y position of center of knob
+ * @param radius of knob in pixels
+ * @param angle in radians. Zero is straight up.
+ */
+ public void drawKnob(Graphics g, int x, int y, int radius, double angle) {
+ Graphics2D g2 = (Graphics2D) g;
+ g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+
+ int diameter = radius * 2;
+ // Draw shaded side.
+ g.setColor(knobColor.darker());
+ g.fillOval(x - radius + 2, y - radius + 2, diameter, diameter);
+ g.setColor(knobColor);
+ g.fillOval(x - radius, y - radius, diameter, diameter);
+
+ // Draw line or other indicator of knob position.
+ drawIndicator(g, x, y, radius, angle);
+ }
+
+ // Draw the round knob based on the current size and model value.
+ // This used to have a bug where the scope would draw in this components background.
+ // Then I changed it from overriding paint() to overriding paintComponent() and it worked.
+ @Override
+ public void paintComponent(Graphics g) {
+ super.paintComponent(g);
+
+ int width = getWidth();
+ int height = getHeight();
+ int x = width / 2;
+ int y = height / 2;
+
+ // Calculate radius from size of component.
+ int diameter = (width < height) ? width : height;
+ diameter -= 4;
+ int radius = diameter / 2;
+
+ double angle = fractionToAngle(getFractionFromModel());
+ drawKnob(g, x, y, radius, angle);
+ }
+
+ public Color getKnobColor() {
+ return knobColor;
+ }
+
+ /**
+ * @param knobColor color of body of knob
+ */
+ public void setKnobColor(Color knobColor) {
+ this.knobColor = knobColor;
+ }
+
+ public Color getLineColor() {
+ return lineColor;
+ }
+
+ /**
+ * @param lineColor color of indicator on knob like a line or arrow
+ */
+ public void setLineColor(Color lineColor) {
+ this.lineColor = lineColor;
+ }
+
+ public void setStyle(Style style) {
+ this.style = style;
+ }
+
+ public Style getStyle() {
+ return style;
+ }
+
+ public double getBaseValue() {
+ return baseValue;
+ }
+
+ /*
+ * Specify where the orange arc originates. For example a pan knob with a centered arc would
+ * have a baseValue of 0.5.
+ * @param baseValue a fraction between 0.0 and 1.0.
+ */
+ public void setBaseValue(double baseValue) {
+ if (baseValue < 0.0) {
+ baseValue = 0.0;
+ } else if (baseValue > 1.0) {
+ baseValue = 1.0;
+ }
+ this.baseValue = baseValue;
+ }
+
+}
diff --git a/src/com/jsyn/swing/RotaryTextController.java b/src/com/jsyn/swing/RotaryTextController.java
new file mode 100644
index 0000000..81d6614
--- /dev/null
+++ b/src/com/jsyn/swing/RotaryTextController.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.swing;
+
+import java.awt.BorderLayout;
+
+import javax.swing.BorderFactory;
+import javax.swing.JPanel;
+
+/**
+ * Combine a RotaryController and a DoubleBoundedTextField into a convenient package.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class RotaryTextController extends JPanel {
+ private static final long serialVersionUID = -2931828326251895375L;
+ private RotaryController rotary;
+ private DoubleBoundedTextField textField;
+
+ public RotaryTextController(DoubleBoundedRangeModel pModel, int numDigits) {
+ rotary = new RotaryController(pModel);
+ textField = new DoubleBoundedTextField(pModel, numDigits);
+ setLayout(new BorderLayout());
+ add(rotary, BorderLayout.CENTER);
+ add(textField, BorderLayout.SOUTH);
+ }
+
+ /** Display the title in a border. */
+ public void setTitle(String label) {
+ setBorder(BorderFactory.createTitledBorder(label));
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ super.setEnabled(enabled);
+ rotary.setEnabled(enabled);
+ textField.setEnabled(enabled);
+ }
+}
diff --git a/src/com/jsyn/swing/SoundTweaker.java b/src/com/jsyn/swing/SoundTweaker.java
new file mode 100644
index 0000000..d41946d
--- /dev/null
+++ b/src/com/jsyn/swing/SoundTweaker.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.swing;
+
+import java.awt.Component;
+import java.awt.GridLayout;
+import java.util.ArrayList;
+import java.util.logging.Logger;
+
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+
+import com.jsyn.Synthesizer;
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.ports.UnitPort;
+import com.jsyn.unitgen.UnitGenerator;
+import com.jsyn.unitgen.UnitSource;
+import com.jsyn.unitgen.UnitVoice;
+import com.jsyn.util.Instrument;
+import com.softsynth.math.AudioMath;
+
+@SuppressWarnings("serial")
+public class SoundTweaker extends JPanel {
+ private UnitSource source;
+ private ASCIIMusicKeyboard keyboard;
+ private Synthesizer synth;
+
+ static Logger logger = Logger.getLogger(SoundTweaker.class.getName());
+
+ public SoundTweaker(Synthesizer synth, String title, UnitSource source) {
+ this.synth = synth;
+ this.source = source;
+
+ setLayout(new GridLayout(0, 1));
+
+ UnitGenerator ugen = source.getUnitGenerator();
+ ArrayList<Component> sliders = new ArrayList<Component>();
+
+ add(new JLabel(title));
+ // Arrange the faders in a stack.
+
+ if (source instanceof Instrument) {
+ add(keyboard = createPolyphonicKeyboard());
+ } else if (source instanceof UnitVoice) {
+ add(keyboard = createMonophonicKeyboard());
+ }
+
+ // Iterate through the ports.
+ for (UnitPort port : ugen.getPorts()) {
+ if (port instanceof UnitInputPort) {
+ UnitInputPort inputPort = (UnitInputPort) port;
+ Component slider;
+ // Use an exponential slider if it seems appropriate.
+ if ((inputPort.getMinimum() > 0.0)
+ && ((inputPort.getMaximum() / inputPort.getMinimum()) > 4.0)) {
+ slider = PortControllerFactory.createExponentialPortSlider(inputPort);
+ } else {
+ slider = PortControllerFactory.createPortSlider(inputPort);
+
+ }
+ add(slider);
+ sliders.add(slider);
+ }
+ }
+
+ if (keyboard != null) {
+ for (Component slider : sliders) {
+ slider.addKeyListener(keyboard.getKeyListener());
+ }
+ }
+ validate();
+ }
+
+ @SuppressWarnings("serial")
+ private ASCIIMusicKeyboard createPolyphonicKeyboard() {
+ ASCIIMusicKeyboard keyboard = new ASCIIMusicKeyboard() {
+ @Override
+ public void keyOff(int pitch) {
+ logger.info("-------------- keyOff " + pitch);
+ ((Instrument) source).noteOff(pitch, synth.createTimeStamp());
+ }
+
+ @Override
+ public void keyOn(int pitch) {
+ logger.info("-------------- keyOn " + pitch);
+ double freq = AudioMath.pitchToFrequency(pitch);
+ ((Instrument) source).noteOn(pitch, freq, 0.5, synth.createTimeStamp());
+ }
+ };
+ return keyboard;
+ }
+
+ @SuppressWarnings("serial")
+ private ASCIIMusicKeyboard createMonophonicKeyboard() {
+ ASCIIMusicKeyboard keyboard = new ASCIIMusicKeyboard() {
+ @Override
+ public void keyOff(int pitch) {
+ ((UnitVoice) source).noteOff(synth.createTimeStamp());
+ }
+
+ @Override
+ public void keyOn(int pitch) {
+ double freq = AudioMath.pitchToFrequency(pitch);
+ ((UnitVoice) source).noteOn(freq, 0.5, synth.createTimeStamp());
+ }
+ };
+ return keyboard;
+ }
+
+}
diff --git a/src/com/jsyn/swing/XYController.java b/src/com/jsyn/swing/XYController.java
new file mode 100644
index 0000000..49cb9fd
--- /dev/null
+++ b/src/com/jsyn/swing/XYController.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 1997 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.swing;
+
+import javax.swing.JPanel;
+
+/**
+ * Root class for 2 dimensional X,Y controller for wave editors, Theremins, etc. Maps pixel
+ * coordinates into "world" coordinates.
+ *
+ * @author (C) 1997 Phil Burk, SoftSynth.com
+ */
+
+public class XYController extends JPanel {
+ double minWorldX = 0.0;
+ double maxWorldX = 1.0;
+ double minWorldY = 0.0;
+ double maxWorldY = 1.0;
+
+ public XYController() {
+ }
+
+ public XYController(double minWX, double minWY, double maxWX, double maxWY) {
+ setMinWorldX(minWX);
+ setMaxWorldX(maxWX);
+ setMinWorldY(minWY);
+ setMaxWorldY(maxWY);
+ }
+
+ /**
+ * Set minimum World coordinate value for the horizontal X dimension. The minimum value
+ * corresponds to the left of the component.
+ */
+ public void setMinWorldX(double minWX) {
+ minWorldX = minWX;
+ }
+
+ public double getMinWorldX() {
+ return minWorldX;
+ }
+
+ /**
+ * Set maximum World coordinate value for the horizontal X dimension. The minimum value
+ * corresponds to the right of the component.
+ */
+ public void setMaxWorldX(double maxWX) {
+ maxWorldX = maxWX;
+ }
+
+ public double getMaxWorldX() {
+ return maxWorldX;
+ }
+
+ /**
+ * Set minimum World coordinate value for the vertical Y dimension. The minimum value
+ * corresponds to the bottom of the component.
+ */
+ public void setMinWorldY(double minWY) {
+ minWorldY = minWY;
+ }
+
+ public double getMinWorldY() {
+ return minWorldY;
+ }
+
+ /**
+ * Set maximum World coordinate value for the vertical Y dimension. The maximum value
+ * corresponds to the top of the component.
+ */
+ public void setMaxWorldY(double maxWY) {
+ maxWorldY = maxWY;
+ }
+
+ public double getMaxWorldY() {
+ return maxWorldY;
+ }
+
+ /** Convert from graphics coordinates (pixels) to world coordinates. */
+ public double convertGXtoWX(int gx) {
+ int width = bounds().width;
+ return minWorldX + ((maxWorldX - minWorldX) * gx) / width;
+ }
+
+ public double convertGYtoWY(int gy) {
+ int height = bounds().height;
+ return minWorldY + ((maxWorldY - minWorldY) * (height - gy)) / height;
+ }
+
+ /** Convert from world coordinates to graphics coordinates (pixels). */
+ public int convertWXtoGX(double wx) {
+ int width = bounds().width;
+ return (int) (((wx - minWorldX) * width) / (maxWorldX - minWorldX));
+ }
+
+ public int convertWYtoGY(double wy) {
+ int height = bounds().height;
+ return height - (int) (((wy - minWorldY) * height) / (maxWorldY - minWorldY));
+ }
+
+ /** Clip wx to the min & max World X values. */
+ public double clipWorldX(double wx) {
+ if (wx < minWorldX)
+ wx = minWorldX;
+ else if (wx > maxWorldX)
+ wx = maxWorldX;
+ return wx;
+ }
+
+ /** Clip wy to the min & max World Y values. */
+ public double clipWorldY(double wy) {
+ if (wy < minWorldY)
+ wy = minWorldY;
+ else if (wy > maxWorldY)
+ wy = maxWorldY;
+ return wy;
+ }
+
+}
diff --git a/src/com/jsyn/unitgen/Add.java b/src/com/jsyn/unitgen/Add.java
new file mode 100644
index 0000000..5a2a24c
--- /dev/null
+++ b/src/com/jsyn/unitgen/Add.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+/**
+ * This unit performs a signed addition on its two inputs. <br>
+ *
+ * <pre>
+ * output = inputA + inputB
+ * </pre>
+ *
+ * <br>
+ * Note that signals connected to an InputPort are automatically added together so you may not need
+ * this unit.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ * @version 016
+ * @see MultiplyAdd
+ * @see Subtract
+ */
+public class Add extends UnitBinaryOperator {
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] aValues = inputA.getValues();
+ double[] bValues = inputB.getValues();
+ double[] outputs = output.getValues();
+
+ // System.out.println("adder = " + this);
+ // System.out.println("A = " + aValues[0]);
+ for (int i = start; i < limit; i++) {
+ outputs[i] = aValues[i] + bValues[i];
+ }
+ // System.out.println("add out = " + outputs[0]);
+ }
+}
diff --git a/src/com/jsyn/unitgen/AsymptoticRamp.java b/src/com/jsyn/unitgen/AsymptoticRamp.java
new file mode 100644
index 0000000..8b51294
--- /dev/null
+++ b/src/com/jsyn/unitgen/AsymptoticRamp.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.ports.UnitVariablePort;
+
+/**
+ * Output approaches Input exponentially. This unit provides a slowly changing value that approaches
+ * its Input value exponentially. The equation is:
+ *
+ * <PRE>
+ * Output = Output + Rate * (Input - Output);
+ * </PRE>
+ *
+ * Note that the output may never reach the value of the input. It approaches the input
+ * asymptotically. The Rate is calculated internally based on the value on the halfLife port. Rate
+ * is generally just slightly less than 1.0.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ * @version 016
+ * @see LinearRamp
+ * @see ExponentialRamp
+ * @see ContinuousRamp
+ */
+public class AsymptoticRamp extends UnitFilter {
+ public UnitVariablePort current;
+ public UnitInputPort halfLife;
+ private double previousHalfLife = -1.0;
+ private double decayScalar = 0.99;
+
+ /* Define Unit Ports used by connect() and set(). */
+ public AsymptoticRamp() {
+ addPort(halfLife = new UnitInputPort(1, "HalfLife", 0.1));
+ addPort(current = new UnitVariablePort("Current"));
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] outputs = output.getValues();
+ double[] inputs = input.getValues();
+ double currentHalfLife = halfLife.getValues()[0];
+ double currentValue = current.getValue();
+ double inputValue = currentValue;
+
+ if (currentHalfLife != previousHalfLife) {
+ decayScalar = this.convertHalfLifeToMultiplier(currentHalfLife);
+ previousHalfLife = currentHalfLife;
+ }
+
+ for (int i = start; i < limit; i++) {
+ inputValue = inputs[i];
+ currentValue = currentValue + decayScalar * (inputValue - currentValue);
+ outputs[i] = currentValue;
+ }
+
+ /*
+ * When current gets close to input, set current to input to prevent FP underflow, which can
+ * cause a severe performance degradation in 'C'.
+ */
+ if (Math.abs(inputValue - currentValue) < VERY_SMALL_FLOAT) {
+ currentValue = inputValue;
+ }
+
+ current.setValue(currentValue);
+ }
+}
diff --git a/src/com/jsyn/unitgen/ChannelIn.java b/src/com/jsyn/unitgen/ChannelIn.java
new file mode 100644
index 0000000..e42c06a
--- /dev/null
+++ b/src/com/jsyn/unitgen/ChannelIn.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitOutputPort;
+
+/**
+ * Provides access to one specific channel of the audio input. For ChannelIn to work you must call
+ * the Synthesizer start() method with numInputChannels > 0.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ * @see ChannelOut
+ * @see LineIn
+ */
+public class ChannelIn extends UnitGenerator {
+ public UnitOutputPort output;
+ private int channelIndex;
+
+ public ChannelIn() {
+ this(0);
+ }
+
+ public ChannelIn(int channelIndex) {
+ addPort(output = new UnitOutputPort());
+ setChannelIndex(channelIndex);
+ }
+
+ public int getChannelIndex() {
+ return channelIndex;
+ }
+
+ public void setChannelIndex(int channelIndex) {
+ this.channelIndex = channelIndex;
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] outputs = output.getValues(0);
+ double[] buffer = synthesisEngine.getInputBuffer(channelIndex);
+ for (int i = start; i < limit; i++) {
+ outputs[i] = buffer[i];
+ }
+ }
+
+}
diff --git a/src/com/jsyn/unitgen/ChannelOut.java b/src/com/jsyn/unitgen/ChannelOut.java
new file mode 100644
index 0000000..5eafacc
--- /dev/null
+++ b/src/com/jsyn/unitgen/ChannelOut.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitInputPort;
+
+/**
+ * Provides access to one channel of the audio output.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ * @see ChannelIn
+ */
+public class ChannelOut extends UnitGenerator {
+ public UnitInputPort input;
+ private int channelIndex;
+
+ public ChannelOut() {
+ addPort(input = new UnitInputPort("Input"));
+ }
+
+ public int getChannelIndex() {
+ return channelIndex;
+ }
+
+ public void setChannelIndex(int channelIndex) {
+ this.channelIndex = channelIndex;
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] inputs = input.getValues(0);
+ double[] buffer = synthesisEngine.getOutputBuffer(channelIndex);
+ for (int i = start; i < limit; i++) {
+ buffer[i] += inputs[i];
+ }
+ }
+
+}
diff --git a/src/com/jsyn/unitgen/Circuit.java b/src/com/jsyn/unitgen/Circuit.java
new file mode 100644
index 0000000..a501600
--- /dev/null
+++ b/src/com/jsyn/unitgen/Circuit.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import java.util.ArrayList;
+
+import com.jsyn.engine.SynthesisEngine;
+
+/**
+ * Contains a list of units that are executed together.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ */
+public class Circuit extends UnitGenerator {
+ private ArrayList<UnitGenerator> units = new ArrayList<UnitGenerator>();
+
+ @Override
+ public void generate(int start, int limit) {
+ for (UnitGenerator unit : units) {
+ unit.generate(start, limit);
+ }
+ }
+
+ /**
+ * Call flattenOutputs on subunits. Flatten output ports so we don't output a changing signal
+ * when stopped.
+ */
+ @Override
+ public void flattenOutputs() {
+ for (UnitGenerator unit : units) {
+ unit.flattenOutputs();
+ }
+ }
+
+ /**
+ * Call setEnabled on subunits.
+ */
+ @Override
+ public void setEnabled(boolean enabled) {
+ super.setEnabled(enabled);
+ for (UnitGenerator unit : units) {
+ unit.setEnabled(enabled);
+ }
+ }
+
+ @Override
+ public void setFrameRate(int frameRate) {
+ super.setFrameRate(frameRate);
+ for (UnitGenerator unit : units) {
+ unit.setFrameRate(frameRate);
+ }
+ }
+
+ @Override
+ public void setSynthesisEngine(SynthesisEngine engine) {
+ super.setSynthesisEngine(engine);
+ for (UnitGenerator unit : units) {
+ unit.setSynthesisEngine(engine);
+ }
+ }
+
+ /** Add a unit to the circuit. */
+ public void add(UnitGenerator unit) {
+ units.add(unit);
+ unit.setCircuit(this);
+ // Propagate circuit properties down into subunits.
+ unit.setEnabled(isEnabled());
+ }
+
+ public void usePreset(int presetIndex) {
+ }
+}
diff --git a/src/com/jsyn/unitgen/Compare.java b/src/com/jsyn/unitgen/Compare.java
new file mode 100644
index 0000000..4d55740
--- /dev/null
+++ b/src/com/jsyn/unitgen/Compare.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+/**
+ *
+ Output 1.0 if inputA > inputB. Otherwise output 0.0.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ * @version 016
+ * @see Maximum
+ */
+public class Compare extends UnitBinaryOperator {
+ @Override
+ public void generate(int start, int limit) {
+ double[] aValues = inputA.getValues();
+ double[] bValues = inputB.getValues();
+ double[] outputs = output.getValues();
+
+ for (int i = start; i < limit; i++) {
+ outputs[i] = (aValues[i] > bValues[i]) ? 1.0 : 0.0;
+ }
+ }
+}
diff --git a/src/com/jsyn/unitgen/ContinuousRamp.java b/src/com/jsyn/unitgen/ContinuousRamp.java
new file mode 100644
index 0000000..dd90445
--- /dev/null
+++ b/src/com/jsyn/unitgen/ContinuousRamp.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.ports.UnitVariablePort;
+
+/**
+ * A ramp whose function over time is continuous in value and in slope. Also called an "S curve".
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ * @version 016
+ * @see LinearRamp
+ * @see ExponentialRamp
+ * @see AsymptoticRamp
+ */
+public class ContinuousRamp extends UnitFilter {
+ public UnitVariablePort current;
+ /**
+ * Time it takes to get from current value to input value when input is changed. Default value
+ * is 1.0 seconds.
+ */
+ public UnitInputPort time;
+ private double previousInput = Double.MIN_VALUE;
+ // Coefficients for cubic polynomial.
+ private double a;
+ private double b;
+ private double d;
+ private int framesLeft;
+
+ /* Define Unit Ports used by connect() and set(). */
+ public ContinuousRamp() {
+ addPort(time = new UnitInputPort(1, "Time", 1.0));
+ addPort(current = new UnitVariablePort("Current"));
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] outputs = output.getValues();
+ double[] inputs = input.getValues();
+ double currentTime = time.getValues()[0];
+ double currentValue = current.getValue();
+ double inputValue = currentValue;
+
+ for (int i = start; i < limit; i++) {
+ inputValue = inputs[i];
+ double x;
+ if (inputValue != previousInput) {
+ x = framesLeft;
+ // Calculate coefficients.
+ double currentSlope = x * ((3 * a * x) + (2 * b));
+
+ framesLeft = (int) (getSynthesisEngine().getFrameRate() * currentTime);
+ if (framesLeft < 1) {
+ framesLeft = 1;
+ }
+ x = framesLeft;
+ // Calculate coefficients.
+ d = inputValue;
+ double xsq = x * x;
+ b = ((3 * currentValue) - (currentSlope * x) - (3 * d)) / xsq;
+ a = (currentSlope - (2 * b * x)) / (3 * xsq);
+ previousInput = inputValue;
+ }
+
+ if (framesLeft > 0) {
+ x = --framesLeft;
+ // Cubic polynomial. c==0
+ currentValue = (x * (x * ((x * a) + b))) + d;
+ }
+
+ outputs[i] = currentValue;
+ }
+
+ current.setValue(currentValue);
+ }
+}
diff --git a/src/com/jsyn/unitgen/CrossFade.java b/src/com/jsyn/unitgen/CrossFade.java
new file mode 100644
index 0000000..1c9143a
--- /dev/null
+++ b/src/com/jsyn/unitgen/CrossFade.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.ports.UnitOutputPort;
+
+/**
+ * Linear CrossFade between parts of the input.
+ * <P>
+ * Mix input[0] and input[1] based on the value of "fade". When fade is -1, output is all input[0].
+ * When fade is 0, output is half input[0] and half input[1]. When fade is +1, output is all
+ * input[1].
+ * <P>
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ * @version 016
+ * @see Pan
+ */
+public class CrossFade extends UnitGenerator {
+ public UnitInputPort input;
+ public UnitInputPort fade;
+ public UnitOutputPort output;
+
+ /* Define Unit Ports used by connect() and set(). */
+ public CrossFade() {
+ addPort(input = new UnitInputPort(2, "Input"));
+ addPort(fade = new UnitInputPort("Fade"));
+ addPort(output = new UnitOutputPort());
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] input0s = input.getValues(0);
+ double[] input1s = input.getValues(1);
+ double[] fades = fade.getValues();
+ double[] outputs = output.getValues();
+ for (int i = start; i < limit; i++) {
+ // Scale and offset to 0.0 to 1.0 range.
+ double gain = (fades[i] * 0.5) + 0.5;
+ outputs[i] = (input0s[i] * (1.0 - gain)) + (input1s[i] * gain);
+ }
+ }
+
+}
diff --git a/src/com/jsyn/unitgen/Delay.java b/src/com/jsyn/unitgen/Delay.java
new file mode 100644
index 0000000..aa450a9
--- /dev/null
+++ b/src/com/jsyn/unitgen/Delay.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+/**
+ * Simple non-interpolating delay. The delay line must be allocated by calling allocate(n).
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ * @version 016
+ * @see InterpolatingDelay
+ */
+
+public class Delay extends UnitFilter {
+ private float[] buffer;
+ private int cursor;
+ private int numSamples;
+
+ /**
+ * Allocate an internal array for the delay line.
+ *
+ * @param numSamples
+ */
+ public void allocate(int numSamples) {
+ this.numSamples = numSamples;
+ buffer = new float[numSamples];
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] inputs = input.getValues();
+ double[] outputs = output.getValues();
+
+ for (int i = start; i < limit; i++) {
+ outputs[i] = buffer[cursor];
+ buffer[cursor] = (float) inputs[i];
+ cursor += 1;
+ if (cursor >= numSamples) {
+ cursor = 0;
+ }
+ }
+ }
+
+}
diff --git a/src/com/jsyn/unitgen/Divide.java b/src/com/jsyn/unitgen/Divide.java
new file mode 100644
index 0000000..cddcd7c
--- /dev/null
+++ b/src/com/jsyn/unitgen/Divide.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+/**
+ * This unit divides its two inputs. <br>
+ *
+ * <pre>
+ * output = inputA / inputB
+ * </pre>
+ *
+ * <br>
+ * Note that this unit is protected from dividing by zero. But you can still get some very big
+ * outputs.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ * @version 016
+ * @see Multiply
+ * @see Subtract
+ */
+public class Divide extends UnitBinaryOperator {
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] aValues = inputA.getValues();
+ double[] bValues = inputB.getValues();
+ double[] outputs = output.getValues();
+ for (int i = start; i < limit; i++) {
+ /* Prevent divide by zero crash. */
+ double b = bValues[i];
+ if (b == 0.0) {
+ b = 0.0000001;
+ }
+
+ outputs[i] = aValues[i] / b;
+ }
+ }
+
+}
diff --git a/src/com/jsyn/unitgen/DualInTwoOut.java b/src/com/jsyn/unitgen/DualInTwoOut.java
new file mode 100644
index 0000000..ec7dff5
--- /dev/null
+++ b/src/com/jsyn/unitgen/DualInTwoOut.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.ports.UnitOutputPort;
+
+/**
+ * This unit splits a dual (stereo) input to two discrete outputs. <br>
+ *
+ * <pre>
+ * outputA = input[0];
+ * outputB = input[1];
+ * </pre>
+ *
+ * <br>
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ * @version 016
+ */
+
+public class DualInTwoOut extends UnitGenerator {
+ public UnitInputPort input;
+ public UnitOutputPort outputA;
+ public UnitOutputPort outputB;
+
+ public DualInTwoOut() {
+ addPort(input = new UnitInputPort(2, "Input"));
+ addPort(outputA = new UnitOutputPort("OutputA"));
+ addPort(outputB = new UnitOutputPort("OutputB"));
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] input0s = input.getValues(0);
+ double[] input1s = input.getValues(1);
+ double[] outputAs = outputA.getValues();
+ double[] outputBs = outputB.getValues();
+
+ for (int i = start; i < limit; i++) {
+ outputAs[i] = input0s[i];
+ outputBs[i] = input1s[i];
+ }
+ }
+}
diff --git a/src/com/jsyn/unitgen/EdgeDetector.java b/src/com/jsyn/unitgen/EdgeDetector.java
new file mode 100644
index 0000000..a5bff9d
--- /dev/null
+++ b/src/com/jsyn/unitgen/EdgeDetector.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 1997 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+/**
+ * Output 1.0 if the input crosses from zero while rising. Otherwise output zero. The output is a
+ * single sample wide impulse. This can be used with a Latch to implement a "sample and hold"
+ * circuit.
+ *
+ * @author (C) 1997-2010 Phil Burk, Mobileer Inc
+ * @see Latch
+ */
+public class EdgeDetector extends UnitFilter {
+ private double previous = 0.0;
+
+ /* Define Unit Ports used by connect() and set(). */
+ public EdgeDetector() {
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] inputs = input.getValues();
+ double[] outputs = output.getValues();
+
+ for (int i = start; i < limit; i++) {
+ double value = 0.0;
+ double in = inputs[i];
+ if ((previous <= 0.0) && (in > 0.0)) {
+ value = 1.0;
+ }
+ outputs[i] = value;
+ previous = in;
+ }
+ }
+}
diff --git a/src/com/jsyn/unitgen/EnvelopeAttackDecay.java b/src/com/jsyn/unitgen/EnvelopeAttackDecay.java
new file mode 100644
index 0000000..db3ecaa
--- /dev/null
+++ b/src/com/jsyn/unitgen/EnvelopeAttackDecay.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright 2011 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.engine.SynthesisEngine;
+import com.jsyn.ports.UnitInputPort;
+
+/**
+ * Two stage Attack/Decay envelope that is triggered by an input level greater than THRESHOLD. This
+ * does not sustain.
+ *
+ * @author Phil Burk (C) 2011 Mobileer Inc
+ */
+public class EnvelopeAttackDecay extends UnitGate {
+ public static final double THRESHOLD = 0.01;
+ private static final double MIN_DURATION = (1.0 / 100000.0);
+
+ /**
+ * Time in seconds for the rising stage of the envelope to go from 0.0 to 1.0. The attack is a
+ * linear ramp.
+ */
+ public UnitInputPort attack;
+ /**
+ * Time in seconds for the falling stage to go from 0 dB to -90 dB.
+ */
+ public UnitInputPort decay;
+
+ public UnitInputPort amplitude;
+
+ private enum State {
+ IDLE, ATTACKING, DECAYING
+ }
+
+ private State state = State.IDLE;
+ private double scaler = 1.0;
+ private double level;
+ private double increment;
+
+ public EnvelopeAttackDecay() {
+ super();
+ addPort(attack = new UnitInputPort("Attack"));
+ attack.setup(0.001, 0.05, 8.0);
+ addPort(decay = new UnitInputPort("Decay"));
+ decay.setup(0.001, 0.2, 8.0);
+ addPort(amplitude = new UnitInputPort("Amplitude", 1.0));
+ startIdle();
+ }
+
+ public void export(Circuit circuit, String prefix) {
+ circuit.addPort(attack, prefix + attack.getName());
+ circuit.addPort(decay, prefix + decay.getName());
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] amplitudes = amplitude.getValues();
+ double[] outputs = output.getValues();
+
+ for (int i = start; i < limit;) {
+ boolean triggered = input.checkGate(i);
+ switch (state) {
+ case IDLE:
+ for (; i < limit; i++) {
+ outputs[i] = level;
+ if (triggered) {
+ startAttack(i);
+ break;
+ }
+ }
+ break;
+ case ATTACKING:
+ for (; i < limit; i++) {
+ // Increment first so we can render fast attacks.
+ level += increment;
+ if (level >= 1.0) {
+ level = 1.0;
+ outputs[i] = level * amplitudes[i];
+ startDecay(i);
+ break;
+ }
+ outputs[i] = level * amplitudes[i];
+ }
+ break;
+ case DECAYING:
+ for (; i < limit; i++) {
+ outputs[i] = level * amplitudes[i];
+ level *= scaler;
+ if (triggered) {
+ startAttack(i);
+ break;
+ } else if (level < SynthesisEngine.DB90) {
+ input.checkAutoDisable();
+ startIdle();
+ break;
+ }
+ }
+ break;
+ }
+ }
+ }
+
+ private void startIdle() {
+ state = State.IDLE;
+ level = 0.0;
+ }
+
+ private void startAttack(int i) {
+ double[] attacks = attack.getValues();
+ double duration = attacks[i];
+ if (duration < MIN_DURATION) {
+ level = 1.0;
+ startDecay(i);
+ } else {
+ // assume going from 0.0 to 1.0 even if retriggering
+ increment = getFramePeriod() / duration;
+ state = State.ATTACKING;
+ }
+ }
+
+ private void startDecay(int i) {
+ double[] decays = decay.getValues();
+ double duration = decays[i];
+ if (duration < MIN_DURATION) {
+ startIdle();
+ } else {
+ scaler = getSynthesisEngine().convertTimeToExponentialScaler(duration);
+ state = State.DECAYING;
+ }
+ }
+
+}
diff --git a/src/com/jsyn/unitgen/EnvelopeDAHDSR.java b/src/com/jsyn/unitgen/EnvelopeDAHDSR.java
new file mode 100644
index 0000000..6acd763
--- /dev/null
+++ b/src/com/jsyn/unitgen/EnvelopeDAHDSR.java
@@ -0,0 +1,294 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.data.SegmentedEnvelope;
+import com.jsyn.engine.SynthesisEngine;
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.ports.UnitOutputPort;
+
+/**
+ * Six stage envelope similar to an ADSR. DAHDSR is like an ADSR but with an additional Delay stage
+ * before the attack, and a Hold stage after the Attack. If Delay and Hold are both set to zero then
+ * it will act like an ADSR. The envelope is triggered when the input goes above THRESHOLD. The
+ * envelope is released when the input goes below THRESHOLD. The THRESHOLD is currently 0.01 but may
+ * change so it would be best to use an input signal that went from 0 to 1. Mathematically an
+ * exponential Release will never reach 0.0. But when it reaches -96 dB the DAHDSR just sets its
+ * output to 0.0 and stops. There is an example program in the ZIP archive called HearDAHDSR. It
+ * drives a DAHDSR with a square wave.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ * @see SegmentedEnvelope
+ */
+public class EnvelopeDAHDSR extends UnitGate implements UnitSource {
+ private static final double MIN_DURATION = (1.0 / 100000.0);
+
+ /**
+ * Time in seconds for first stage of the envelope, before the attack. Typically zero.
+ */
+ public UnitInputPort delay;
+ /**
+ * Time in seconds for the rising stage of the envelope to go from 0.0 to 1.0. The attack is a
+ * linear ramp.
+ */
+ public UnitInputPort attack;
+ /** Time in seconds for the plateau between the attack and decay stages. */
+ public UnitInputPort hold;
+ /**
+ * Time in seconds for the falling stage to go from 0 dB to -90 dB. The decay stage will stop at
+ * the sustain level. But we calculate the time to fall to -90 dB so that the decay
+ * <em>rate</em> will be unaffected by the sustain level.
+ */
+ public UnitInputPort decay;
+ /**
+ * Level for the sustain stage. The envelope will hold here until the input goes to zero or
+ * less. This should be set between 0.0 and 1.0.
+ */
+ public UnitInputPort sustain;
+ /**
+ * Time in seconds to go from 0 dB to -90 dB. This stage is triggered when the input goes to
+ * zero or less. The release stage will start from the sustain level. But we calculate the time
+ * to fall from full amplitude so that the release <em>rate</em> will be unaffected by the
+ * sustain level.
+ */
+ public UnitInputPort release;
+ public UnitInputPort amplitude;
+
+ enum State {
+ IDLE, DELAYING, ATTACKING, HOLDING, DECAYING, SUSTAINING, RELEASING
+ }
+
+ private State state = State.IDLE;
+ private double countdown;
+ private double scaler = 1.0;
+ private double level;
+ private double increment;
+
+ public EnvelopeDAHDSR() {
+ super();
+ addPort(delay = new UnitInputPort("Delay", 0.0));
+ delay.setup(0.0, 0.0, 2.0);
+ addPort(attack = new UnitInputPort("Attack", 0.1));
+ attack.setup(0.01, 0.1, 8.0);
+ addPort(hold = new UnitInputPort("Hold", 0.0));
+ hold.setup(0.0, 0.0, 2.0);
+ addPort(decay = new UnitInputPort("Decay", 0.2));
+ decay.setup(0.01, 0.2, 8.0);
+ addPort(sustain = new UnitInputPort("Sustain", 0.5));
+ sustain.setup(0.0, 0.5, 1.0);
+ addPort(release = new UnitInputPort("Release", 0.3));
+ release.setup(0.01, 0.3, 8.0);
+ addPort(amplitude = new UnitInputPort("Amplitude", 1.0));
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] sustains = sustain.getValues();
+ double[] amplitudes = amplitude.getValues();
+ double[] outputs = output.getValues();
+
+ for (int i = start; i < limit;) {
+ boolean triggered = input.checkGate(i);
+ switch (state) {
+ case IDLE:
+ for (; i < limit; i++) {
+ outputs[i] = level * amplitudes[i];
+ if (triggered) {
+ startDelay(i);
+ break;
+ }
+ }
+ break;
+
+ case DELAYING:
+ for (; i < limit; i++) {
+ outputs[i] = level * amplitudes[i];
+ if (input.isOff()) {
+ startRelease(i);
+ break;
+ } else {
+ countdown -= 1;
+ if (countdown <= 0) {
+ startAttack(i);
+ break;
+ }
+ }
+ }
+ break;
+
+ case ATTACKING:
+ for (; i < limit; i++) {
+ // Increment first so we can render fast attacks.
+ level += increment;
+ if (level >= 1.0) {
+ level = 1.0;
+ outputs[i] = level * amplitudes[i];
+ startHold(i);
+ break;
+ } else {
+ outputs[i] = level * amplitudes[i];
+ if (input.isOff()) {
+ startRelease(i);
+ break;
+ }
+ }
+ }
+ break;
+
+ case HOLDING:
+ for (; i < limit; i++) {
+ outputs[i] = amplitudes[i]; // level is 1.0
+ countdown -= 1;
+ if (countdown <= 0) {
+ startDecay(i);
+ break;
+ } else if (input.isOff()) {
+ startRelease(i);
+ break;
+ }
+ }
+ break;
+
+ case DECAYING:
+ for (; i < limit; i++) {
+ outputs[i] = level * amplitudes[i];
+ level *= scaler; // exponential decay
+ if (triggered) {
+ startDelay(i);
+ break;
+ } else if (level < sustains[i]) {
+ level = sustains[i];
+ startSustain(i);
+ break;
+ } else if (level < SynthesisEngine.DB96) {
+ input.checkAutoDisable();
+ startIdle();
+ break;
+ } else if (input.isOff()) {
+ startRelease(i);
+ break;
+ }
+ }
+ break;
+
+ case SUSTAINING:
+ for (; i < limit; i++) {
+ level = sustains[i];
+ outputs[i] = level * amplitudes[i];
+ if (triggered) {
+ startDelay(i);
+ break;
+ } else if (input.isOff()) {
+ startRelease(i);
+ break;
+ }
+ }
+ break;
+
+ case RELEASING:
+ for (; i < limit; i++) {
+ outputs[i] = level * amplitudes[i];
+ level *= scaler; // exponential decay
+ if (triggered) {
+ startDelay(i);
+ break;
+ } else if (level < SynthesisEngine.DB96) {
+ input.checkAutoDisable();
+ startIdle();
+ break;
+ }
+ }
+ break;
+ }
+ }
+ }
+
+ private void startIdle() {
+ state = State.IDLE;
+ level = 0.0;
+ }
+
+ private void startDelay(int i) {
+ double[] delays = delay.getValues();
+ if (delays[i] <= 0.0) {
+ startAttack(i);
+ } else {
+ countdown = (int) (delays[i] * getFrameRate());
+ state = State.DELAYING;
+ }
+ }
+
+ private void startAttack(int i) {
+ double[] attacks = attack.getValues();
+ double duration = attacks[i];
+ if (duration < MIN_DURATION) {
+ level = 1.0;
+ startHold(i);
+ } else {
+ increment = getFramePeriod() / duration;
+ state = State.ATTACKING;
+ }
+ }
+
+ private void startHold(int i) {
+ double[] holds = hold.getValues();
+ if (holds[i] <= 0.0) {
+ startDecay(i);
+ } else {
+ countdown = (int) (holds[i] * getFrameRate());
+ state = State.HOLDING;
+ }
+ }
+
+ private void startDecay(int i) {
+ double[] decays = decay.getValues();
+ double duration = decays[i];
+ if (duration < MIN_DURATION) {
+ startSustain(i);
+ } else {
+ scaler = getSynthesisEngine().convertTimeToExponentialScaler(duration);
+ state = State.DECAYING;
+ }
+ }
+
+ private void startSustain(int i) {
+ state = State.SUSTAINING;
+ }
+
+ private void startRelease(int i) {
+ double[] releases = release.getValues();
+ double duration = releases[i];
+ if (duration < MIN_DURATION) {
+ duration = MIN_DURATION;
+ }
+ scaler = getSynthesisEngine().convertTimeToExponentialScaler(duration);
+ state = State.RELEASING;
+ }
+
+ public void export(Circuit circuit, String prefix) {
+ circuit.addPort(attack, prefix + attack.getName());
+ circuit.addPort(decay, prefix + decay.getName());
+ circuit.addPort(sustain, prefix + sustain.getName());
+ circuit.addPort(release, prefix + release.getName());
+ }
+
+ @Override
+ public UnitOutputPort getOutput() {
+ return output;
+ }
+
+}
diff --git a/src/com/jsyn/unitgen/ExponentialRamp.java b/src/com/jsyn/unitgen/ExponentialRamp.java
new file mode 100644
index 0000000..4befbeb
--- /dev/null
+++ b/src/com/jsyn/unitgen/ExponentialRamp.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.ports.UnitVariablePort;
+
+/**
+ * Output approaches Input exponentially and will reach it in the specified time.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ * @version 016
+ * @see LinearRamp
+ * @see AsymptoticRamp
+ * @see ContinuousRamp
+ */
+public class ExponentialRamp extends UnitFilter {
+ public UnitInputPort time;
+ public UnitVariablePort current;
+
+ private double target;
+ private double timeHeld = 0.0;
+ private double scaler = 1.0;
+
+ public ExponentialRamp() {
+ addPort(time = new UnitInputPort("Time"));
+ addPort(current = new UnitVariablePort("Current", 1.0));
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] outputs = output.getValues();
+ double currentInput = input.getValues()[0];
+ double currentTime = time.getValues()[0];
+ double currentValue = current.getValue();
+
+ if (currentTime != timeHeld) {
+ scaler = convertTimeToExponentialScaler(currentTime, currentValue, currentInput);
+ timeHeld = currentTime;
+ }
+
+ // If input has changed, start new segment.
+ // Equality check is OK because we set them exactly equal below.
+ if (currentInput != target) {
+ scaler = convertTimeToExponentialScaler(currentTime, currentValue, currentInput);
+ target = currentInput;
+ }
+
+ if (currentValue < target) {
+ // Going up.
+ for (int i = start; i < limit; i++) {
+ currentValue = currentValue * scaler;
+ if (currentValue > target) {
+ currentValue = target;
+ scaler = 1.0;
+ }
+ outputs[i] = currentValue;
+ }
+ } else if (currentValue > target) {
+ // Going down.
+ for (int i = start; i < limit; i++) {
+ currentValue = currentValue * scaler;
+ if (currentValue < target) {
+ currentValue = target;
+ scaler = 1.0;
+ }
+ outputs[i] = currentValue;
+ }
+
+ } else if (currentValue == target) {
+ for (int i = start; i < limit; i++) {
+ outputs[i] = target;
+ }
+ }
+
+ current.setValue(currentValue);
+ }
+
+ private double convertTimeToExponentialScaler(double duration, double source, double target) {
+ double product = source * target;
+ if (product <= 0.0000001) {
+ throw new IllegalArgumentException(
+ "Exponential ramp crosses zero or gets too close to zero.");
+ }
+ // Calculate scaler so that scaler^frames = target/source
+ double numFrames = duration * getFrameRate();
+ return Math.pow((target / source), (1.0 / numFrames));
+ }
+}
diff --git a/src/com/jsyn/unitgen/FFT.java b/src/com/jsyn/unitgen/FFT.java
new file mode 100644
index 0000000..63fce50
--- /dev/null
+++ b/src/com/jsyn/unitgen/FFT.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2013 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+/**
+ * Periodically transform the complex input signal using an FFT to a complex spectral stream. This
+ * is probably not as useful as the SpectralFFT, which outputs complete spectra.
+ *
+ * @author Phil Burk (C) 2013 Mobileer Inc
+ * @see IFFT
+ * @see SpectralFFT
+ */
+public class FFT extends FFTBase {
+ public FFT() {
+ super();
+ }
+
+ @Override
+ protected int getSign() {
+ return 1; // 1 for FFT
+ }
+}
diff --git a/src/com/jsyn/unitgen/FFTBase.java b/src/com/jsyn/unitgen/FFTBase.java
new file mode 100644
index 0000000..055c04b
--- /dev/null
+++ b/src/com/jsyn/unitgen/FFTBase.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2013 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.data.Spectrum;
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.ports.UnitOutputPort;
+import com.softsynth.math.FourierMath;
+
+/**
+ * Periodically transform the complex input signal using an FFT to a complex spectral stream.
+ *
+ * @author Phil Burk (C) 2013 Mobileer Inc
+ * @version 016
+ * @see IFFT
+ */
+public abstract class FFTBase extends UnitGenerator {
+ public UnitInputPort inputReal;
+ public UnitInputPort inputImaginary;
+ public UnitOutputPort outputReal;
+ public UnitOutputPort outputImaginary;
+ protected double[] realInput;
+ protected double[] realOutput;
+ protected double[] imaginaryInput;
+ protected double[] imaginaryOutput;
+ protected int cursor;
+
+ protected FFTBase() {
+ addPort(inputReal = new UnitInputPort("InputReal"));
+ addPort(inputImaginary = new UnitInputPort("InputImaginary"));
+ addPort(outputReal = new UnitOutputPort("OutputReal"));
+ addPort(outputImaginary = new UnitOutputPort("OutputImaginary"));
+ setSize(Spectrum.DEFAULT_SIZE);
+ }
+
+ public void setSize(int size) {
+ realInput = new double[size];
+ realOutput = new double[size];
+ imaginaryInput = new double[size];
+ imaginaryOutput = new double[size];
+ cursor = 0;
+ }
+
+ public int getSize() {
+ return realInput.length;
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] inputRs = inputReal.getValues();
+ double[] inputIs = inputImaginary.getValues();
+ double[] outputRs = outputReal.getValues();
+ double[] outputIs = outputImaginary.getValues();
+ for (int i = start; i < limit; i++) {
+ realInput[cursor] = inputRs[i];
+ imaginaryInput[cursor] = inputIs[i];
+ outputRs[i] = realOutput[cursor];
+ outputIs[i] = imaginaryOutput[cursor];
+ cursor += 1;
+ // When it is full, do the FFT.
+ if (cursor == realInput.length) {
+ // Copy to output buffer so we can do the FFT in place.
+ System.arraycopy(realInput, 0, realOutput, 0, realInput.length);
+ System.arraycopy(imaginaryInput, 0, imaginaryOutput, 0, imaginaryInput.length);
+ FourierMath.transform(getSign(), realOutput.length, realOutput, imaginaryOutput);
+ cursor = 0;
+ }
+ }
+ }
+
+ protected abstract int getSign();
+}
diff --git a/src/com/jsyn/unitgen/FilterAllPass.java b/src/com/jsyn/unitgen/FilterAllPass.java
new file mode 100644
index 0000000..749b2d6
--- /dev/null
+++ b/src/com/jsyn/unitgen/FilterAllPass.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2014 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitInputPort;
+
+/**
+ * AllPass filter using the following formula:
+ *
+ * <pre>
+ * y(n) = -gain * x(n) + x(n - 1) + gain * y(n - 1)
+ * </pre>
+ *
+ * where y(n) is Output, x(n) is Input, x(n-1) is a delayed copy of the input, and y(n-1) is a
+ * delayed copy of the output. An all-pass filter will pass all frequencies with equal amplitude.
+ * But it changes the phase relationships of the partials by delaying them by an amount proportional
+ * to their wavelength,.
+ *
+ * @author (C) 2014 Phil Burk, SoftSynth.com
+ * @see FilterLowPass
+ */
+
+public class FilterAllPass extends UnitFilter {
+ /** Feedback gain. Should be less than 1.0. Default is 0.8. */
+ public UnitInputPort gain;
+
+ private double x1;
+ private double y1;
+
+ public FilterAllPass() {
+ addPort(gain = new UnitInputPort("Gain", 0.8));
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] inputs = input.getValues();
+ double[] outputs = output.getValues();
+ double g = gain.getValue();
+
+ for (int i = start; i < limit; i++) {
+ double x0 = inputs[i];
+ y1 = (g * (y1 - x0)) + x1;
+ x1 = x0;
+ outputs[i] = y1;
+ }
+
+ }
+}
diff --git a/src/com/jsyn/unitgen/FilterBandPass.java b/src/com/jsyn/unitgen/FilterBandPass.java
new file mode 100644
index 0000000..39404d3
--- /dev/null
+++ b/src/com/jsyn/unitgen/FilterBandPass.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * Aug 21, 2009
+ * com.jsyn.engine.units.Filter_HighPass.java
+ */
+
+package com.jsyn.unitgen;
+
+/**
+ * Filter that allows frequencies around the center frequency to pass and blocks others. This filter
+ * is based on the BiQuad filter. Coefficients are updated whenever the frequency or Q changes.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc Translated from 'C' to Java by Lisa
+ * Tolenti.
+ */
+public class FilterBandPass extends FilterBiquadCommon {
+ /**
+ * This method is by Filter_Biquad to update coefficients for the Filter_BandPass filter.
+ */
+ @Override
+ public void updateCoefficients() {
+ double scalar = 1.0 / (1.0 + alpha);
+
+ a0 = alpha * scalar;
+ a1 = 0.0;
+ a2 = -a0;
+ b1 = -2.0 * cos_omega * scalar;
+ b2 = (1.0 - alpha) * scalar;
+ }
+}
diff --git a/src/com/jsyn/unitgen/FilterBandStop.java b/src/com/jsyn/unitgen/FilterBandStop.java
new file mode 100644
index 0000000..1ac371b
--- /dev/null
+++ b/src/com/jsyn/unitgen/FilterBandStop.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+/**
+ * Filter that blocks frequencies around the center frequency. This filter is based on the BiQuad
+ * filter. Coefficients are updated whenever the frequency or Q changes.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc Translated from 'C' to Java by Lisa
+ * Tolenti.
+ */
+public class FilterBandStop extends FilterBiquadCommon {
+
+ @Override
+ public void updateCoefficients() {
+
+ // scalar = 1.0f / (1.0f + BQCM.alpha);
+ // A1_B1_Value = -2.0f * BQCM.cos_omega * scalar;
+ //
+ // csFilter->csFBQ_A0 = scalar;
+ // csFilter->csFBQ_A1 = A1_B1_Value;
+ // csFilter->csFBQ_A2 = scalar;
+ // csFilter->csFBQ_B1 = A1_B1_Value;
+ // csFilter->csFBQ_B2 = (1.0f - BQCM.alpha) * scalar;
+
+ double scalar = 1.0 / (1.0 + alpha);
+ double a1_b1_value = -2.0 * cos_omega * scalar;
+
+ this.a0 = scalar;
+ this.a1 = a1_b1_value;
+ this.a2 = scalar;
+ this.b1 = a1_b1_value;
+ this.b2 = (1.0 - alpha) * scalar;
+ }
+}
diff --git a/src/com/jsyn/unitgen/FilterBiquad.java b/src/com/jsyn/unitgen/FilterBiquad.java
new file mode 100644
index 0000000..5e5d508
--- /dev/null
+++ b/src/com/jsyn/unitgen/FilterBiquad.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2011 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitInputPort;
+
+/**
+ * Base class for a set of IIR filters.
+ *
+ * @author Phil Burk (C) 2011 Mobileer Inc
+ * @see FilterBandStop
+ * @see FilterBandPass
+ * @see FilterLowPass
+ * @see FilterHighPass
+ * @see FilterTwoPoleTwoZero
+ */
+public abstract class FilterBiquad extends TunableFilter {
+ public UnitInputPort amplitude;
+
+ protected static final double MINIMUM_FREQUENCY = 0.00001;
+ protected static final double MINIMUM_GAIN = 0.00001;
+ protected static final double RATIO_MINIMUM = 0.499;
+ protected double a0;
+ protected double a1;
+ protected double a2;
+ protected double b1;
+ protected double b2;
+ private double x1;
+ private double x2;
+ private double y1;
+ private double y2;
+ protected double previousFrequency;
+ protected double omega;
+ protected double sin_omega;
+ protected double cos_omega;
+
+ public FilterBiquad() {
+ addPort(amplitude = new UnitInputPort("Amplitude", 1.0));
+ }
+
+ /**
+ * Generic generate(int start, int limit) method calls this filter's recalculate() and
+ * performBiquadFilter(int, int) methods.
+ */
+ @Override
+ public void generate(int start, int limit) {
+ recalculate();
+ performBiquadFilter(start, limit);
+ }
+
+ protected abstract void recalculate();
+
+ /**
+ * Each filter calls performBiquadFilter() through the generate(int, int) method. This method
+ * has converted Robert Bristow-Johnson's coefficients for the Direct I form in this way: Here
+ * is the equation that JSyn uses for this filter: y(n) = A0*x(n) + A1*x(n-1) + A2*x(n-2) -
+ * B1*y(n-1) - B2*y(n-2) Here is the equation that Robert Bristow-Johnson uses: y[n] =
+ * (b0/a0)*x[n] + (b1/a0)*x[n-1] + (b2/a0)*x[n-2] - (a1/a0)*y[n-1] - (a2/a0)*y[n-2] So to
+ * translate between JSyn coefficients and RBJ coefficients:
+ *
+ * <pre>
+ * JSyn => RBJ
+ * A0 => b0/a0
+ * A1 => b1/a0
+ * A2 => b2/a0
+ * B1 => a1/a0
+ * B2 => a2/a0
+ * </pre>
+ *
+ * @param start
+ * @param limit
+ */
+ public void performBiquadFilter(int start, int limit) {
+ double[] inputs = input.getValues();
+ double[] amplitudes = amplitude.getValues();
+ double[] outputs = output.getValues();
+ double a0_jsyn, a1_jsyn, a2_jsyn, b1_jsyn, b2_jsyn;
+ double x0_jsyn, x1_jsyn, x2_jsyn, y1_jsyn, y2_jsyn;
+
+ x1_jsyn = this.x1;
+ x2_jsyn = this.x2;
+
+ y1_jsyn = this.y1;
+ y2_jsyn = this.y2;
+
+ a0_jsyn = this.a0;
+ a1_jsyn = this.a1;
+ a2_jsyn = this.a2;
+
+ b1_jsyn = this.b1;
+ b2_jsyn = this.b2;
+
+ // Permute filter operations to reduce data movement.
+ for (int i = start; i < limit; i += 2)
+
+ {
+ x0_jsyn = inputs[i];
+ y2_jsyn = (a0_jsyn * x0_jsyn) + (a1_jsyn * x1_jsyn) + (a2_jsyn * x2_jsyn)
+ - (b1_jsyn * y1_jsyn) - (b2_jsyn * y2_jsyn);
+
+ outputs[i] = amplitudes[i] * y2_jsyn;
+
+ x2_jsyn = inputs[i + 1];
+ y1_jsyn = (a0_jsyn * x2_jsyn) + (a1_jsyn * x0_jsyn) + (a2_jsyn * x1_jsyn)
+ - (b1_jsyn * y2_jsyn) - (b2_jsyn * y1_jsyn);
+
+ outputs[i + 1] = amplitudes[i + 1] * y1_jsyn;
+
+ x1_jsyn = x2_jsyn;
+ x2_jsyn = x0_jsyn;
+ }
+
+ this.x1 = x1_jsyn; // save filter state for next time
+ this.x2 = x2_jsyn;
+
+ // apply small bipolar impulse to prevent arithmetic underflow
+ this.y1 = y1_jsyn + VERY_SMALL_FLOAT;
+ this.y2 = y2_jsyn - VERY_SMALL_FLOAT;
+ }
+
+ protected void calculateOmega(double ratio) {
+ if (ratio >= FilterBiquad.RATIO_MINIMUM) // keep a minimum
+ // distance from Nyquist
+ {
+ ratio = FilterBiquad.RATIO_MINIMUM;
+ }
+
+ omega = 2.0 * Math.PI * ratio;
+ cos_omega = Math.cos(omega); // compute cosine
+ sin_omega = Math.sin(omega); // compute sine
+ }
+
+}
diff --git a/src/com/jsyn/unitgen/FilterBiquadCommon.java b/src/com/jsyn/unitgen/FilterBiquadCommon.java
new file mode 100644
index 0000000..5d294a3
--- /dev/null
+++ b/src/com/jsyn/unitgen/FilterBiquadCommon.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitInputPort;
+
+/**
+ * Extend this class to create a filter that implements a Biquad filter with a Q port.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc Translated from 'C' to Java by Lisa
+ * Tolenti.
+ */
+public abstract class FilterBiquadCommon extends FilterBiquad {
+ public UnitInputPort Q;
+
+ protected final static double MINIMUM_Q = 0.00001;
+ private double previousQ;
+ protected double alpha;
+
+ /**
+ * No-argument constructor instantiates the Biquad common and adds a Q port to this filter.
+ */
+ public FilterBiquadCommon() {
+ addPort(Q = new UnitInputPort("Q"));
+ Q.setup(0.1, 1.0, 10.0);
+ }
+
+ /**
+ * Calculate coefficients based on the filter type, eg. LowPass.
+ */
+ public abstract void updateCoefficients();
+
+ public void computeBiquadCommon(double ratio, double Q) {
+ if (ratio >= FilterBiquad.RATIO_MINIMUM) // keep a minimum distance
+ // from Nyquist
+ {
+ ratio = FilterBiquad.RATIO_MINIMUM;
+ }
+
+ omega = 2.0 * Math.PI * ratio;
+ cos_omega = Math.cos(omega); // compute cosine
+ sin_omega = Math.sin(omega); // compute sine
+ alpha = sin_omega / (2.0 * Q); // set alpha
+ // System.out.println("Q = " + Q + ", omega = " + omega +
+ // ", cos(omega) = " + cos_omega + ", alpha = " + alpha );
+ }
+
+ /**
+ * The recalculate() method checks and ensures that the frequency and Q values are at a minimum.
+ * It also only updates the Biquad coefficients if either frequency or Q have changed.
+ */
+ @Override
+ public void recalculate() {
+ double frequencyValue = frequency.getValues()[0]; // grab frequency
+ // element (we'll
+ // only use
+ // element[0])
+ double qValue = Q.getValues()[0]; // grab Q element (we'll only use
+ // element[0])
+
+ if (frequencyValue < MINIMUM_FREQUENCY) // ensure a minimum frequency
+ {
+ frequencyValue = MINIMUM_FREQUENCY;
+ }
+
+ if (qValue < MINIMUM_Q) // ensure a minimum Q
+ {
+ qValue = MINIMUM_Q;
+ }
+ // only update changed values
+ if (isRecalculationNeeded(frequencyValue, qValue)) {
+ previousFrequency = frequencyValue; // hold previous frequency
+ previousQ = qValue; // hold previous Q
+
+ double ratio = frequencyValue * getFramePeriod();
+ computeBiquadCommon(ratio, qValue);
+ updateCoefficients();
+ }
+ }
+
+ protected boolean isRecalculationNeeded(double frequencyValue, double qValue) {
+ return (frequencyValue != previousFrequency) || (qValue != previousQ);
+ }
+
+}
diff --git a/src/com/jsyn/unitgen/FilterBiquadShelf.java b/src/com/jsyn/unitgen/FilterBiquadShelf.java
new file mode 100644
index 0000000..737d18d
--- /dev/null
+++ b/src/com/jsyn/unitgen/FilterBiquadShelf.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitInputPort;
+
+/**
+ * This filter is based on the BiQuad filter and is used as a base class for FilterLowShelf and
+ * FilterHighShelf. Coefficients are updated whenever the frequency, gain or slope changes.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ */
+public abstract class FilterBiquadShelf extends FilterBiquad {
+ protected final static double MINIMUM_SLOPE = 0.00001;
+
+ /**
+ * Gain of peak. Use 1.0 for flat response.
+ */
+ public UnitInputPort gain;
+
+ /**
+ * Shelf Slope parameter. When S = 1, the shelf slope is as steep as you can get it and remain
+ * monotonically increasing or decreasing gain with frequency.
+ */
+ public UnitInputPort slope;
+
+ private double prevGain;
+ private double prevSlope;
+
+ private double beta;
+ protected double alpha;
+ protected double factorA;
+ protected double AP1;
+ protected double AM1;
+ protected double beta_sn;
+ protected double AP1cs;
+ protected double AM1cs;
+
+ public FilterBiquadShelf() {
+ addPort(gain = new UnitInputPort("Gain", 1.0));
+ addPort(slope = new UnitInputPort("Slope", 1.0));
+ }
+
+ /**
+ * Abstract method. Each filter must implement its update of coefficients.
+ */
+ public abstract void updateCoefficients();
+
+ /**
+ * Compute coefficients for shelf filter if frequency, gain or slope have changed.
+ */
+ @Override
+ public void recalculate() {
+ // Just look at first value to save time.
+ double frequencyValue = frequency.getValues()[0];
+ if (frequencyValue < MINIMUM_FREQUENCY) {
+ frequencyValue = MINIMUM_FREQUENCY;
+ }
+
+ double gainValue = gain.getValues()[0];
+ if (gainValue < MINIMUM_GAIN) {
+ gainValue = MINIMUM_GAIN;
+ }
+
+ double slopeValue = slope.getValues()[0];
+ if (slopeValue < MINIMUM_SLOPE) {
+ slopeValue = MINIMUM_SLOPE;
+ }
+
+ // Only do complex calculations if input changed.
+ if ((frequencyValue != previousFrequency) || (gainValue != prevGain)
+ || (slopeValue != prevSlope)) {
+ previousFrequency = frequencyValue; // hold previous frequency
+ prevGain = gainValue;
+ prevSlope = slopeValue;
+
+ double ratio = frequencyValue * getFramePeriod();
+ calculateOmega(ratio);
+
+ factorA = Math.sqrt(gainValue);
+
+ AP1 = factorA + 1.0;
+ AM1 = factorA - 1.0;
+
+ /* Avoid sqrt(r<0) which hangs filter. */
+ double beta2 = ((gainValue + 1.0) / slopeValue) - (AM1 * AM1);
+ beta = (beta2 < 0.0) ? 0.0 : Math.sqrt(beta2);
+
+ beta_sn = beta * sin_omega;
+ AP1cs = AP1 * cos_omega;
+ AM1cs = AM1 * cos_omega;
+
+ updateCoefficients();
+ }
+ }
+
+}
diff --git a/src/com/jsyn/unitgen/FilterFourPoles.java b/src/com/jsyn/unitgen/FilterFourPoles.java
new file mode 100644
index 0000000..5ad6487
--- /dev/null
+++ b/src/com/jsyn/unitgen/FilterFourPoles.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2014 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitInputPort;
+
+/**
+ * Resonant filter in the style of the Moog ladder filter. This implementation is loosely based on:
+ * http://www.musicdsp.org/archive.php?classid=3#26
+ * More interesting reading:
+ * http://dafx04.na.infn.it/WebProc/Proc/P_061.pdf
+ * http://www.acoustics.ed.ac.uk/wp-content/uploads/AMT_MSc_FinalProjects
+ * /2012__Daly__AMT_MSc_FinalProject_MoogVCF.pdf
+ * http://www.music.mcgill.ca/~ich/research/misc/papers/cr1071.pdf
+ *
+ * @author Phil Burk (C) 2014 Mobileer Inc
+ * @see FilterLowPass
+ */
+public class FilterFourPoles extends TunableFilter {
+ public UnitInputPort Q;
+ public UnitInputPort gain;
+
+ private static final double MINIMUM_FREQUENCY = 0.00001;
+ private static final double MINIMUM_Q = 0.00001;
+
+ private double x1;
+ private double x2;
+ private double x3;
+ private double x4;
+ private double y1;
+ private double y2;
+ private double y3;
+ private double y4;
+
+ private double previousFrequency;
+ private double previousQ;
+ // filter coefficients
+ private double f;
+ private double fTo4th;
+ private double feedback;
+
+ private boolean oversampled = true;
+
+ public FilterFourPoles() {
+ addPort(Q = new UnitInputPort("Q"));
+ Q.setup(0.1, 2.0, 10.0);
+ }
+
+ /**
+ * The recalculate() method checks and ensures that the frequency and Q values are at a minimum.
+ * It also only updates the coefficients if either frequency or Q have changed.
+ */
+ public void recalculate() {
+ double frequencyValue = frequency.getValues()[0];
+ double qValue = Q.getValues()[0];
+
+ if (frequencyValue < MINIMUM_FREQUENCY) // ensure a minimum frequency
+ {
+ frequencyValue = MINIMUM_FREQUENCY;
+ }
+ if (qValue < MINIMUM_Q) // ensure a minimum Q
+ {
+ qValue = MINIMUM_Q;
+ }
+
+ // Only recompute coefficients if changed.
+ if ((frequencyValue != previousFrequency) || (qValue != previousQ)) {
+ previousFrequency = frequencyValue;
+ previousQ = qValue;
+ computeCoefficients();
+ }
+ }
+
+ private void computeCoefficients() {
+ double normalizedFrequency = previousFrequency * getFramePeriod();
+ double fudge = 4.9 - 0.27 * previousQ;
+ if (fudge < 3.0)
+ fudge = 3.0;
+ f = normalizedFrequency * (oversampled ? 1.0 : 2.0) * fudge;
+
+ double fSquared = f * f;
+ fTo4th = fSquared * fSquared;
+ feedback = 0.5 * previousQ * (1.0 - 0.15 * fSquared);
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] inputs = input.getValues();
+ double[] outputs = output.getValues();
+
+ recalculate();
+
+ for (int i = start; i < limit; i++) {
+ double x0 = inputs[i];
+
+ if (oversampled) {
+ oneSample(0.0);
+ }
+ oneSample(x0);
+
+ outputs[i] = y4;
+ }
+
+ // apply small bipolar impulse to prevent arithmetic underflow
+ y1 += VERY_SMALL_FLOAT;
+ y2 -= VERY_SMALL_FLOAT;
+ }
+
+ private void oneSample(double x0) {
+ final double coeff = 0.3;
+ x0 -= y4 * feedback; // feedback
+ x0 *= 0.35013 * fTo4th;
+ y1 = x0 + coeff * x1 + (1 - f) * y1; // pole 1
+ x1 = x0;
+ y2 = y1 + coeff * x2 + (1 - f) * y2; // pole 2
+ x2 = y1;
+ y3 = y2 + coeff * x3 + (1 - f) * y3; // pole 3
+ x3 = y2;
+ y4 = y3 + coeff * x4 + (1 - f) * y4; // pole 4
+ y4 = clip(y4);
+ x4 = y3;
+ }
+
+ public boolean isOversampled() {
+ return oversampled;
+ }
+
+ public void setOversampled(boolean oversampled) {
+ this.oversampled = oversampled;
+ }
+
+ private double clip(double x) {
+ return x - (x * x * x * 0.1666667);
+ }
+
+}
diff --git a/src/com/jsyn/unitgen/FilterHighPass.java b/src/com/jsyn/unitgen/FilterHighPass.java
new file mode 100644
index 0000000..8641797
--- /dev/null
+++ b/src/com/jsyn/unitgen/FilterHighPass.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * Aug 21, 2009
+ * com.jsyn.engine.units.Filter_HighPass.java
+ */
+
+package com.jsyn.unitgen;
+
+/**
+ * Filter that allows frequencies above the center frequency to pass. This filter is based on the
+ * BiQuad filter. Coefficients are updated whenever the frequency or Q changes.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc Translated from 'C' to Java by Lisa
+ * Tolenti.
+ */
+public class FilterHighPass extends FilterBiquadCommon {
+ /**
+ * This method is used by Filter_Biquad to update coefficients for the Filter_HighPass filter.
+ */
+ @Override
+ public void updateCoefficients() {
+ double scalar = 1.0 / (1.0 + alpha);
+ double onePlusCosine = 1.0 + cos_omega;
+ double a0_a2_value = onePlusCosine * 0.5 * scalar;
+
+ this.a0 = a0_a2_value;
+ this.a1 = -onePlusCosine * scalar;
+ this.a2 = a0_a2_value;
+ this.b1 = -2.0 * cos_omega * scalar;
+ this.b2 = (1.0 - alpha) * scalar;
+ }
+}
diff --git a/src/com/jsyn/unitgen/FilterHighShelf.java b/src/com/jsyn/unitgen/FilterHighShelf.java
new file mode 100644
index 0000000..449090a
--- /dev/null
+++ b/src/com/jsyn/unitgen/FilterHighShelf.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+/**
+ * HighShelf Filter. This creates a flat response above the cutoff frequency. This filter is
+ * sometimes used at the end of a bank of EQ filters.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ */
+public class FilterHighShelf extends FilterBiquadShelf {
+ /**
+ * This method is called by by Filter_BiquadShelf to update coefficients.
+ */
+ @Override
+ public void updateCoefficients() {
+ double scalar = 1.0 / (AP1 - AM1cs + beta_sn);
+ a0 = factorA * (AP1 + AM1cs + beta_sn) * scalar;
+ a1 = -2.0 * factorA * (AM1 + AP1cs) * scalar;
+ a2 = factorA * (AP1 + AM1cs - beta_sn) * scalar;
+ b1 = 2.0 * (AM1 - AP1cs) * scalar;
+ b2 = (AP1 - AM1cs - beta_sn) * scalar;
+ }
+}
diff --git a/src/com/jsyn/unitgen/FilterLowPass.java b/src/com/jsyn/unitgen/FilterLowPass.java
new file mode 100644
index 0000000..93d58f2
--- /dev/null
+++ b/src/com/jsyn/unitgen/FilterLowPass.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * Aug 21, 2009
+ * com.jsyn.engine.units.Filter_HighPass.java
+ */
+
+package com.jsyn.unitgen;
+
+/**
+ * Filter that allows frequencies below the center frequency to pass. This filter is based on the
+ * BiQuad filter. Coefficients are updated whenever the frequency or Q changes.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc Translated from 'C' to Java by Lisa
+ * Tolenti.
+ */
+public class FilterLowPass extends FilterBiquadCommon {
+
+ /**
+ * This method is by Filter_Biquad to update coefficients for the Filter_LowPass filter.
+ */
+ @Override
+ public void updateCoefficients() {
+
+ // scalar = 1.0f / (1.0f + BQCM.alpha);
+ // omc = (1.0f - BQCM.cos_omega);
+ // A0_A2_Value = omc * 0.5f * scalar;
+ // // translating from RBJ coefficients
+ // // A0 = (b0/(2*a0)
+ // // = ((1 - cos_omega)/2) / (1 + alpha)
+ // // = (omc*0.5) / (1 + alpha)
+ // // = (omc*0.5) * (1.0/(1 + alpha))
+ // // = omc * 0.5 * scalar
+ // csFilter->csFBQ_A0 = A0_A2_Value;
+ // csFilter->csFBQ_A1 = omc * scalar;
+ // csFilter->csFBQ_A2 = A0_A2_Value;
+ // csFilter->csFBQ_B1 = -2.0f * BQCM.cos_omega * scalar;
+ // csFilter->csFBQ_B2 = (1.0f - BQCM.alpha) * scalar;
+
+ double scalar = 1.0 / (1.0 + alpha);
+ double oneMinusCosine = 1.0 - cos_omega;
+ double a0_a2_value = oneMinusCosine * 0.5 * scalar;
+
+ this.a0 = a0_a2_value;
+ this.a1 = oneMinusCosine * scalar;
+ this.a2 = a0_a2_value;
+ this.b1 = -2.0 * cos_omega * scalar;
+ this.b2 = (1.0 - alpha) * scalar;
+ }
+}
diff --git a/src/com/jsyn/unitgen/FilterLowShelf.java b/src/com/jsyn/unitgen/FilterLowShelf.java
new file mode 100644
index 0000000..cf41f45
--- /dev/null
+++ b/src/com/jsyn/unitgen/FilterLowShelf.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+/**
+ * LowShelf Filter. This creates a flat response below the cutoff frequency. This filter is
+ * sometimes used at the end of a bank of EQ filters. This filter is based on the BiQuad filter.
+ * Coefficients are updated whenever the frequency or Q changes.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ */
+public class FilterLowShelf extends FilterBiquadShelf {
+
+ /**
+ * This method is called by Filter_BiquadShelf to update coefficients.
+ */
+ @Override
+ public void updateCoefficients() {
+ double scalar = 1.0 / (AP1 + AM1cs + beta_sn);
+ a0 = factorA * (AP1 - AM1cs + beta_sn) * scalar;
+ a1 = 2.0 * factorA * (AM1 - AP1cs) * scalar;
+ a2 = factorA * (AP1 - AM1cs - beta_sn) * scalar;
+ b1 = -2.0 * (AM1 + AP1cs) * scalar;
+ b2 = (AP1 + AM1cs - beta_sn) * scalar;
+ }
+}
diff --git a/src/com/jsyn/unitgen/FilterOnePole.java b/src/com/jsyn/unitgen/FilterOnePole.java
new file mode 100644
index 0000000..090e42b
--- /dev/null
+++ b/src/com/jsyn/unitgen/FilterOnePole.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitVariablePort;
+
+/**
+ * First Order, One Pole filter using the following formula:
+ *
+ * <pre>
+ * y(n) = A0 * x(n) - B1 * y(n - 1)
+ * </pre>
+ *
+ * where y(n) is Output, x(n) is Input and y(n-1) is a delayed copy of the output. This filter is a
+ * recursive IIR or Infinite Impulse Response filter. It can be unstable depending on the values of
+ * the coefficients. This can be useful as a low-pass filter, or a "leaky integrator". A thorough
+ * description of the digital filter theory needed to fully describe this filter is beyond the scope
+ * of this document. Calculating coefficients is non-intuitive; the interested user is referred to
+ * one of the standard texts on filter theory (e.g., Moore, "Elements of Computer Music", section
+ * 2.4).
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ * @see FilterLowPass
+ */
+public class FilterOnePole extends UnitFilter {
+ public UnitVariablePort a0;
+ public UnitVariablePort b1;
+ private double y1;
+
+ public FilterOnePole() {
+ addPort(a0 = new UnitVariablePort("A0", 0.6));
+ addPort(b1 = new UnitVariablePort("B1", -0.3));
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] inputs = input.getValues();
+ double[] outputs = output.getValues();
+ double a0v = a0.getValue();
+ double b1v = b1.getValue();
+
+ for (int i = start; i < limit; i++) {
+ double x0 = inputs[i];
+ outputs[i] = y1 = (a0v * x0) - (b1v * y1);
+ }
+
+ }
+}
diff --git a/src/com/jsyn/unitgen/FilterOnePoleOneZero.java b/src/com/jsyn/unitgen/FilterOnePoleOneZero.java
new file mode 100644
index 0000000..ed1868c
--- /dev/null
+++ b/src/com/jsyn/unitgen/FilterOnePoleOneZero.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 1997 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitVariablePort;
+
+/**
+ * First Order, One Pole, One Zero filter using the following formula:
+ *
+ * <pre>
+ * y(n) = A0 * x(n) + A1 * x(n - 1) - B1 * y(n - 1)
+ * </pre>
+ *
+ * where y(n) is Output, x(n) is Input, x(n-1) is a delayed copy of the input, and y(n-1) is a
+ * delayed copy of the output. This filter is a recursive IIR or Infinite Impulse Response filter.
+ * it can be unstable depending on the values of the coefficients. A thorough description of the
+ * digital filter theory needed to fully describe this filter is beyond the scope of this document.
+ * Calculating coefficients is non-intuitive; the interested user is referred to one of the standard
+ * texts on filter theory (e.g., Moore, "Elements of Computer Music", section 2.4).
+ *
+ * @author (C) 1997-2009 Phil Burk, SoftSynth.com
+ * @see FilterLowPass
+ */
+
+public class FilterOnePoleOneZero extends UnitFilter {
+ public UnitVariablePort a0;
+ public UnitVariablePort a1;
+ public UnitVariablePort b1;
+
+ private double x1;
+ private double y1;
+
+ public FilterOnePoleOneZero() {
+ addPort(a0 = new UnitVariablePort("A0"));
+ addPort(a1 = new UnitVariablePort("A1"));
+ addPort(b1 = new UnitVariablePort("B1"));
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] inputs = input.getValues();
+ double[] outputs = output.getValues();
+ double a0v = a0.getValue();
+ double a1v = a1.getValue();
+ double b1v = b1.getValue();
+
+ for (int i = start; i < limit; i++) {
+ double x0 = inputs[i];
+ outputs[i] = y1 = (a0v * x0) + (a1v * x1) + (b1v * y1);
+ x1 = x0;
+ }
+
+ }
+}
diff --git a/src/com/jsyn/unitgen/FilterOneZero.java b/src/com/jsyn/unitgen/FilterOneZero.java
new file mode 100644
index 0000000..2a07a16
--- /dev/null
+++ b/src/com/jsyn/unitgen/FilterOneZero.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2011 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitVariablePort;
+
+/**
+ * First Order, One Zero filter using the following formula:
+ *
+ * <pre>
+ * y(n) = A0 * x(n) + A1 * x(n - 1)
+ * </pre>
+ *
+ * where y(n) is Output, x(n) is Input and x(n-1) is Input at the prior sample tick. Setting A1
+ * positive gives a low-pass response; setting A1 negative gives a high-pass response. The bandwidth
+ * of this filter is fairly high, so it often serves a building block by being cascaded with other
+ * filters. If A0 and A1 are both 0.5, then this filter is a simple averaging lowpass filter, with a
+ * zero at SR/2 = 22050 Hz. If A0 is 0.5 and A1 is -0.5, then this filter is a high pass filter,
+ * with a zero at 0.0 Hz. A thorough description of the digital filter theory needed to fully
+ * describe this filter is beyond the scope of this document. Calculating coefficients is
+ * non-intuitive; the interested user is referred to one of the standard texts on filter theory
+ * (e.g., Moore, "Elements of Computer Music", section 2.4).
+ *
+ * @author Phil Burk (C) 2011 Mobileer Inc
+ * @see FilterLowPass
+ */
+public class FilterOneZero extends UnitFilter {
+ public UnitVariablePort a0;
+ public UnitVariablePort a1;
+ private double x1;
+
+ public FilterOneZero() {
+ addPort(a0 = new UnitVariablePort("A0", 0.5));
+ addPort(a1 = new UnitVariablePort("A1", 0.5));
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] inputs = input.getValues();
+ double[] outputs = output.getValues();
+ double a0v = a0.getValue();
+ double a1v = a1.getValue();
+
+ for (int i = start; i < limit; i++) {
+ double x0 = inputs[i];
+ outputs[i] = (a0v * x0) + (a1v * x1);
+ x1 = x0;
+ }
+
+ }
+}
diff --git a/src/com/jsyn/unitgen/FilterPeakingEQ.java b/src/com/jsyn/unitgen/FilterPeakingEQ.java
new file mode 100644
index 0000000..bec7096
--- /dev/null
+++ b/src/com/jsyn/unitgen/FilterPeakingEQ.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitInputPort;
+
+/**
+ * PeakingEQ Filter. This can be used to raise or lower the gain around the cutoff frequency. This
+ * filter is sometimes used in the middle of a bank of EQ filters. This filter is based on the
+ * BiQuad filter. Coefficients are updated whenever the frequency or Q changes.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ */
+public class FilterPeakingEQ extends FilterBiquadCommon {
+ public UnitInputPort gain;
+
+ private double previousGain;
+
+ public FilterPeakingEQ() {
+ addPort(gain = new UnitInputPort("Gain", 1.0));
+ }
+
+ @Override
+ protected boolean isRecalculationNeeded(double frequencyValue, double qValue) {
+ double currentGain = gain.getValues()[0];
+ if (currentGain < MINIMUM_GAIN) {
+ currentGain = MINIMUM_GAIN;
+ }
+
+ boolean needed = super.isRecalculationNeeded(frequencyValue, qValue);
+ needed |= (previousGain != currentGain);
+
+ previousGain = currentGain;
+ return needed;
+ }
+
+ @Override
+ public void updateCoefficients() {
+ double factorA = Math.sqrt(previousGain);
+ double alphaTimesA = alpha * factorA;
+ double alphaOverA = alpha / factorA;
+ // Note this is not the normal scalar!
+ double scalar = 1.0 / (1.0 + alphaOverA);
+ double a1_b1_value = -2.0 * cos_omega * scalar;
+
+ this.a0 = (1.0 + alphaTimesA) * scalar;
+
+ this.a1 = a1_b1_value;
+ this.a2 = (1.0 - alphaTimesA) * scalar;
+
+ this.b1 = a1_b1_value;
+ this.b2 = (1.0 - alphaOverA) * scalar;
+ }
+}
diff --git a/src/com/jsyn/unitgen/FilterStateVariable.java b/src/com/jsyn/unitgen/FilterStateVariable.java
new file mode 100644
index 0000000..751b3ae
--- /dev/null
+++ b/src/com/jsyn/unitgen/FilterStateVariable.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.ports.UnitOutputPort;
+
+/**
+ * A versatile filter described in Hal Chamberlain's "Musical Applications of MicroProcessors". It
+ * is convenient because its frequency and resonance can each be controlled by a single value. The
+ * "output" port of this filter is the "lowPass" output multiplied by the "amplitude"
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ * @see FilterLowPass
+ * @see FilterHighPass
+ */
+public class FilterStateVariable extends TunableFilter {
+ /**
+ * Amplitude of Output in the range of 0.0 to 1.0. SIGNAL_TYPE_RAW_SIGNED Defaults to 1.0
+ * <P>
+ * Note that the amplitude only affects the "output" port and not the lowPass, bandPass or
+ * highPass signals. Use a MultiplyUnit if you need to scale those signals.
+ */
+ public UnitInputPort amplitude;
+
+ /**
+ * Controls feedback that causes self oscillation. Actually 1/Q - SIGNAL_TYPE_RAW_SIGNED in the
+ * range of 0.0 to 1.0. Defaults to 0.125.
+ */
+ public UnitInputPort resonance;
+ /**
+ * Low pass filtered signal.
+ * <P>
+ * Note that this signal is not affected by the amplitude port.
+ */
+ public UnitOutputPort lowPass;
+ /**
+ * Band pass filtered signal.
+ * <P>
+ * Note that this signal is not affected by the amplitude port.
+ */
+ public UnitOutputPort bandPass;
+ /**
+ * High pass filtered signal.
+ * <P>
+ * Note that this signal is not affected by the amplitude port.
+ */
+ public UnitOutputPort highPass;
+
+ private double freqInternal;
+ private double previousFrequency = Double.MAX_VALUE; // So we trigger an immediate update.
+ private double lowPassValue;
+ private double bandPassValue;
+
+ /**
+ * No-argument constructor instantiates the Biquad common and adds an amplitude port to this
+ * filter.
+ */
+ public FilterStateVariable() {
+ frequency.set(440.0);
+ addPort(resonance = new UnitInputPort("Resonance", 0.2));
+ addPort(amplitude = new UnitInputPort("Amplitude", 1.0));
+ addPort(lowPass = new UnitOutputPort("LowPass"));
+ addPort(bandPass = new UnitOutputPort("BandPass"));
+ addPort(highPass = new UnitOutputPort("HighPass"));
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] inputs = input.getValues();
+ double[] outputs = output.getValues();
+ double[] frequencies = frequency.getValues();
+ double[] amplitudes = amplitude.getValues();
+ double[] reses = resonance.getValues();
+ double[] lows = lowPass.getValues();
+ double[] highs = highPass.getValues();
+ double[] bands = bandPass.getValues();
+
+ double newFreq = frequencies[0];
+ if (newFreq != previousFrequency) {
+ previousFrequency = newFreq;
+ freqInternal = 2.0 * Math.sin(Math.PI * newFreq * getFramePeriod());
+ }
+
+ for (int i = start; i < limit; i++) {
+ lowPassValue = (freqInternal * bandPassValue) + lowPassValue;
+ // Clip between -1 and +1 to prevent blowup.
+ lowPassValue = (lowPassValue < -1.0) ? -1.0 : ((lowPassValue > 1.0) ? 1.0
+ : lowPassValue);
+ lows[i] = lowPassValue;
+
+ outputs[i] = lowPassValue * (amplitudes[i]);
+ double highPassValue = inputs[i] - (reses[i] * bandPassValue) - lowPassValue;
+ // System.out.println("low = " + lowPassValue + ", band = " + bandPassValue +
+ // ", high = " + highPassValue );
+ highs[i] = highPassValue;
+
+ bandPassValue = (freqInternal * highPassValue) + bandPassValue;
+ bands[i] = bandPassValue;
+ // System.out.println("low = " + lowPassValue + ", band = " + bandPassValue +
+ // ", high = " + highPassValue );
+ }
+ }
+
+}
diff --git a/src/com/jsyn/unitgen/FilterTwoPoles.java b/src/com/jsyn/unitgen/FilterTwoPoles.java
new file mode 100644
index 0000000..0c68a64
--- /dev/null
+++ b/src/com/jsyn/unitgen/FilterTwoPoles.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 1997 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitVariablePort;
+
+/**
+ * Second Order, Two Pole filter using the following formula:
+ *
+ * <pre>
+ * y(n) = A0 * x(n) - B1 * y(n - 1) - B2 * y(n - 2)
+ * </pre>
+ *
+ * where y(n) is Output, x(n) is Input, and y(n-1) is a delayed copy of the output. This filter is a
+ * recursive IIR or Infinite Impulse Response filter. it can be unstable depending on the values of
+ * the coefficients. A thorough description of the digital filter theory needed to fully describe
+ * this filter is beyond the scope of this document. Calculating coefficients is non-intuitive; the
+ * interested user is referred to one of the standard texts on filter theory (e.g., Moore,
+ * "Elements of Computer Music", section 2.4).
+ *
+ * @author (C) 1997-2009 Phil Burk, Mobileer Inc
+ */
+
+public class FilterTwoPoles extends UnitFilter {
+ public UnitVariablePort a0;
+ public UnitVariablePort b1;
+ public UnitVariablePort b2;
+ private double y1;
+ private double y2;
+
+ public FilterTwoPoles() {
+ addPort(a0 = new UnitVariablePort("A0"));
+ addPort(b1 = new UnitVariablePort("B1"));
+ addPort(b2 = new UnitVariablePort("B2"));
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] inputs = input.getValues();
+ double[] outputs = output.getValues();
+ double a0v = a0.getValue();
+ double b1v = b1.getValue();
+ double b2v = b2.getValue();
+
+ for (int i = start; i < limit; i++) {
+ double x0 = inputs[i];
+ outputs[i] = y1 = 2.0 * ((a0v * x0) + (b1v * y1) + (b2v * y2));
+ y2 = y1;
+ }
+
+ }
+}
diff --git a/src/com/jsyn/unitgen/FilterTwoPolesTwoZeros.java b/src/com/jsyn/unitgen/FilterTwoPolesTwoZeros.java
new file mode 100644
index 0000000..cde279f
--- /dev/null
+++ b/src/com/jsyn/unitgen/FilterTwoPolesTwoZeros.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 1997 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitVariablePort;
+
+/**
+ * Second Order, Two Pole, Two Zero filter using the following formula:
+ *
+ * <pre>
+ * y(n) = 2.0 * (A0 * x(n) + A1 * x(n - 1) + A2 * x(n - 2) - B1 * y(n - 1) - B2 * y(n - 2))
+ * </pre>
+ *
+ * where y(n) is Output, x(n) is Input, x(n-1) is a delayed copy of the input, and y(n-1) is a
+ * delayed copy of the output. This filter is a recursive IIR or Infinite Impulse Response filter.
+ * It can be unstable depending on the values of the coefficients. This filter is basically the same
+ * as the FilterBiquad with different ports. A thorough description of the digital filter theory
+ * needed to fully describe this filter is beyond the scope of this document. Calculating
+ * coefficients is non-intuitive; the interested user is referred to one of the standard texts on
+ * filter theory (e.g., Moore, "Elements of Computer Music", section 2.4). Special thanks to Robert
+ * Bristow-Johnson for contributing his filter equations to the music-dsp list. They were used for
+ * calculating the coefficients for the lowPass, highPass, and other parametric filter calculations.
+ *
+ * @author (C) 1997-2009 Phil Burk, SoftSynth.com
+ */
+
+public class FilterTwoPolesTwoZeros extends UnitFilter {
+ public UnitVariablePort a0;
+ public UnitVariablePort a1;
+ public UnitVariablePort a2;
+ public UnitVariablePort b1;
+ public UnitVariablePort b2;
+ private double x1;
+ private double y1;
+ private double x2;
+ private double y2;
+
+ public FilterTwoPolesTwoZeros() {
+ addPort(a0 = new UnitVariablePort("A0"));
+ addPort(a1 = new UnitVariablePort("A1"));
+ addPort(a2 = new UnitVariablePort("A2"));
+ addPort(b1 = new UnitVariablePort("B1"));
+ addPort(b2 = new UnitVariablePort("B2"));
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] inputs = input.getValues();
+ double[] outputs = output.getValues();
+ double a0v = a0.getValue();
+ double a1v = a1.getValue();
+ double a2v = a2.getValue();
+ double b1v = b1.getValue();
+ double b2v = b2.getValue();
+
+ for (int i = start; i < limit; i++) {
+ double x0 = inputs[i];
+ outputs[i] = y1 = 2.0 * ((a0v * x0) + (a1v * x1) + (a2v * x2) + (b1v * y1) + (b2v * y2));
+ x2 = x1;
+ x1 = x0;
+ y2 = y1;
+ }
+
+ }
+}
diff --git a/src/com/jsyn/unitgen/FixedRateMonoReader.java b/src/com/jsyn/unitgen/FixedRateMonoReader.java
new file mode 100644
index 0000000..c6edc23
--- /dev/null
+++ b/src/com/jsyn/unitgen/FixedRateMonoReader.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitOutputPort;
+
+/**
+ * Simple sample player. Play one sample per audio frame with no interpolation.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ */
+public class FixedRateMonoReader extends SequentialDataReader {
+
+ public FixedRateMonoReader() {
+ addPort(output = new UnitOutputPort());
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+
+ double[] amplitudes = amplitude.getValues();
+ double[] outputs = output.getValues();
+
+ for (int i = start; i < limit; i++) {
+ if (dataQueue.hasMore()) {
+ double fdata = dataQueue.readNextMonoDouble(getFramePeriod());
+ outputs[i] = fdata * amplitudes[i];
+ } else {
+ outputs[i] = 0.0;
+ if (dataQueue.testAndClearAutoStop()) {
+ autoStop();
+ }
+ }
+ dataQueue.firePendingCallbacks();
+ }
+ }
+
+}
diff --git a/src/com/jsyn/unitgen/FixedRateMonoWriter.java b/src/com/jsyn/unitgen/FixedRateMonoWriter.java
new file mode 100644
index 0000000..4dd9f2a
--- /dev/null
+++ b/src/com/jsyn/unitgen/FixedRateMonoWriter.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitInputPort;
+
+/**
+ * Simple sample writer. Write one sample per audio frame with no interpolation. This can be used to
+ * record audio or to build delay lines.
+ *
+ * @see FixedRateStereoWriter
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ */
+public class FixedRateMonoWriter extends SequentialDataWriter {
+ public FixedRateMonoWriter() {
+ addPort(input = new UnitInputPort("Input"));
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] inputs = input.getValues();
+
+ for (int i = start; i < limit; i++) {
+ if (dataQueue.hasMore()) {
+ double value = inputs[i];
+ dataQueue.writeNextDouble(value);
+ } else {
+ if (dataQueue.testAndClearAutoStop()) {
+ autoStop();
+ }
+ }
+ dataQueue.firePendingCallbacks();
+ }
+
+ }
+
+}
diff --git a/src/com/jsyn/unitgen/FixedRateStereoReader.java b/src/com/jsyn/unitgen/FixedRateStereoReader.java
new file mode 100644
index 0000000..c5c00ce
--- /dev/null
+++ b/src/com/jsyn/unitgen/FixedRateStereoReader.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitOutputPort;
+
+/**
+ * Simple stereo sample player. Play one sample per audio frame with no interpolation.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ */
+public class FixedRateStereoReader extends SequentialDataReader {
+ public FixedRateStereoReader() {
+ addPort(output = new UnitOutputPort(2, "Output"));
+ dataQueue.setNumChannels(2);
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] amplitudes = amplitude.getValues();
+ double[] output0s = output.getValues(0);
+ double[] output1s = output.getValues(1);
+
+ for (int i = start; i < limit; i++) {
+ if (dataQueue.hasMore()) {
+ dataQueue.beginFrame(getFramePeriod());
+ double fdata = dataQueue.readCurrentChannelDouble(0);
+ // System.out.println("SampleReader_16F2: left = " + fdata );
+ double amp = amplitudes[i];
+ output0s[i] = fdata * amp;
+ fdata = dataQueue.readCurrentChannelDouble(1);
+ // System.out.println("SampleReader_16F2: right = " + fdata );
+ output1s[i] = fdata * amp;
+ dataQueue.endFrame();
+ } else {
+ output0s[i] = 0.0;
+ output1s[i] = 0.0;
+ if (dataQueue.testAndClearAutoStop()) {
+ autoStop();
+ }
+ }
+ dataQueue.firePendingCallbacks();
+ }
+ }
+}
diff --git a/src/com/jsyn/unitgen/FixedRateStereoWriter.java b/src/com/jsyn/unitgen/FixedRateStereoWriter.java
new file mode 100644
index 0000000..9f4e38d
--- /dev/null
+++ b/src/com/jsyn/unitgen/FixedRateStereoWriter.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitInputPort;
+
+/**
+ * Simple stereo sample writer. Write two samples per audio frame with no interpolation. This can be
+ * used to record audio or to build delay lines.
+ *
+ * @see FixedRateMonoWriter
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ */
+public class FixedRateStereoWriter extends SequentialDataWriter {
+
+ public FixedRateStereoWriter() {
+ addPort(input = new UnitInputPort(2, "Input"));
+ dataQueue.setNumChannels(2);
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] input0s = input.getValues(0);
+ double[] input1s = input.getValues(1);
+
+ for (int i = start; i < limit; i++) {
+ if (dataQueue.hasMore()) {
+ dataQueue.beginFrame(getFramePeriod());
+ double value = input0s[i];
+ dataQueue.writeCurrentChannelDouble(0, value);
+ value = input1s[i];
+ dataQueue.writeCurrentChannelDouble(1, value);
+ dataQueue.endFrame();
+ } else {
+ if (dataQueue.testAndClearAutoStop()) {
+ autoStop();
+ }
+ }
+ dataQueue.firePendingCallbacks();
+ }
+
+ }
+
+}
diff --git a/src/com/jsyn/unitgen/FourWayFade.java b/src/com/jsyn/unitgen/FourWayFade.java
new file mode 100644
index 0000000..ac30739
--- /dev/null
+++ b/src/com/jsyn/unitgen/FourWayFade.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 1997 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.ports.UnitOutputPort;
+
+/**
+ * FourWayFade unit.
+ * <P>
+ * Mix inputs 0-3 based on the value of two fade ports. You can think of the four inputs arranged
+ * clockwise as follows.
+ * <P>
+ *
+ * <PRE>
+ * input[0] ---- input[1]
+ * | |
+ * | |
+ * | |
+ * input[3] ---- input[2]
+ * </PRE>
+ *
+ * The "fade" port has two parts. Fade[0] fades between the pair of inputs (0&3) and the pair of
+ * inputs (1&2). Fade[1] fades between the pair of inputs (0&1) and the pair of inputs (3&2).
+ *
+ * <PRE>
+ * Fade[0] Fade[1] Output
+ * -1 -1 Input[3]
+ * -1 +1 Input[0]
+ * +1 -1 Input[2]
+ * +1 +1 Input[1]
+ *
+ *
+ * -----Fade[0]----->
+ *
+ * A
+ * |
+ * |
+ * Fade[1]
+ * |
+ * |
+ * </PRE>
+ * <P>
+ *
+ * @author (C) 1997-2009 Phil Burk, Mobileer Inc
+ */
+public class FourWayFade extends UnitGenerator {
+ public UnitInputPort input;
+ public UnitInputPort fade;
+ public UnitOutputPort output;
+
+ /* Define Unit Ports used by connect() and set(). */
+ public FourWayFade() {
+ addPort(input = new UnitInputPort(4, "Input"));
+ addPort(fade = new UnitInputPort(2, "Fade"));
+ addPort(output = new UnitOutputPort());
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] inputAs = input.getValues(0);
+ double[] inputBs = input.getValues(1);
+ double[] inputCs = input.getValues(2);
+ double[] inputDs = input.getValues(3);
+ double[] fadeLRs = fade.getValues(0);
+ double[] fadeFBs = fade.getValues(1);
+ double[] outputs = output.getValues(0);
+
+ for (int i = start; i < limit; i++) {
+ // Scale and offset to 0.0 to 1.0 range.
+ double gainLR = (fadeLRs[i] * 0.5) + 0.5;
+ double temp = 1.0 - gainLR;
+ double mixFront = (inputAs[i] * temp) + (inputBs[i] * gainLR);
+ double mixBack = (inputDs[i] * temp) + (inputCs[i] * gainLR);
+
+ double gainFB = (fadeFBs[i] * 0.5) + 0.5;
+ outputs[i] = (mixBack * (1.0 - gainFB)) + (mixFront * gainFB);
+ }
+ }
+}
diff --git a/src/com/jsyn/unitgen/FunctionEvaluator.java b/src/com/jsyn/unitgen/FunctionEvaluator.java
new file mode 100644
index 0000000..0cc0c83
--- /dev/null
+++ b/src/com/jsyn/unitgen/FunctionEvaluator.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * Aug 26, 2009
+ * com.jsyn.engine.units.TunableFilter.java
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.data.Function;
+import com.jsyn.ports.UnitFunctionPort;
+import com.jsyn.ports.UnitInputPort;
+
+/**
+ * Convert an input value to an output value. The Function is typically implemented by looking up a
+ * value in a DoubleTable. But other implementations of Function can be used. Input typically ranges
+ * from -1.0 to +1.0.
+ *
+ * <pre>
+ * <code>
+ * // A unit that will lookup the function.
+ * FunctionEvaluator shaper = new FunctionEvaluator();
+ * synth.add( shaper );
+ * shaper.start();
+ * // Define a custom function.
+ * Function cuber = new Function()
+ * {
+ * public double evaluate( double x )
+ * {
+ * return x * x * x;
+ * }
+ * };
+ * shaper.function.set(cuber);
+ *
+ * shaper.input.set( 0.5 );
+ * </code>
+ * </pre>
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ * @see Function
+ */
+public class FunctionEvaluator extends UnitFilter {
+ public UnitInputPort amplitude;
+ public UnitFunctionPort function;
+
+ public FunctionEvaluator() {
+ addPort(amplitude = new UnitInputPort("Amplitude", 1.0));
+ addPort(function = new UnitFunctionPort("Function"));
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] inputs = input.getValues();
+ double[] amplitudes = amplitude.getValues();
+ double[] outputs = output.getValues();
+ Function functionObject = function.get();
+
+ for (int i = start; i < limit; i++) {
+ outputs[i] = functionObject.evaluate(inputs[i]) * amplitudes[i];
+ }
+
+ }
+}
diff --git a/src/com/jsyn/unitgen/FunctionOscillator.java b/src/com/jsyn/unitgen/FunctionOscillator.java
new file mode 100644
index 0000000..30d32d5
--- /dev/null
+++ b/src/com/jsyn/unitgen/FunctionOscillator.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.data.Function;
+import com.jsyn.ports.UnitFunctionPort;
+
+/**
+ * Oscillator that uses a Function object to define the waveform. Note that a DoubleTable can be
+ * used as the Function.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ */
+public class FunctionOscillator extends UnitOscillator {
+ public UnitFunctionPort function;
+
+ public FunctionOscillator() {
+ addPort(function = new UnitFunctionPort("Function"));
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] frequencies = frequency.getValues();
+ double[] amplitudes = amplitude.getValues();
+ double[] outputs = output.getValues();
+
+ Function functionObject = function.get();
+
+ // Variables have a single value.
+ double currentPhase = phase.getValue();
+
+ for (int i = start; i < limit; i++) {
+ // Generate sawtooth phasor to provide phase for function lookup.
+ double phaseIncrement = convertFrequencyToPhaseIncrement(frequencies[i]);
+ currentPhase = incrementWrapPhase(currentPhase, phaseIncrement);
+ double value = functionObject.evaluate(currentPhase);
+ outputs[i] = value * amplitudes[i];
+ }
+
+ // Value needs to be saved for next time.
+ phase.setValue(currentPhase);
+ }
+
+}
diff --git a/src/com/jsyn/unitgen/Grain.java b/src/com/jsyn/unitgen/Grain.java
new file mode 100644
index 0000000..812061c
--- /dev/null
+++ b/src/com/jsyn/unitgen/Grain.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2011 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+/**
+ * A single Grain that is normally created and controlled by a GrainFarm.
+ *
+ * @author Phil Burk (C) 2011 Mobileer Inc
+ */
+public class Grain implements GrainEnvelope {
+ private double frameRate;
+ private double amplitude = 1.0;
+
+ private GrainSource source;
+ private GrainEnvelope envelope;
+
+ public Grain(GrainSource source, GrainEnvelope envelope) {
+ this.source = source;
+ this.envelope = envelope;
+ }
+
+ @Override
+ public double next() {
+ if (envelope.hasMoreValues()) {
+ double env = envelope.next();
+ return source.next() * env * amplitude;
+ } else {
+ return 0.0;
+ }
+ }
+
+ @Override
+ public boolean hasMoreValues() {
+ return envelope.hasMoreValues();
+ }
+
+ @Override
+ public void reset() {
+ source.reset();
+ envelope.reset();
+ }
+
+ public void setRate(double rate) {
+ source.setRate(rate);
+ }
+
+ @Override
+ public void setDuration(double duration) {
+ envelope.setDuration(duration);
+ }
+
+ @Override
+ public double getFrameRate() {
+ return frameRate;
+ }
+
+ @Override
+ public void setFrameRate(double frameRate) {
+ this.frameRate = frameRate;
+ source.setFrameRate(frameRate);
+ envelope.setFrameRate(frameRate);
+ }
+
+ public double getAmplitude() {
+ return amplitude;
+ }
+
+ public void setAmplitude(double amplitude) {
+ this.amplitude = amplitude;
+ }
+
+ public GrainSource getSource() {
+ return source;
+ }
+}
diff --git a/src/com/jsyn/unitgen/GrainCommon.java b/src/com/jsyn/unitgen/GrainCommon.java
new file mode 100644
index 0000000..a7a04fc
--- /dev/null
+++ b/src/com/jsyn/unitgen/GrainCommon.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+public class GrainCommon {
+ protected double frameRate;
+
+ public double getFrameRate() {
+ return frameRate;
+ }
+
+ public void setFrameRate(double frameRate) {
+ this.frameRate = frameRate;
+ }
+
+ public void reset() {
+ }
+}
diff --git a/src/com/jsyn/unitgen/GrainEnvelope.java b/src/com/jsyn/unitgen/GrainEnvelope.java
new file mode 100644
index 0000000..e6ff24c
--- /dev/null
+++ b/src/com/jsyn/unitgen/GrainEnvelope.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2011 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+/**
+ * This envelope should start at 0.0, go up to 1.0 and then return to 0.0 in duration time.
+ *
+ * @author Phil Burk (C) 2011 Mobileer Inc
+ */
+public interface GrainEnvelope {
+
+ double getFrameRate();
+
+ void setFrameRate(double frameRate);
+
+ /**
+ * @return next amplitude value of envelope
+ */
+ double next();
+
+ /**
+ * Are there any more values to be generated in the envelope?
+ *
+ * @return true if more
+ */
+ boolean hasMoreValues();
+
+ /**
+ * Prepare to start a new envelope.
+ */
+ void reset();
+
+ /**
+ * @param duration in seconds.
+ */
+ void setDuration(double duration);
+
+}
diff --git a/src/com/jsyn/unitgen/GrainFarm.java b/src/com/jsyn/unitgen/GrainFarm.java
new file mode 100644
index 0000000..7cff6f1
--- /dev/null
+++ b/src/com/jsyn/unitgen/GrainFarm.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright 2011 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.ports.UnitOutputPort;
+import com.jsyn.util.PseudoRandom;
+
+/**
+ * A unit generator that generates a cloud of sound using multiple Grains. Special thanks to my
+ * friend Ross Bencina for his excellent article on Granular Synthesis. Several of his ideas are
+ * reflected in this architecture. "Implementing Real-Time Granular Synthesis" by Ross Bencina,
+ * Audio Anecdotes III, 2001. <code><pre>
+ synth.add( sampleGrainFarm = new GrainFarm() );
+ grainFarm.allocate( NUM_GRAINS );
+</pre><code>
+ *
+ * @author Phil Burk (C) 2011 Mobileer Inc
+ * @see Grain
+ * @see GrainSourceSine
+ * @see RaisedCosineEnvelope
+ */
+public class GrainFarm extends UnitGenerator implements UnitSource {
+ /** A scaler for playback rate. Nominally 1.0. */
+ public UnitInputPort rate;
+ public UnitInputPort rateRange;
+ public UnitInputPort amplitude;
+ public UnitInputPort amplitudeRange;
+ public UnitInputPort density;
+ public UnitInputPort duration;
+ public UnitInputPort durationRange;
+ public UnitOutputPort output;
+
+ PseudoRandom randomizer;
+ private GrainState[] states;
+ private double countScaler = 1.0;
+ private GrainScheduler scheduler = new StochasticGrainScheduler();
+
+ public GrainFarm() {
+ randomizer = new PseudoRandom();
+ addPort(rate = new UnitInputPort("Rate", 1.0));
+ addPort(amplitude = new UnitInputPort("Amplitude", 1.0));
+ addPort(duration = new UnitInputPort("Duration", 0.01));
+ addPort(rateRange = new UnitInputPort("RateRange", 0.0));
+ addPort(amplitudeRange = new UnitInputPort("AmplitudeRange", 0.0));
+ addPort(durationRange = new UnitInputPort("DurationRange", 0.0));
+ addPort(density = new UnitInputPort("Density", 0.1));
+ addPort(output = new UnitOutputPort());
+ }
+
+ private class GrainState {
+ Grain grain;
+ int countdown;
+ double lastDuration;
+ final static int STATE_IDLE = 0;
+ final static int STATE_GAP = 1;
+ final static int STATE_RUNNING = 2;
+ int state = STATE_IDLE;
+ private double gapError;
+
+ public double next(int i) {
+ double output = 0.0;
+ if (state == STATE_RUNNING) {
+ if (grain.hasMoreValues()) {
+ output = grain.next();
+ } else {
+ startGap(i);
+ }
+ } else if (state == STATE_GAP) {
+ if (countdown > 0) {
+ countdown -= 1;
+ } else if (countdown == 0) {
+ state = STATE_RUNNING;
+ grain.reset();
+
+ setupGrain(grain, i);
+
+ double dur = nextDuration(i);
+ grain.setDuration(dur);
+ }
+ } else if (state == STATE_IDLE) {
+ nextDuration(i); // initialize lastDuration
+ startGap(i);
+ }
+ return output;
+ }
+
+ private double nextDuration(int i) {
+ double dur = duration.getValues()[i];
+ dur = scheduler.nextDuration(dur);
+ lastDuration = dur;
+ return dur;
+ }
+
+ private void startGap(int i) {
+ state = STATE_GAP;
+ double dens = density.getValues()[i];
+ double gap = scheduler.nextGap(lastDuration, dens) * getFrameRate();
+ gap += gapError;
+ countdown = (int) gap;
+ gapError = gap - countdown;
+ }
+ }
+
+ public void setGrainArray(Grain[] grains) {
+ countScaler = 1.0 / grains.length;
+ states = new GrainState[grains.length];
+ for (int i = 0; i < states.length; i++) {
+ states[i] = new GrainState();
+ states[i].grain = grains[i];
+ grains[i].setFrameRate(getSynthesisEngine().getFrameRate());
+ }
+ }
+
+ public void setupGrain(Grain grain, int i) {
+ double temp = rate.getValues()[i] * calculateOctaveScaler(rateRange.getValues()[i]);
+ grain.setRate(temp);
+
+ // Scale the amplitude range so that we never go above
+ // original amplitude.
+ double base = amplitude.getValues()[i];
+ double offset = base * Math.random() * amplitudeRange.getValues()[i];
+ grain.setAmplitude(base - offset);
+ }
+
+ public void allocate(int numGrains) {
+ Grain[] grainArray = new Grain[numGrains];
+ for (int i = 0; i < numGrains; i++) {
+ Grain grain = new Grain(new GrainSourceSine(), new RaisedCosineEnvelope());
+ grainArray[i] = grain;
+ }
+ setGrainArray(grainArray);
+ }
+
+ @Override
+ public UnitOutputPort getOutput() {
+ return output;
+ }
+
+ private double calculateOctaveScaler(double rangeValue) {
+ double octaveRange = 0.5 * randomizer.nextRandomDouble() * rangeValue;
+ return Math.pow(2.0, octaveRange);
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] outputs = output.getValues();
+ double[] amplitudes = amplitude.getValues();
+ // double frp = getSynthesisEngine().getFramePeriod();
+ for (int i = start; i < limit; i++) {
+ double result = 0.0;
+
+ // Mix the grains together.
+ for (GrainState grainState : states) {
+ result += grainState.next(i);
+ }
+
+ outputs[i] = result * amplitudes[i] * countScaler;
+ }
+
+ }
+}
diff --git a/src/com/jsyn/unitgen/GrainScheduler.java b/src/com/jsyn/unitgen/GrainScheduler.java
new file mode 100644
index 0000000..df9c25e
--- /dev/null
+++ b/src/com/jsyn/unitgen/GrainScheduler.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2011 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+/**
+ * Defines a class that can schedule the execution of Grains in a GrainFarm. This is mostly for
+ * internal use.
+ *
+ * @author Phil Burk (C) 2011 Mobileer Inc
+ */
+public interface GrainScheduler {
+
+ /**
+ * Calculate time in seconds for the next gap between grains.
+ *
+ * @param duration
+ * @param density
+ * @return seconds before next grain
+ */
+ double nextGap(double duration, double density);
+
+ /**
+ * Calculate duration in seconds for the next grains.
+ *
+ * @param suggestedDuration
+ * @return duration of grain seconds
+ */
+ double nextDuration(double suggestedDuration);
+
+}
diff --git a/src/com/jsyn/unitgen/GrainSource.java b/src/com/jsyn/unitgen/GrainSource.java
new file mode 100644
index 0000000..1d5c522
--- /dev/null
+++ b/src/com/jsyn/unitgen/GrainSource.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2011 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+/**
+ * Defines classes that can provide the signal inside a Grain.
+ *
+ * @author Phil Burk (C) 2011 Mobileer Inc
+ */
+public interface GrainSource {
+ double getFrameRate();
+
+ void setFrameRate(double frameRate);
+
+ /** Generate one more value or the source signal. */
+ double next();
+
+ /** Reset the internal phase of the grain. */
+ void reset();
+
+ void setRate(double rate);
+}
diff --git a/src/com/jsyn/unitgen/GrainSourceSine.java b/src/com/jsyn/unitgen/GrainSourceSine.java
new file mode 100644
index 0000000..0af9cbd
--- /dev/null
+++ b/src/com/jsyn/unitgen/GrainSourceSine.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2011 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+/**
+ * A simple sine wave generator for a Grain. This uses the same fast Taylor expansion that the
+ * SineOscillator uses.
+ *
+ * @author Phil Burk (C) 2011 Mobileer Inc
+ */
+public class GrainSourceSine extends GrainCommon implements GrainSource {
+ protected double phase;
+ protected double phaseIncrement;
+
+ public GrainSourceSine() {
+ setRate(1.0);
+ }
+
+ public void setPhaseIncrement(double phaseIncrement) {
+ this.phaseIncrement = phaseIncrement;
+ }
+
+ @Override
+ public double next() {
+ phase += phaseIncrement;
+ if (phase > 1.0) {
+ phase -= 2.0;
+ }
+ return SineOscillator.fastSin(phase);
+ }
+
+ @Override
+ public void setRate(double rate) {
+ setPhaseIncrement(rate * 0.1 / Math.PI);
+ }
+
+}
diff --git a/src/com/jsyn/unitgen/IFFT.java b/src/com/jsyn/unitgen/IFFT.java
new file mode 100644
index 0000000..307acd2
--- /dev/null
+++ b/src/com/jsyn/unitgen/IFFT.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2013 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+/**
+ * Periodically transform the complex input spectrum using an IFFT to a complex signal stream.
+ *
+ * @author Phil Burk (C) 2013 Mobileer Inc
+ * @version 016
+ * @see FFT
+ */
+public class IFFT extends FFTBase {
+
+ public IFFT() {
+ super();
+ }
+
+ @Override
+ protected int getSign() {
+ return -1; // -1 for IFFT
+ }
+}
diff --git a/src/com/jsyn/unitgen/ImpulseOscillator.java b/src/com/jsyn/unitgen/ImpulseOscillator.java
new file mode 100644
index 0000000..8c676f3
--- /dev/null
+++ b/src/com/jsyn/unitgen/ImpulseOscillator.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+/**
+ * Narrow impulse oscillator. An impulse is only one sample wide. It is useful for pinging filters
+ * or generating an "impulse response".
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ */
+public class ImpulseOscillator extends UnitOscillator {
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] frequencies = frequency.getValues();
+ double[] amplitudes = amplitude.getValues();
+ double[] outputs = output.getValues();
+
+ // Variables have a single value.
+ double currentPhase = phase.getValue();
+
+ double inverseNyquist = synthesisEngine.getInverseNyquist();
+
+ for (int i = start; i < limit; i++) {
+ /* Generate sawtooth phasor to provide phase for impulse generation. */
+ double phaseIncrement = frequencies[i] * inverseNyquist;
+ currentPhase += phaseIncrement;
+
+ double ampl = amplitudes[i];
+ double result = 0.0;
+ if (currentPhase >= 1.0) {
+ currentPhase -= 2.0;
+ result = ampl;
+ } else if (currentPhase < -1.0) {
+ currentPhase += 2.0;
+ result = ampl;
+ }
+ outputs[i] = result;
+ }
+
+ // Value needs to be saved for next time.
+ phase.setValue(currentPhase);
+ }
+
+}
diff --git a/src/com/jsyn/unitgen/ImpulseOscillatorBL.java b/src/com/jsyn/unitgen/ImpulseOscillatorBL.java
new file mode 100644
index 0000000..23686b8
--- /dev/null
+++ b/src/com/jsyn/unitgen/ImpulseOscillatorBL.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.engine.MultiTable;
+
+/**
+ * Impulse oscillator created by differentiating a sawtoothBL. A band limited impulse is very narrow
+ * but is slightly wider than one sample.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ */
+public class ImpulseOscillatorBL extends SawtoothOscillatorBL {
+ private double previous = 0.0;
+
+ @Override
+ protected double generateBL(MultiTable multiTable, double currentPhase,
+ double positivePhaseIncrement, double flevel, int i) {
+ double saw = multiTable.calculateSawtooth(currentPhase, positivePhaseIncrement, flevel);
+ double result = previous - saw;
+ previous = saw;
+ return result;
+ }
+
+}
diff --git a/src/com/jsyn/unitgen/Integrate.java b/src/com/jsyn/unitgen/Integrate.java
new file mode 100644
index 0000000..b5beea9
--- /dev/null
+++ b/src/com/jsyn/unitgen/Integrate.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 1997 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.ports.UnitOutputPort;
+
+/**
+ * IntegrateUnit unit.
+ * <P>
+ * Output accumulated sum of the input signal. This can be used to transform one signal into
+ * another, or to generate ramps between the limits by setting the input signal positive or
+ * negative. For a "leaky integrator" use a FilterOnePoleOneZero.
+ * <P>
+ *
+ * <pre>
+ * output = output + input;
+ * if (output &lt; lowerLimit)
+ * output = lowerLimit;
+ * else if (output &gt; upperLimit)
+ * output = upperLimit;
+ * </pre>
+ *
+ * @author (C) 1997-2011 Phil Burk, Mobileer Inc
+ * @see FilterOnePoleOneZero
+ */
+public class Integrate extends UnitGenerator {
+ public UnitInputPort input;
+ /**
+ * Output will be stopped internally from going below this value. Default is -1.0.
+ */
+ public UnitInputPort lowerLimit;
+ /**
+ * Output will be stopped internally from going above this value. Default is +1.0.
+ */
+ public UnitInputPort upperLimit;
+ public UnitOutputPort output;
+
+ private double accum;
+
+ /* Define Unit Ports used by connect() and set(). */
+ public Integrate() {
+ addPort(input = new UnitInputPort("Input"));
+ addPort(lowerLimit = new UnitInputPort("LowerLimit", -1.0));
+ addPort(upperLimit = new UnitInputPort("UpperLimit", 1.0));
+ addPort(output = new UnitOutputPort("Output"));
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] inputs = input.getValues();
+ double[] lowerLimits = lowerLimit.getValues();
+ double[] upperLimits = upperLimit.getValues();
+ double[] outputs = output.getValues();
+
+ for (int i = start; i < limit; i++) {
+ accum += inputs[i]; // INTEGRATE
+
+ // clip to limits
+ if (accum > upperLimits[i])
+ accum = upperLimits[i];
+ else if (accum < lowerLimits[i])
+ accum = lowerLimits[i];
+
+ outputs[i] = accum;
+ }
+ }
+}
diff --git a/src/com/jsyn/unitgen/InterpolatingDelay.java b/src/com/jsyn/unitgen/InterpolatingDelay.java
new file mode 100644
index 0000000..24de4f9
--- /dev/null
+++ b/src/com/jsyn/unitgen/InterpolatingDelay.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 1997 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitInputPort;
+
+/**
+ * InterpolatingDelay
+ * <P>
+ * InterpolatingDelay provides a variable time delay with an input and output. The internal data
+ * format is 32-bit floating point. The amount of delay can be varied from 0.0 to a time in seconds
+ * corresponding to the numFrames allocated. The fractional delay values are calculated by linearly
+ * interpolating between adjacent values in the delay line.
+ * <P>
+ * This unit can be used to implement time varying delay effects such as a flanger or a chorus. It
+ * can also be used to implement physical models of acoustic instruments, or other tunable delay
+ * based resonant systems.
+ * <P>
+ *
+ * @author (C) 1997-2011 Phil Burk, Mobileer Inc
+ * @see Delay
+ */
+
+public class InterpolatingDelay extends UnitFilter {
+ /**
+ * Delay time in seconds. This value will converted to frames and clipped between zero and the
+ * numFrames value passed to allocate(). The minimum and default delay time is 0.0.
+ */
+ public UnitInputPort delay;
+
+ private float[] buffer;
+ private int cursor;
+ private int numFrames;
+
+ public InterpolatingDelay() {
+ addPort(delay = new UnitInputPort("Delay"));
+ }
+
+ /**
+ * Allocate memory for the delay buffer. For a 2 second delay at 44100 Hz sample rate you will
+ * need at least 88200 samples.
+ *
+ * @param numFrames size of the float array to hold the delayed samples
+ */
+ public void allocate(int numFrames) {
+ this.numFrames = numFrames;
+ // Allocate extra frame for guard point to speed up interpolation.
+ buffer = new float[numFrames + 1];
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] inputs = input.getValues();
+ double[] outputs = output.getValues();
+ double[] delays = delay.getValues();
+
+ for (int i = start; i < limit; i++) {
+ // This should be at the beginning of the loop
+ // because the guard point should == buffer[0].
+ if (cursor == numFrames) {
+ // Write guard point! Must allocate one extra sample.
+ buffer[numFrames] = (float) inputs[i];
+ cursor = 0;
+ }
+
+ buffer[cursor] = (float) inputs[i];
+
+ /* Convert delay time to a clipped frame offset. */
+ double delayFrames = delays[i] * getFrameRate();
+
+ // Clip to zero delay.
+ if (delayFrames <= 0.0) {
+ outputs[i] = buffer[cursor];
+ } else {
+ // Clip to maximum delay.
+ if (delayFrames >= numFrames) {
+ delayFrames = numFrames - 1;
+ }
+
+ // Calculate fractional index into delay buffer.
+ double readIndex = cursor - delayFrames;
+ if (readIndex < 0.0) {
+ readIndex += numFrames;
+ }
+ // setup for interpolation.
+ // We know readIndex is > 0 so we do not need to call floor().
+ int iReadIndex = (int) readIndex;
+ double frac = readIndex - iReadIndex;
+
+ // Get adjacent values relying on guard point to prevent overflow.
+ double val0 = buffer[iReadIndex];
+ double val1 = buffer[iReadIndex + 1];
+
+ // Interpolate new value.
+ outputs[i] = val0 + (frac * (val1 - val0));
+ }
+
+ cursor += 1;
+ }
+
+ }
+
+}
diff --git a/src/com/jsyn/unitgen/Latch.java b/src/com/jsyn/unitgen/Latch.java
new file mode 100644
index 0000000..5818101
--- /dev/null
+++ b/src/com/jsyn/unitgen/Latch.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 1997 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitInputPort;
+
+/**
+ * Latch or hold an input value.
+ * <P>
+ * Pass a value unchanged if gate true, otherwise output held value.
+ * <P>
+ * output = ( gate > 0.0 ) ? input : previous_output;
+ *
+ * @author (C) 1997-2010 Phil Burk, Mobileer Inc
+ * @see EdgeDetector
+ */
+public class Latch extends UnitFilter {
+ public UnitInputPort gate;
+ private double held;
+
+ /* Define Unit Ports used by connect() and set(). */
+ public Latch() {
+ addPort(gate = new UnitInputPort("Gate"));
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] inputs = input.getValues();
+ double[] gates = gate.getValues();
+ double[] outputs = output.getValues();
+
+ for (int i = start; i < limit; i++) {
+ if (gates[i] > 0.0) {
+ held = inputs[i];
+ }
+ outputs[i] = held;
+ }
+ }
+}
diff --git a/src/com/jsyn/unitgen/LatchZeroCrossing.java b/src/com/jsyn/unitgen/LatchZeroCrossing.java
new file mode 100644
index 0000000..9e6c011
--- /dev/null
+++ b/src/com/jsyn/unitgen/LatchZeroCrossing.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.ports.UnitOutputPort;
+
+/**
+ * Latches when input crosses zero.
+ * <P>
+ * Pass a value unchanged if gate true, otherwise pass input unchanged until input crosses zero then
+ * output zero. This can be used to turn off a sound at a zero crossing so there is no pop.
+ * <P>
+ *
+ * @author (C) 2010 Phil Burk, Mobileer Inc
+ * @see Latch
+ * @see Minimum
+ */
+public class LatchZeroCrossing extends UnitGenerator {
+ public UnitInputPort input;
+ public UnitInputPort gate;
+ public UnitOutputPort output;
+ private double held;
+ private boolean crossed;
+
+ /* Define Unit Ports used by connect() and set(). */
+ public LatchZeroCrossing() {
+ addPort(input = new UnitInputPort("Input"));
+ addPort(gate = new UnitInputPort("Gate", 1.0));
+ addPort(output = new UnitOutputPort("Output"));
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] inputs = input.getValues();
+ double[] gates = gate.getValues();
+ double[] outputs = output.getValues();
+
+ for (int i = start; i < limit; i++) {
+ double current = inputs[i];
+ if (gates[i] > 0.0) {
+ held = current;
+ crossed = false;
+ } else {
+ // If we haven't already seen a zero crossing then look for one.
+ if (!crossed) {
+ if ((held * current) <= 0.0) {
+ held = 0.0;
+ crossed = true;
+ } else {
+ held = current;
+ }
+ }
+ }
+ outputs[i] = held;
+ }
+ }
+}
diff --git a/src/com/jsyn/unitgen/LineIn.java b/src/com/jsyn/unitgen/LineIn.java
new file mode 100644
index 0000000..500b55c
--- /dev/null
+++ b/src/com/jsyn/unitgen/LineIn.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.Synthesizer;
+import com.jsyn.ports.UnitOutputPort;
+
+/**
+ * External audio input is sent to the output of this unit. The LineIn provides a stereo signal
+ * containing channels 0 and 1. For LineIn to work you must call the Synthesizer start() method with
+ * numInputChannels > 0.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ * @see Synthesizer
+ * @see ChannelIn
+ * @see LineOut
+ */
+public class LineIn extends UnitGenerator {
+ public UnitOutputPort output;
+
+ public LineIn() {
+ addPort(output = new UnitOutputPort(2, "Output"));
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] outputs0 = output.getValues(0);
+ double[] outputs1 = output.getValues(1);
+ double[] buffer0 = synthesisEngine.getInputBuffer(0);
+ double[] buffer1 = synthesisEngine.getInputBuffer(1);
+ for (int i = start; i < limit; i++) {
+ outputs0[i] = buffer0[i];
+ outputs1[i] = buffer1[i];
+ }
+ }
+
+}
diff --git a/src/com/jsyn/unitgen/LineOut.java b/src/com/jsyn/unitgen/LineOut.java
new file mode 100644
index 0000000..489033e
--- /dev/null
+++ b/src/com/jsyn/unitgen/LineOut.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitInputPort;
+
+/**
+ * Input audio is sent to the external audio output device.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ */
+public class LineOut extends UnitGenerator implements UnitSink {
+ public UnitInputPort input;
+
+ public LineOut() {
+ addPort(input = new UnitInputPort(2, "Input"));
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] inputs0 = input.getValues(0);
+ double[] inputs1 = input.getValues(1);
+ double[] buffer0 = synthesisEngine.getOutputBuffer(0);
+ double[] buffer1 = synthesisEngine.getOutputBuffer(1);
+ for (int i = start; i < limit; i++) {
+ buffer0[i] += inputs0[i];
+ buffer1[i] += inputs1[i];
+ }
+ }
+
+ @Override
+ public UnitInputPort getInput() {
+ return input;
+ }
+}
diff --git a/src/com/jsyn/unitgen/LinearRamp.java b/src/com/jsyn/unitgen/LinearRamp.java
new file mode 100644
index 0000000..438adf6
--- /dev/null
+++ b/src/com/jsyn/unitgen/LinearRamp.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 1997 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.ports.UnitVariablePort;
+
+/**
+ * Output approaches Input linearly.
+ * <P>
+ * When you change the value of the input port, the ramp will start changing from its current output
+ * value toward the value of input. An internal phase value will go from 0.0 to 1.0 at a rate
+ * controlled by time. When the internal phase reaches 1.0, the output will equal input.
+ * <P>
+ *
+ * @author (C) 1997 Phil Burk, SoftSynth.com
+ * @see ExponentialRamp
+ * @see AsymptoticRamp
+ * @see ContinuousRamp
+ */
+public class LinearRamp extends UnitFilter {
+ /** Time in seconds to get to the input value. */
+ public UnitInputPort time;
+ public UnitVariablePort current;
+
+ private double source;
+ private double phase;
+ private double target;
+ private double timeHeld = 0.0;
+ private double rate = 1.0;
+
+ public LinearRamp() {
+ addPort(time = new UnitInputPort("Time"));
+ addPort(current = new UnitVariablePort("Current"));
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] outputs = output.getValues();
+ double currentInput = input.getValues()[0];
+ double currentTime = time.getValues()[0];
+ double currentValue = current.getValue();
+
+ if (currentTime != timeHeld) {
+ rate = convertTimeToRate(currentTime);
+ timeHeld = currentTime;
+ }
+
+ /* If input has changed, start new segment */
+ if (currentInput != target) /*
+ * Equality check is OK because we set them exactly equal below.
+ */
+ {
+ source = currentValue;
+ phase = 0.0;
+ target = currentInput;
+ }
+
+ for (int i = start; i < limit; i++) {
+ if (phase < 1.0) {
+ /* Interpolate current. */
+ currentValue = source + (phase * (target - source));
+ phase += rate;
+ } else {
+ currentValue = target;
+ }
+ outputs[i] = currentValue;
+ }
+
+ current.setValue(currentValue);
+ }
+}
diff --git a/src/com/jsyn/unitgen/Maximum.java b/src/com/jsyn/unitgen/Maximum.java
new file mode 100644
index 0000000..296e5da
--- /dev/null
+++ b/src/com/jsyn/unitgen/Maximum.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+/**
+ *
+ Output largest of inputA or inputB.
+ *
+ * <pre>
+ * output = (inputA &gt; InputB) ? inputA : InputB;
+ * </pre>
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ * @version 016
+ * @see Minimum
+ */
+public class Maximum extends UnitBinaryOperator {
+ @Override
+ public void generate(int start, int limit) {
+ double[] aValues = inputA.getValues();
+ double[] bValues = inputB.getValues();
+ double[] outputs = output.getValues();
+
+ for (int i = start; i < limit; i++) {
+ outputs[i] = (aValues[i] > bValues[i]) ? aValues[i] : bValues[i];
+ }
+ }
+}
diff --git a/src/com/jsyn/unitgen/Minimum.java b/src/com/jsyn/unitgen/Minimum.java
new file mode 100644
index 0000000..046387e
--- /dev/null
+++ b/src/com/jsyn/unitgen/Minimum.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+/**
+ *
+ Output smallest of inputA or inputB.
+ *
+ * <pre>
+ * output = (inputA &lt; InputB) ? inputA : InputB;
+ * </pre>
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ * @version 016
+ * @see Maximum
+ */
+public class Minimum extends UnitBinaryOperator {
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] aValues = inputA.getValues();
+ double[] bValues = inputB.getValues();
+ double[] outputs = output.getValues();
+
+ for (int i = start; i < limit; i++) {
+ outputs[i] = (aValues[i] < bValues[i]) ? aValues[i] : bValues[i];
+ }
+ }
+}
diff --git a/src/com/jsyn/unitgen/MixerMono.java b/src/com/jsyn/unitgen/MixerMono.java
new file mode 100644
index 0000000..f4c7d7d
--- /dev/null
+++ b/src/com/jsyn/unitgen/MixerMono.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2014 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.ports.UnitOutputPort;
+
+/**
+ * Multi-channel mixer with mono output and master amplitude.
+ *
+ * @author Phil Burk (C) 2014 Mobileer Inc
+ * @see MixerMonoRamped
+ * @see MixerStereo
+ */
+public class MixerMono extends UnitGenerator implements UnitSink, UnitSource {
+ public UnitInputPort input;
+ /**
+ * Linear gain for the corresponding input.
+ */
+ public UnitInputPort gain;
+ /**
+ * Master gain control.
+ */
+ public UnitInputPort amplitude;
+ public UnitOutputPort output;
+
+ public MixerMono(int numInputs) {
+ addPort(input = new UnitInputPort(numInputs, "Input"));
+ addPort(gain = new UnitInputPort(numInputs, "Gain", 1.0));
+ addPort(amplitude = new UnitInputPort("Amplitude", 1.0));
+ addPort(output = new UnitOutputPort(getNumOutputs(), "Output"));
+ }
+
+ public int getNumOutputs() {
+ return 1;
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] amplitudes = amplitude.getValues(0);
+ double[] outputs = output.getValues(0);
+ for (int i = start; i < limit; i++) {
+ double sum = 0;
+ for (int n = 0; n < input.getNumParts(); n++) {
+ double[] inputs = input.getValues(n);
+ double[] gains = gain.getValues(n);
+ sum += inputs[i] * gains[i];
+ }
+ outputs[i] = sum * amplitudes[i];
+ }
+ }
+
+ @Override
+ public UnitInputPort getInput() {
+ return input;
+ }
+
+ @Override
+ public UnitOutputPort getOutput() {
+ return output;
+ }
+
+}
diff --git a/src/com/jsyn/unitgen/MixerMonoRamped.java b/src/com/jsyn/unitgen/MixerMonoRamped.java
new file mode 100644
index 0000000..30f5342
--- /dev/null
+++ b/src/com/jsyn/unitgen/MixerMonoRamped.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2014 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+/**
+ * Similar to MixerMono but the gain and amplitude ports are smoothed using short linear ramps. So
+ * you can control them with knobs and not hear any zipper noise.
+ *
+ * @author Phil Burk (C) 2014 Mobileer Inc
+ */
+public class MixerMonoRamped extends MixerMono {
+ private Unzipper[] unzippers;
+ private Unzipper amplitudeUnzipper;
+
+ public MixerMonoRamped(int numInputs) {
+ super(numInputs);
+ unzippers = new Unzipper[numInputs];
+ for (int i = 0; i < numInputs; i++) {
+ unzippers[i] = new Unzipper();
+ }
+ amplitudeUnzipper = new Unzipper();
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] amplitudes = amplitude.getValues(0);
+ double[] outputs = output.getValues(0);
+ for (int i = start; i < limit; i++) {
+ double sum = 0;
+ for (int n = 0; n < input.getNumParts(); n++) {
+ double[] inputs = input.getValues(n);
+ double[] gains = gain.getValues(n);
+ double smoothGain = unzippers[n].smooth(gains[i]);
+ sum += inputs[i] * smoothGain;
+ }
+ outputs[i] = sum * amplitudeUnzipper.smooth(amplitudes[i]);
+ }
+ }
+
+}
diff --git a/src/com/jsyn/unitgen/MixerStereo.java b/src/com/jsyn/unitgen/MixerStereo.java
new file mode 100644
index 0000000..218546e
--- /dev/null
+++ b/src/com/jsyn/unitgen/MixerStereo.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2014 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitInputPort;
+
+/**
+ * Mixer with monophonic inputs and two channels of output. Each signal can be panned left or right
+ * using an equal power curve. The "left" signal will be on output part zero. The "right" signal
+ * will be on output part one.
+ *
+ * @author Phil Burk (C) 2014 Mobileer Inc
+ * @see MixerMono
+ * @see MixerStereoRamped
+ */
+public class MixerStereo extends MixerMono {
+ /**
+ * Set to -1.0 for all left channel, 0.0 for center, or +1.0 for all right. Or anywhere in
+ * between.
+ */
+ public UnitInputPort pan;
+ protected PanTracker[] panTrackers;
+
+ static class PanTracker {
+ double previousPan = Double.MAX_VALUE; // so we update immediately
+ double leftGain;
+ double rightGain;
+
+ public void update(double pan) {
+ if (pan != previousPan) {
+ // fastSine range is -1.0 to +1.0 for full cycle.
+ // We want a quarter cycle. So map -1.0 to +1.0 into 0.0 to 0.5
+ double phase = pan * 0.25 + 0.25;
+ leftGain = SineOscillator.fastSin(0.5 - phase);
+ rightGain = SineOscillator.fastSin(phase);
+ previousPan = pan;
+ }
+ }
+ }
+
+ public MixerStereo(int numInputs) {
+ super(numInputs);
+ addPort(pan = new UnitInputPort(numInputs, "Pan"));
+ pan.setup(-1.0, 0.0, 1.0);
+ panTrackers = new PanTracker[numInputs];
+ for (int i = 0; i < numInputs; i++) {
+ panTrackers[i] = new PanTracker();
+ }
+ }
+
+ @Override
+ public int getNumOutputs() {
+ return 2;
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] amplitudes = amplitude.getValues(0);
+ double[] outputs0 = output.getValues(0);
+ double[] outputs1 = output.getValues(1);
+ for (int i = start; i < limit; i++) {
+ double sum0 = 0.0;
+ double sum1 = 0.0;
+ for (int n = 0; n < input.getNumParts(); n++) {
+ double[] inputs = input.getValues(n);
+ double[] gains = gain.getValues(n);
+ double[] pans = pan.getValues(n);
+ PanTracker panTracker = panTrackers[n];
+ panTracker.update(pans[i]);
+ double scaledInput = inputs[i] * gains[i];
+ sum0 += scaledInput * panTracker.leftGain;
+ sum1 += scaledInput * panTracker.rightGain;
+ }
+ double amp = amplitudes[i];
+ outputs0[i] = sum0 * amp;
+ outputs1[i] = sum1 * amp;
+ }
+ }
+
+}
diff --git a/src/com/jsyn/unitgen/MixerStereoRamped.java b/src/com/jsyn/unitgen/MixerStereoRamped.java
new file mode 100644
index 0000000..6f3bfcc
--- /dev/null
+++ b/src/com/jsyn/unitgen/MixerStereoRamped.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2014 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+/**
+ * Similar to MixerStereo but the gain, pan and amplitude ports are smoothed using short linear
+ * ramps. So you can control them with knobs and not hear any zipper noise.
+ *
+ * @author Phil Burk (C) 2014 Mobileer Inc
+ */
+public class MixerStereoRamped extends MixerStereo {
+ private Unzipper[] gainUnzippers;
+ private Unzipper[] panUnzippers;
+ private Unzipper amplitudeUnzipper;
+
+ public MixerStereoRamped(int numInputs) {
+ super(numInputs);
+ gainUnzippers = new Unzipper[numInputs];
+ for (int i = 0; i < numInputs; i++) {
+ gainUnzippers[i] = new Unzipper();
+ }
+ panUnzippers = new Unzipper[numInputs];
+ for (int i = 0; i < numInputs; i++) {
+ panUnzippers[i] = new Unzipper();
+ }
+ amplitudeUnzipper = new Unzipper();
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] amplitudes = amplitude.getValues(0);
+ double[] outputs0 = output.getValues(0);
+ double[] outputs1 = output.getValues(1);
+ for (int i = start; i < limit; i++) {
+ double sum0 = 0;
+ double sum1 = 0;
+ for (int n = 0; n < input.getNumParts(); n++) {
+ double[] inputs = input.getValues(n);
+ double[] gains = gain.getValues(n);
+ double[] pans = pan.getValues(n);
+
+ PanTracker panTracker = panTrackers[n];
+ double smoothPan = panUnzippers[n].smooth(pans[i]);
+ panTracker.update(smoothPan);
+
+ double smoothGain = gainUnzippers[n].smooth(gains[i]);
+ double scaledInput = inputs[i] * smoothGain;
+ sum0 += scaledInput * panTracker.leftGain;
+ sum1 += scaledInput * panTracker.rightGain;
+ }
+ double amp = amplitudeUnzipper.smooth(amplitudes[i]);
+ outputs0[i] = sum0 * amp;
+ outputs1[i] = sum1 * amp;
+ }
+ }
+
+}
diff --git a/src/com/jsyn/unitgen/MonoStreamWriter.java b/src/com/jsyn/unitgen/MonoStreamWriter.java
new file mode 100644
index 0000000..0184766
--- /dev/null
+++ b/src/com/jsyn/unitgen/MonoStreamWriter.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import java.io.IOException;
+
+import com.jsyn.io.AudioOutputStream;
+import com.jsyn.ports.UnitInputPort;
+
+/**
+ * Write one sample per audio frame to an AudioOutputStream with no interpolation.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ */
+public class MonoStreamWriter extends UnitStreamWriter {
+ public MonoStreamWriter() {
+ addPort(input = new UnitInputPort("Input"));
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] inputs = input.getValues();
+ AudioOutputStream output = outputStream;
+ if (output != null) {
+ int count = limit - start;
+ try {
+ output.write(inputs, start, count);
+ } catch (IOException e) {
+ }
+ }
+ }
+
+}
diff --git a/src/com/jsyn/unitgen/Multiply.java b/src/com/jsyn/unitgen/Multiply.java
new file mode 100644
index 0000000..ded7646
--- /dev/null
+++ b/src/com/jsyn/unitgen/Multiply.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.ports.UnitOutputPort;
+
+/**
+ * This unit multiplies its two inputs. <br>
+ *
+ * <pre>
+ * output = inputA * inputB
+ * </pre>
+ *
+ * <br>
+ * Note that some units have an amplitude port, which controls an internal multiply. So you may not
+ * need this unit.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ * @version 016
+ * @see MultiplyAdd
+ * @see Subtract
+ */
+public class Multiply extends UnitBinaryOperator {
+ public Multiply() {
+ }
+
+ /** Connect a to inputA and b to inputB. */
+ public Multiply(UnitOutputPort a, UnitOutputPort b) {
+ a.connect(inputA);
+ b.connect(inputB);
+ }
+
+ /** Connect a to inputA and b to inputB and connect output to c. */
+ public Multiply(UnitOutputPort a, UnitOutputPort b, UnitInputPort c) {
+ this(a, b);
+ output.connect(c);
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] aValues = inputA.getValues();
+ double[] bValues = inputB.getValues();
+ double[] outputs = output.getValues();
+ for (int i = start; i < limit; i++) {
+ outputs[i] = aValues[i] * bValues[i];
+ }
+ }
+
+}
diff --git a/src/com/jsyn/unitgen/MultiplyAdd.java b/src/com/jsyn/unitgen/MultiplyAdd.java
new file mode 100644
index 0000000..adbee6c
--- /dev/null
+++ b/src/com/jsyn/unitgen/MultiplyAdd.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.ports.UnitOutputPort;
+
+/**
+ * <pre>
+ * output = (inputA * inputB) + inputC
+ * </pre>
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ * @version 016
+ * @see Multiply
+ * @see Add
+ */
+public class MultiplyAdd extends UnitGenerator {
+ public UnitInputPort inputA;
+ public UnitInputPort inputB;
+ public UnitInputPort inputC;
+ public UnitOutputPort output;
+
+ /* Define Unit Ports used by connect() and set(). */
+ public MultiplyAdd() {
+ addPort(inputA = new UnitInputPort("InputA"));
+ addPort(inputB = new UnitInputPort("InputB"));
+ addPort(inputC = new UnitInputPort("InputC"));
+ addPort(output = new UnitOutputPort("Output"));
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] aValues = inputA.getValues();
+ double[] bValues = inputB.getValues();
+ double[] cValues = inputC.getValues();
+ double[] outputs = output.getValues();
+ for (int i = start; i < limit; i++) {
+ outputs[i] = (aValues[i] * bValues[i]) + cValues[i];
+ }
+ }
+
+}
diff --git a/src/com/jsyn/unitgen/Pan.java b/src/com/jsyn/unitgen/Pan.java
new file mode 100644
index 0000000..329342a
--- /dev/null
+++ b/src/com/jsyn/unitgen/Pan.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 1997 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.ports.UnitOutputPort;
+
+/**
+ * Pan unit. The profile is constant amplitude and not constant energy.
+ * <P>
+ * Takes an input and pans it between two outputs based on value of pan. When pan is -1, output[0]
+ * is input, and output[1] is zero. When pan is 0, output[0] and output[1] are both input/2. When
+ * pan is +1, output[0] is zero, and output[1] is input.
+ * <P>
+ *
+ * @author (C) 1997 Phil Burk, SoftSynth.com
+ * @see SelectUnit
+ */
+public class Pan extends UnitGenerator {
+ public UnitInputPort input;
+ /**
+ * Pan control varies from -1.0 for full left to +1.0 for full right. Set to 0.0 for center.
+ */
+ public UnitInputPort pan;
+ public UnitOutputPort output;
+
+ public Pan() {
+ addPort(input = new UnitInputPort("Input"));
+ addPort(pan = new UnitInputPort("Pan"));
+ addPort(output = new UnitOutputPort(2, "Output"));
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] inputs = input.getValues();
+ double[] panPtr = pan.getValues();
+ double[] outputs_0 = output.getValues(0);
+ double[] outputs_1 = output.getValues(1);
+
+ for (int i = start; i < limit; i++) {
+ double gainB = (panPtr[i] * 0.5) + 0.5; /*
+ * Scale and offset to 0.0 to 1.0
+ */
+ double gainA = 1.0 - gainB;
+ double inVal = inputs[i];
+ outputs_0[i] = inVal * gainA;
+ outputs_1[i] = inVal * gainB;
+ }
+ }
+}
diff --git a/src/com/jsyn/unitgen/PanControl.java b/src/com/jsyn/unitgen/PanControl.java
new file mode 100644
index 0000000..63bddd8
--- /dev/null
+++ b/src/com/jsyn/unitgen/PanControl.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 1997 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.ports.UnitOutputPort;
+
+/**
+ * PanControl unit.
+ * <P>
+ * Generates control signals that can be used to control a mixer or the amplitude ports of two
+ * units.
+ *
+ * <PRE>
+ * temp = (pan * 0.5) + 0.5;
+ * output[0] = temp;
+ * output[1] = 1.0 - temp;
+ * </PRE>
+ * <P>
+ *
+ * @author (C) 1997-2009 Phil Burk, SoftSynth.com
+ */
+public class PanControl extends UnitGenerator {
+ public UnitInputPort pan;
+ public UnitOutputPort output;
+
+ /* Define Unit Ports used by connect() and set(). */
+ public PanControl() {
+ addPort(pan = new UnitInputPort("Pan"));
+ addPort(output = new UnitOutputPort(2, "Output", 0.0));
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] panPtr = pan.getValues();
+ double[] output0s = output.getValues(0);
+ double[] output1s = output.getValues(1);
+
+ for (int i = start; i < limit; i++) {
+ double gainB = (panPtr[i] * 0.5) + 0.5; /*
+ * Scale and offset to 0.0 to 1.0
+ */
+ output0s[i] = 1.0 - gainB;
+ output1s[i] = gainB;
+ }
+ }
+}
diff --git a/src/com/jsyn/unitgen/ParabolicEnvelope.java b/src/com/jsyn/unitgen/ParabolicEnvelope.java
new file mode 100644
index 0000000..6de97d9
--- /dev/null
+++ b/src/com/jsyn/unitgen/ParabolicEnvelope.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 1997 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.ports.UnitOutputPort;
+
+/**
+ * ParabolicEnvelope unit. Output goes from zero to amplitude then back to zero in a parabolic arc.
+ * <P>
+ * Generate a short parabolic envelope that could be used for granular synthesis. The output starts
+ * at zero, peaks at the value of amplitude then returns to zero. This unit has two states, IDLE and
+ * RUNNING. If a trigger is received when IDLE, the envelope is started and another trigger is sent
+ * out the triggerOutput port. This triggerOutput can be used to latch values for the synthesis of a
+ * grain. If a trigger is received when RUNNING, then it is ignored and passed out the triggerPass
+ * port. The triggerPass can be connected to the triggerInput of another ParabolicEnvelope. Thus you
+ * can implement a simple grain allocation scheme by daisy chaining the triggers of
+ * ParabolicEnvelopes.
+ * <P>
+ * The envelope is generated by a double integrator method so it uses relatively little CPU time.
+ *
+ * @author (C) 1997 Phil Burk, SoftSynth.com
+ * @see EnvelopeDAHDSR
+ */
+public class ParabolicEnvelope extends UnitGenerator {
+
+ /** Fastest repeat rate of envelope if it were continually retriggered in Hertz. */
+ public UnitInputPort frequency;
+ /** True value triggers envelope when in resting state. */
+ public UnitInputPort triggerInput;
+ public UnitInputPort amplitude;
+
+ /** Trigger output when envelope started. */
+ public UnitOutputPort triggerOutput;
+ /** Input trigger passed out if ignored for daisy chaining. */
+ public UnitOutputPort triggerPass;
+ public UnitOutputPort output;
+
+ private double slope;
+ private double curve;
+ private double level;
+ private boolean running;
+
+ /* Define Unit Ports used by connect() and set(). */
+ public ParabolicEnvelope() {
+ addPort(triggerInput = new UnitInputPort("Input"));
+ addPort(frequency = new UnitInputPort("Frequency", UnitOscillator.DEFAULT_FREQUENCY));
+ addPort(amplitude = new UnitInputPort("Amplitude", UnitOscillator.DEFAULT_AMPLITUDE));
+
+ addPort(output = new UnitOutputPort("Output"));
+ addPort(triggerOutput = new UnitOutputPort("TriggerOutput"));
+ addPort(triggerPass = new UnitOutputPort("TriggerPass"));
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] frequencies = frequency.getValues();
+ double[] amplitudes = amplitude.getValues();
+ double[] triggerInputs = triggerInput.getValues();
+ double[] outputs = output.getValues();
+ double[] triggerPasses = triggerPass.getValues();
+ double[] triggerOutputs = triggerOutput.getValues();
+
+ for (int i = start; i < limit; i++) {
+ if (!running) {
+ if (triggerInputs[i] > 0) {
+ double freq = frequencies[i] * synthesisEngine.getInverseNyquist();
+ freq = (freq > 1.0) ? 1.0 : ((freq < -1.0) ? -1.0 : freq);
+ double ampl = amplitudes[i];
+ double freq2 = freq * freq; /* Square frequency. */
+ slope = 4.0 * ampl * (freq - freq2);
+ curve = -8.0 * ampl * freq2;
+ level = 0.0;
+ triggerOutputs[i] = UnitGenerator.TRUE;
+ running = true;
+ } else {
+ triggerOutputs[i] = UnitGenerator.FALSE;
+ }
+ triggerPasses[i] = UnitGenerator.FALSE;
+ } else /* RUNNING */
+ {
+ level += slope;
+ slope += curve;
+ if (level <= 0.0) {
+ level = 0.0;
+ running = false;
+ /* Autostop? - FIXME */
+ }
+
+ triggerOutputs[i] = UnitGenerator.FALSE;
+ triggerPasses[i] = triggerInputs[i];
+ }
+ outputs[i] = level;
+ }
+ }
+}
diff --git a/src/com/jsyn/unitgen/PassThrough.java b/src/com/jsyn/unitgen/PassThrough.java
new file mode 100644
index 0000000..de19c65
--- /dev/null
+++ b/src/com/jsyn/unitgen/PassThrough.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2011 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+/**
+ * Pass the input through to the output unchanged. This is often used for distributing a signal to
+ * multiple ports inside a circuit. It can also be used as a summing node, in other words, a mixer.
+ *
+ * @author Phil Burk (C) 2011 Mobileer Inc
+ * @see Circuit
+ */
+public class PassThrough extends UnitFilter {
+ @Override
+ public void generate(int start, int limit) {
+ double[] inputs = input.getValues();
+ double[] outputs = output.getValues();
+
+ for (int i = start; i < limit; i++) {
+ outputs[i] = inputs[i];
+ }
+
+ }
+}
diff --git a/src/com/jsyn/unitgen/PeakFollower.java b/src/com/jsyn/unitgen/PeakFollower.java
new file mode 100644
index 0000000..7bf0508
--- /dev/null
+++ b/src/com/jsyn/unitgen/PeakFollower.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 1997 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.ports.UnitOutputPort;
+import com.jsyn.ports.UnitVariablePort;
+
+/**
+ * Tracks the peaks of an input signal. This can be used to monitor the overall amplitude of a
+ * signal. The output can be used to drive color organs, vocoders, VUmeters, etc. Output drops
+ * exponentially when the input drops below the current output level. The output approaches zero
+ * based on the value on the halfLife port.
+ *
+ * @author (C) 1997-2009 Phil Burk, SoftSynth.com
+ */
+public class PeakFollower extends UnitGenerator {
+ public UnitInputPort input;
+ public UnitVariablePort current;
+ public UnitInputPort halfLife;
+ public UnitOutputPort output;
+
+ private double previousHalfLife = -1.0;
+ private double decayScalar = 0.99;
+
+ /* Define Unit Ports used by connect() and set(). */
+ public PeakFollower() {
+ addPort(input = new UnitInputPort("Input"));
+ addPort(halfLife = new UnitInputPort(1, "HalfLife", 0.1));
+ addPort(current = new UnitVariablePort("Current"));
+ addPort(output = new UnitOutputPort("Output"));
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] inputs = input.getValues();
+ double[] outputs = output.getValues();
+ double currentHalfLife = halfLife.getValues()[0];
+ double currentValue = current.getValue();
+
+ if (currentHalfLife != previousHalfLife) {
+ decayScalar = this.convertHalfLifeToMultiplier(currentHalfLife);
+ previousHalfLife = currentHalfLife;
+ }
+
+ double scalar = 1.0 - decayScalar;
+
+ for (int i = start; i < limit; i++) {
+ double inputValue = inputs[i];
+ if (inputValue < 0.0) {
+ inputValue = -inputValue; // absolute value
+ }
+
+ if (inputValue >= currentValue) {
+ currentValue = inputValue;
+ } else {
+ currentValue = currentValue * scalar;
+ }
+
+ outputs[i] = currentValue;
+ }
+
+ /*
+ * When current gets close to zero, set current to zero to prevent FP underflow, which can
+ * cause a severe performance degradation in 'C'.
+ */
+ if (currentValue < VERY_SMALL_FLOAT) {
+ currentValue = 0.0;
+ }
+
+ current.setValue(currentValue);
+ }
+}
diff --git a/src/com/jsyn/unitgen/PhaseShifter.java b/src/com/jsyn/unitgen/PhaseShifter.java
new file mode 100644
index 0000000..4b17245
--- /dev/null
+++ b/src/com/jsyn/unitgen/PhaseShifter.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2014 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitInputPort;
+
+/**
+ * PhaseShifter effects processor. This unit emulates a common guitar pedal effect but without the
+ * LFO modulation. You can use your own modulation source connected to the "offset" port. Different
+ * frequencies are phase shifted varying amounts using a series of AllPass filters. By feeding the
+ * output back to the input we can get varying phase cancellation. This implementation was based on
+ * code posted to the music-dsp archive by Ross Bencina. http://www.musicdsp.org/files/phaser.cpp
+ *
+ * @author (C) 2014 Phil Burk, Mobileer Inc
+ * @see FilterLowPass
+ * @see FilterAllPass
+ * @see RangeConverter
+ */
+
+public class PhaseShifter extends UnitFilter {
+ /**
+ * Connect an oscillator to this port to sweep the phase. A range of 0.05 to 0.4 is a good
+ * start.
+ */
+ public UnitInputPort offset;
+ public UnitInputPort feedback;
+ public UnitInputPort depth;
+
+ private double zm1;
+ private double[] xs;
+ private double[] ys;
+
+ public PhaseShifter() {
+ this(6);
+ }
+
+ public PhaseShifter(int numStages) {
+ addPort(offset = new UnitInputPort("Offset", 0.1));
+ addPort(feedback = new UnitInputPort("Feedback", 0.7));
+ addPort(depth = new UnitInputPort("Depth", 1.0));
+
+ xs = new double[numStages];
+ ys = new double[numStages];
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] inputs = input.getValues();
+ double[] outputs = output.getValues();
+ double[] feedbacks = feedback.getValues();
+ double[] depths = depth.getValues();
+ double[] offsets = offset.getValues();
+ double gain;
+
+ for (int i = start; i < limit; i++) {
+ // Support audio rate modulation.
+ double currentOffset = offsets[i];
+
+ // Prevent gain from exceeding 1.0.
+ gain = 1.0 - (currentOffset * currentOffset);
+ if (gain < -1.0) {
+ gain = -1.0;
+ }
+
+ double x = inputs[i] + (zm1 * feedbacks[i]);
+ // Cascaded all-pass filters.
+ for (int stage = 0; stage < xs.length; stage++) {
+ double temp = ys[stage] = (gain * (ys[stage] - x)) + xs[stage];
+ xs[stage] = x;
+ x = temp;
+ }
+ zm1 = x;
+ outputs[i] = inputs[i] + (x * depths[i]);
+ }
+ }
+}
diff --git a/src/com/jsyn/unitgen/PinkNoise.java b/src/com/jsyn/unitgen/PinkNoise.java
new file mode 100644
index 0000000..84aa2f2
--- /dev/null
+++ b/src/com/jsyn/unitgen/PinkNoise.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.ports.UnitOutputPort;
+import com.jsyn.util.PseudoRandom;
+
+/**
+ * Random output with 3dB per octave rolloff providing a soft natural noise sound. Generated using
+ * Gardner method. Optimization suggested by James McCartney uses a tree to select which random
+ * value to replace.
+ *
+ * <pre>
+ * x x x x x x x x x x x x x x x x
+ * x x x x x x x x
+ * x x x x
+ * x x
+ * x
+ * </pre>
+ *
+ * Tree is generated by counting trailing zeros in an increasing index. When the index is zero, no
+ * random number is selected. Author: Phil Burk (C) 1996 SoftSynth.com.
+ */
+
+public class PinkNoise extends UnitGenerator implements UnitSource {
+
+ public UnitInputPort amplitude;
+ public UnitOutputPort output;
+
+ private final int NUM_ROWS = 16;
+ private final int RANDOM_BITS = 24;
+ private final int RANDOM_SHIFT = 32 - RANDOM_BITS;
+
+ private PseudoRandom randomNum;
+ protected double prevNoise, currNoise;
+
+ private long[] rows = new long[NUM_ROWS]; // NEXT RANDOM UNSIGNED 32
+ private double scalar; // used to scale within range of -1.0 to +1.0
+ private int runningSum; // used to optimize summing of generators
+ private int index; // incremented with each sample
+ private int indexMask; // index wrapped and ANDing with this mask
+
+ /* Define Unit Ports used by connect() and set(). */
+ public PinkNoise() {
+ addPort(amplitude = new UnitInputPort("Amplitude", UnitOscillator.DEFAULT_AMPLITUDE));
+ addPort(output = new UnitOutputPort("Output"));
+
+ randomNum = new PseudoRandom();
+
+ // set up for N rows of generators
+ index = 0;
+ indexMask = (1 << NUM_ROWS) - 1;
+
+ // Calculate maximum possible signed random value. Extra 1 for white
+ // noise always added.
+ int pmax = (NUM_ROWS + 1) * (1 << (RANDOM_BITS - 1));
+ scalar = 1.0 / pmax;
+
+ // initialize rows
+ for (int i = 0; i < NUM_ROWS; i++) {
+ rows[i] = 0;
+ }
+
+ runningSum = 0;
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] amplitudes = amplitude.getValues();
+ double[] outputs = output.getValues();
+
+ for (int i = start; i < limit; i++) {
+ outputs[i] = generatePinkNoise() * amplitudes[i];
+ }
+ }
+
+ public double generatePinkNoise() {
+ index = (index + 1) & indexMask;
+
+ // If index is zero, don't update any random values.
+ if (index != 0) {
+ // Determine how many trailing zeros in PinkIndex.
+ // This algorithm will hang of n==0 so test first
+ int numZeros = 0;
+ int n = index;
+
+ while ((n & 1) == 0) {
+ n = n >> 1;
+ numZeros++;
+ }
+
+ // Replace the indexed ROWS random value.
+ // Subtract and add back to RunningSum instead of adding all the
+ // random values together. Only one changes each time.
+ runningSum -= rows[numZeros];
+ int newRandom = randomNum.nextRandomInteger() >> RANDOM_SHIFT;
+ runningSum += newRandom;
+ rows[numZeros] = newRandom;
+ }
+
+ // Add extra white noise value.
+ int newRandom = randomNum.nextRandomInteger() >> RANDOM_SHIFT;
+ int sum = runningSum + newRandom;
+
+ // Scale to range of -1.0 to 0.9999.
+ return scalar * sum;
+ }
+
+ @Override
+ public UnitOutputPort getOutput() {
+ return output;
+ }
+}
diff --git a/src/com/jsyn/unitgen/PitchDetector.java b/src/com/jsyn/unitgen/PitchDetector.java
new file mode 100644
index 0000000..59bba69
--- /dev/null
+++ b/src/com/jsyn/unitgen/PitchDetector.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2012 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.ports.UnitOutputPort;
+import com.jsyn.util.AutoCorrelator;
+import com.jsyn.util.SignalCorrelator;
+
+/**
+ * Estimate the fundamental frequency of a monophonic signal. Analyzes an input signal and outputs
+ * an estimated period in frames and a frequency in Hertz. The frequency is frameRate/period. The
+ * confidence tells you how accurate the estimate is. When the confidence is low, you should ignore
+ * the period. You can use a CompareUnit and a LatchUnit to hold values that you are confident of.
+ * <P>
+ * Note that a stable monophonic signal is required for accurate pitch tracking.
+ *
+ * @author (C) 2012 Phil Burk, Mobileer Inc
+ */
+public class PitchDetector extends UnitGenerator {
+ public UnitInputPort input;
+
+ public UnitOutputPort period;
+ public UnitOutputPort confidence;
+ public UnitOutputPort frequency;
+ public UnitOutputPort updated;
+
+ protected SignalCorrelator signalCorrelator;
+
+ private double lastFrequency = 440.0;
+ private double lastPeriod = 44100.0 / lastFrequency; // result of analysis
+ private double lastConfidence = 0.0; // Measure of confidence in the result.
+
+ private static final int LOWEST_FREQUENCY = 40;
+ private static final int HIGHEST_RATE = 48000;
+ private static final int CYCLES_NEEDED = 2;
+
+ public PitchDetector() {
+ super();
+ addPort(input = new UnitInputPort("Input"));
+
+ addPort(period = new UnitOutputPort("Period"));
+ addPort(confidence = new UnitOutputPort("Confidence"));
+ addPort(frequency = new UnitOutputPort("Frequency"));
+ addPort(updated = new UnitOutputPort("Updated"));
+ signalCorrelator = createSignalCorrelator();
+ }
+
+ public SignalCorrelator createSignalCorrelator() {
+ int framesNeeded = HIGHEST_RATE * CYCLES_NEEDED / LOWEST_FREQUENCY;
+ return new AutoCorrelator(framesNeeded);
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] inputs = input.getValues();
+ double[] periods = period.getValues();
+ double[] confidences = confidence.getValues();
+ double[] frequencies = frequency.getValues();
+ double[] updateds = updated.getValues();
+
+ for (int i = start; i < limit; i++) {
+ double current = inputs[i];
+ if (signalCorrelator.addSample(current)) {
+ lastPeriod = signalCorrelator.getPeriod();
+ if (lastPeriod < 0.1) {
+ System.out.println("ILLEGAL PERIOD");
+ }
+ double currentFrequency = getFrameRate() / (lastPeriod + 0);
+ double confidence = signalCorrelator.getConfidence();
+ if (confidence > 0.1) {
+ if (true) {
+ double coefficient = confidence * 0.2;
+ // Take weighted average with previous frequency.
+ lastFrequency = ((lastFrequency * (1.0 - coefficient)) + (currentFrequency * coefficient));
+ } else {
+ lastFrequency = ((lastFrequency * lastConfidence) + (currentFrequency * confidence))
+ / (lastConfidence + confidence);
+ }
+ }
+ lastConfidence = confidence;
+ updateds[i] = 1.0;
+ } else {
+ updateds[i] = 0.0;
+ }
+ periods[i] = lastPeriod;
+ confidences[i] = lastConfidence;
+ frequencies[i] = lastFrequency;
+ }
+ }
+
+ /**
+ * For debugging only.
+ *
+ * @return internal array of correlation results.
+ */
+ public float[] getDiffs() {
+ return signalCorrelator.getDiffs();
+ }
+
+}
diff --git a/src/com/jsyn/unitgen/PowerOfTwo.java b/src/com/jsyn/unitgen/PowerOfTwo.java
new file mode 100644
index 0000000..5f1b4cd
--- /dev/null
+++ b/src/com/jsyn/unitgen/PowerOfTwo.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.ports.UnitOutputPort;
+
+/**
+ * output = (2.0^input) This is useful for converting a pitch modulation value into a frequency
+ * scaler. An input value of +1.0 will output 2.0 for an octave increase. An input value of -1.0
+ * will output 0.5 for an octave decrease. This implementation uses a table lookup to optimize for
+ * speed. It is accurate enough for tuning. It also checks to see if the current input value is the
+ * same as the previous input value. If so then it reuses the previous computed value.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class PowerOfTwo extends UnitGenerator {
+ public UnitInputPort input;
+ public UnitOutputPort output;
+
+ private static double[] table;
+ private static final int NUM_VALUES = 1024;
+ // Cached computation.
+ private double lastInput = 0.0;
+ private double lastOutput = 1.0;
+
+ static {
+ // Add guard point for faster interpolation.
+ // Add another point to handle inputs like -1.5308084989341915E-17,
+ // which generate indices above range.
+ table = new double[NUM_VALUES + 2];
+ // Fill one octave of the table.
+ for (int i = 0; i < table.length; i++) {
+ double value = Math.pow(2.0, ((double) i) / NUM_VALUES);
+ table[i] = value;
+ }
+ }
+
+ public PowerOfTwo() {
+ addPort(input = new UnitInputPort("Input"));
+ input.setup(-8.0, 0.0, 8.0);
+ addPort(output = new UnitOutputPort("Output"));
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] inputs = input.getValues();
+ double[] outputs = output.getValues();
+
+ if (true) {
+ for (int i = start; i < limit; i++) {
+ double in = inputs[i];
+ // Can we reuse a previously computed value?
+ if (in == lastInput) {
+ outputs[i] = lastOutput;
+ } else {
+ int octave = (int) Math.floor(in);
+ double normal = in - octave;
+ // Do table lookup.
+ double findex = normal * NUM_VALUES;
+ int index = (int) findex;
+ double fraction = findex - index;
+ double value = table[index] + (fraction * (table[index + 1] - table[index]));
+
+ // Adjust for octave.
+ while (octave > 0) {
+ octave -= 1;
+ value *= 2.0;
+ }
+ while (octave < 0) {
+ octave += 1;
+ value *= 0.5;
+ }
+ outputs[i] = value;
+ lastInput = in;
+ lastOutput = value;
+ }
+ }
+ } else {
+ for (int i = start; i < limit; i++) {
+ outputs[i] = Math.pow(2.0, inputs[i]);
+ }
+ }
+ }
+}
diff --git a/src/com/jsyn/unitgen/PulseOscillator.java b/src/com/jsyn/unitgen/PulseOscillator.java
new file mode 100644
index 0000000..5ac7352
--- /dev/null
+++ b/src/com/jsyn/unitgen/PulseOscillator.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitInputPort;
+
+/**
+ * Simple pulse wave oscillator.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ */
+public class PulseOscillator extends UnitOscillator {
+ /**
+ * Pulse width varies from -1.0 to +1.0. At 0.0 the pulse is actually a square wave.
+ */
+ public UnitInputPort width;
+
+ public PulseOscillator() {
+ addPort(width = new UnitInputPort("Width"));
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] frequencies = frequency.getValues();
+ double[] amplitudes = amplitude.getValues();
+ double[] widths = width.getValues();
+ double[] outputs = output.getValues();
+
+ // Variables have a single value.
+ double currentPhase = phase.getValue();
+
+ for (int i = start; i < limit; i++) {
+ // Generate sawtooth phaser to provide phase for pulse generation.
+ double phaseIncrement = convertFrequencyToPhaseIncrement(frequencies[i]);
+ currentPhase = incrementWrapPhase(currentPhase, phaseIncrement);
+ double ampl = amplitudes[i];
+ // Either full negative or positive amplitude.
+ outputs[i] = (currentPhase < widths[i]) ? -ampl : ampl;
+ }
+
+ // Value needs to be saved for next time.
+ phase.setValue(currentPhase);
+ }
+
+}
diff --git a/src/com/jsyn/unitgen/PulseOscillatorBL.java b/src/com/jsyn/unitgen/PulseOscillatorBL.java
new file mode 100644
index 0000000..43fe27b
--- /dev/null
+++ b/src/com/jsyn/unitgen/PulseOscillatorBL.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.engine.MultiTable;
+import com.jsyn.ports.UnitInputPort;
+
+/**
+ * Pulse oscillator that uses two band limited sawtooth oscillators.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ */
+public class PulseOscillatorBL extends SawtoothOscillatorBL {
+ public UnitInputPort width;
+
+ public PulseOscillatorBL() {
+ addPort(width = new UnitInputPort("Width"));
+ }
+
+ @Override
+ protected double generateBL(MultiTable multiTable, double currentPhase,
+ double positivePhaseIncrement, double flevel, int i) {
+ double[] widths = width.getValues();
+ double width = widths[i];
+ width = (width > 0.999) ? 0.999 : ((width < -0.999) ? -0.999 : width);
+
+ double val1 = multiTable.calculateSawtooth(currentPhase, positivePhaseIncrement, flevel);
+
+ // Generate second sawtooth so we can add them together.
+ double phase2 = currentPhase + 1.0 - width; // 180 degrees out of phase
+ if (phase2 >= 1.0) {
+ phase2 -= 2.0;
+ }
+ double val2 = multiTable.calculateSawtooth(phase2, positivePhaseIncrement, flevel);
+
+ /*
+ * Need to adjust amplitude based on positive phaseInc. little less than half at
+ * Nyquist/2.0!
+ */
+ double scale = 1.0 - positivePhaseIncrement;
+ return scale * (val1 - val2 - width);
+ }
+}
diff --git a/src/com/jsyn/unitgen/RaisedCosineEnvelope.java b/src/com/jsyn/unitgen/RaisedCosineEnvelope.java
new file mode 100644
index 0000000..c32417c
--- /dev/null
+++ b/src/com/jsyn/unitgen/RaisedCosineEnvelope.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2011 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+/**
+ * An envelope that can be used in a GrainFarm to shape the amplitude of a Grain. The envelope
+ * starts at 0.0, rises to 1.0, then returns to 0.0 following a cosine curve.
+ *
+ * <pre>
+ * output = 0.5 - (0.5 * cos(phase))
+ * </pre>
+ *
+ * @author Phil Burk (C) 2011 Mobileer Inc
+ * @see GrainFarm
+ */
+public class RaisedCosineEnvelope extends GrainCommon implements GrainEnvelope {
+ protected double phase;
+ protected double phaseIncrement;
+
+ public RaisedCosineEnvelope() {
+ setFrameRate(44100);
+ setDuration(0.1);
+ }
+
+ /**
+ * @return next value of the envelope.
+ */
+ @Override
+ public double next() {
+ phase += phaseIncrement;
+ if (phase > (2.0 * Math.PI)) {
+ return 0.0;
+ } else {
+ return 0.5 - (0.5 * Math.cos(phase)); // TODO optimize using Taylor expansion
+ }
+ }
+
+ /**
+ * @return true if there are more envelope values left.
+ */
+ @Override
+ public boolean hasMoreValues() {
+ return (phase < (2.0 * Math.PI));
+ }
+
+ /**
+ * Reset the envelope back to the beginning.
+ */
+ @Override
+ public void reset() {
+ phase = 0.0;
+ }
+
+ @Override
+ public void setDuration(double duration) {
+ phaseIncrement = 2.0 * Math.PI / (getFrameRate() * duration);
+ }
+
+}
diff --git a/src/com/jsyn/unitgen/RangeConverter.java b/src/com/jsyn/unitgen/RangeConverter.java
new file mode 100644
index 0000000..ae94b0f
--- /dev/null
+++ b/src/com/jsyn/unitgen/RangeConverter.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2014 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitInputPort;
+
+/**
+ * Convert an input signal between -1.0 and +1.0 to the range min to max. This is handy when using
+ * an oscillator as a modulator.
+ *
+ * @author (C) 2014 Phil Burk, Mobileer Inc
+ * @see EdgeDetector
+ */
+public class RangeConverter extends UnitFilter {
+ public UnitInputPort min;
+ public UnitInputPort max;
+
+ /* Define Unit Ports used by connect() and set(). */
+ public RangeConverter() {
+ addPort(min = new UnitInputPort("Min", 40.0));
+ addPort(max = new UnitInputPort("Max", 2000.0));
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] inputs = input.getValues();
+ double[] mins = min.getValues();
+ double[] maxs = max.getValues();
+ double[] outputs = output.getValues();
+
+ for (int i = start; i < limit; i++) {
+ double low = mins[i];
+ outputs[i] = low + ((maxs[i] - low) * (inputs[i] + 1) * 0.5);
+ }
+ }
+}
diff --git a/src/com/jsyn/unitgen/RectangularWindow.java b/src/com/jsyn/unitgen/RectangularWindow.java
new file mode 100644
index 0000000..d61f763
--- /dev/null
+++ b/src/com/jsyn/unitgen/RectangularWindow.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2013 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.data.SpectralWindow;
+
+/**
+ * Window that is just 1.0. Flat like a rectangle.
+ *
+ * @author Phil Burk (C) 2013 Mobileer Inc
+ * @see SpectralFFT
+ */
+public class RectangularWindow implements SpectralWindow {
+ static RectangularWindow instance = new RectangularWindow();
+
+ @Override
+ /** This always returns 1.0. Do not pass indices outside the window range. */
+ public double get(int index) {
+ return 1.0; // impressive, eh?
+ }
+
+ public static RectangularWindow getInstance() {
+ return instance;
+ }
+}
diff --git a/src/com/jsyn/unitgen/RedNoise.java b/src/com/jsyn/unitgen/RedNoise.java
new file mode 100644
index 0000000..d3e4321
--- /dev/null
+++ b/src/com/jsyn/unitgen/RedNoise.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 1997 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.util.PseudoRandom;
+
+/**
+ * RedNoise unit. This unit interpolates straight line segments between pseudo-random numbers to
+ * produce "red" noise. It is a grittier alternative to the white generator WhiteNoise. It is also
+ * useful as a slowly changing random control generator for natural sounds. Frequency port controls
+ * the number of times per second that a new random number is chosen.
+ *
+ * @author (C) 1997 Phil Burk, SoftSynth.com
+ * @see WhiteNoise
+ */
+public class RedNoise extends UnitOscillator {
+ private PseudoRandom randomNum;
+ protected double prevNoise, currNoise;
+
+ /* Define Unit Ports used by connect() and set(). */
+ public RedNoise() {
+ super();
+ randomNum = new PseudoRandom();
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] amplitudes = amplitude.getValues();
+ double[] frequencies = frequency.getValues();
+ double[] outputs = output.getValues();
+ double currPhase = phase.getValue();
+ double phaseIncrement, currOutput;
+
+ double framePeriod = getFramePeriod();
+
+ for (int i = start; i < limit; i++) {
+ // compute phase
+ phaseIncrement = frequencies[i] * framePeriod;
+
+ // verify that phase is within minimums and is not negative
+ if (phaseIncrement < 0.0) {
+ phaseIncrement = 0.0 - phaseIncrement;
+ }
+ if (phaseIncrement > 1.0) {
+ phaseIncrement = 1.0;
+ }
+
+ currPhase += phaseIncrement;
+
+ // calculate new random whenever phase passes 1.0
+ if (currPhase > 1.0) {
+ prevNoise = currNoise;
+ currNoise = randomNum.nextRandomDouble();
+ // reset phase for interpolation
+ currPhase -= 1.0;
+ }
+
+ // interpolate current
+ currOutput = prevNoise + (currPhase * (currNoise - prevNoise));
+ outputs[i] = currOutput * amplitudes[i];
+ }
+
+ // store new phase
+ phase.setValue(currPhase);
+ }
+}
diff --git a/src/com/jsyn/unitgen/SampleGrainFarm.java b/src/com/jsyn/unitgen/SampleGrainFarm.java
new file mode 100644
index 0000000..93a694f
--- /dev/null
+++ b/src/com/jsyn/unitgen/SampleGrainFarm.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2011 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.data.FloatSample;
+import com.jsyn.ports.UnitInputPort;
+
+/**
+ * A GrainFarm that uses a FloatSample as source material. In this example we load a FloatSample for
+ * use as a source material. <code><pre>
+ synth.add( sampleGrainFarm = new SampleGrainFarm() );
+ // Load a sample that we want to "granulate" from a file.
+ sample = SampleLoader.loadFloatSample( sampleFile );
+ sampleGrainFarm.setSample( sample );
+ // Use a ramp to move smoothly within the file.
+ synth.add( ramp = new ContinuousRamp() );
+ ramp.output.connect( sampleGrainFarm.position );
+</pre><code>
+ *
+ * @author Phil Burk (C) 2011 Mobileer Inc
+ */
+public class SampleGrainFarm extends GrainFarm {
+ private FloatSample sample;
+ public UnitInputPort position;
+ public UnitInputPort positionRange;
+
+ public SampleGrainFarm() {
+ super();
+ addPort(position = new UnitInputPort("Position", 0.0));
+ addPort(positionRange = new UnitInputPort("PositionRange", 0.0));
+ }
+
+ @Override
+ public void allocate(int numGrains) {
+ Grain[] grainArray = new Grain[numGrains];
+ for (int i = 0; i < numGrains; i++) {
+ Grain grain = new Grain(new SampleGrainSource(), new RaisedCosineEnvelope());
+ grainArray[i] = grain;
+ }
+ setGrainArray(grainArray);
+ }
+
+ @Override
+ public void setupGrain(Grain grain, int i) {
+ SampleGrainSource sampleGrainSource = (SampleGrainSource) grain.getSource();
+ sampleGrainSource.setSample(sample);
+ sampleGrainSource.setPosition(position.getValues()[i]);
+ sampleGrainSource.setPositionRange(positionRange.getValues()[i]);
+ super.setupGrain(grain, i);
+ }
+
+ public void setSample(FloatSample sample) {
+ this.sample = sample;
+ }
+}
diff --git a/src/com/jsyn/unitgen/SampleGrainSource.java b/src/com/jsyn/unitgen/SampleGrainSource.java
new file mode 100644
index 0000000..f33817f
--- /dev/null
+++ b/src/com/jsyn/unitgen/SampleGrainSource.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.data.FloatSample;
+
+public class SampleGrainSource extends GrainCommon implements GrainSource {
+ private FloatSample sample;
+ private double position; // ranges from -1.0 to 1.0
+ private double positionRange;
+ private double phase; // ranges from 0.0 to 1.0
+ private double phaseIncrement;
+ private int numFramesGuarded;
+ private static final double MAX_PHASE = 0.9999999999;
+
+ @Override
+ public double next() {
+ phase += phaseIncrement;
+ if (phase > MAX_PHASE) {
+ phase = MAX_PHASE;
+ }
+ double fractionalIndex = phase * numFramesGuarded;
+ return sample.interpolate(fractionalIndex);
+ }
+
+ @Override
+ public void setRate(double rate) {
+ phaseIncrement = rate * sample.getFrameRate() / (getFrameRate() * numFramesGuarded);
+ }
+
+ public void setSample(FloatSample sample) {
+ this.sample = sample;
+ numFramesGuarded = sample.getNumFrames() - 1;
+ }
+
+ public void setPosition(double position) {
+ this.position = position;
+ }
+
+ @Override
+ public void reset() {
+ double randomPosition = position + (positionRange * (Math.random() - 0.5));
+ phase = (randomPosition * 0.5) + 0.5;
+ if (phase < 0.0) {
+ phase = 0.0;
+ } else if (phase > MAX_PHASE) {
+ phase = MAX_PHASE;
+ }
+ }
+
+ public void setPositionRange(double positionRange) {
+ this.positionRange = positionRange;
+ }
+
+}
diff --git a/src/com/jsyn/unitgen/SawtoothOscillator.java b/src/com/jsyn/unitgen/SawtoothOscillator.java
new file mode 100644
index 0000000..1b3dead
--- /dev/null
+++ b/src/com/jsyn/unitgen/SawtoothOscillator.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+/**
+ * Simple sawtooth oscillator.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ */
+public class SawtoothOscillator extends UnitOscillator {
+
+ @Override
+ public void generate(int start, int limit) {
+
+ double[] frequencies = frequency.getValues();
+ double[] amplitudes = amplitude.getValues();
+ double[] outputs = output.getValues();
+
+ // Variables have a single value.
+ double currentPhase = phase.getValue();
+
+ for (int i = start; i < limit; i++) {
+ /* Generate sawtooth phasor to provide phase for sine generation. */
+ double phaseIncrement = convertFrequencyToPhaseIncrement(frequencies[i]);
+ currentPhase = incrementWrapPhase(currentPhase, phaseIncrement);
+ outputs[i] = currentPhase * amplitudes[i];
+ }
+
+ // Value needs to be saved for next time.
+ phase.setValue(currentPhase);
+ }
+
+}
diff --git a/src/com/jsyn/unitgen/SawtoothOscillatorBL.java b/src/com/jsyn/unitgen/SawtoothOscillatorBL.java
new file mode 100644
index 0000000..8b58f6c
--- /dev/null
+++ b/src/com/jsyn/unitgen/SawtoothOscillatorBL.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.engine.MultiTable;
+
+/**
+ * Sawtooth oscillator that uses multiple wave tables for band limiting. This requires more CPU than
+ * a plain SawtoothOscillator but has less aliasing at high frequencies.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ */
+public class SawtoothOscillatorBL extends UnitOscillator {
+ @Override
+ public void generate(int start, int limit) {
+ MultiTable multiTable = MultiTable.getInstance();
+
+ double[] frequencies = frequency.getValues();
+ double[] amplitudes = amplitude.getValues();
+ double[] outputs = output.getValues();
+ // Variables have a single value.
+ double currentPhase = phase.getValue();
+
+ double phaseIncrement = convertFrequencyToPhaseIncrement(frequencies[0]);
+ double positivePhaseIncrement = Math.abs(phaseIncrement);
+ // This is very expensive so we moved it outside the loop.
+ // Try to optimize it with a table lookup.
+ double flevel = multiTable.convertPhaseIncrementToLevel(positivePhaseIncrement);
+
+ for (int i = start; i < limit; i++) {
+ /* Generate sawtooth phasor to provide phase for sine generation. */
+ phaseIncrement = convertFrequencyToPhaseIncrement(frequencies[i]);
+ currentPhase = incrementWrapPhase(currentPhase, phaseIncrement);
+ positivePhaseIncrement = Math.abs(phaseIncrement);
+
+ double val = generateBL(multiTable, currentPhase, positivePhaseIncrement, flevel, i);
+
+ outputs[i] = val * amplitudes[i];
+ }
+
+ // Value needs to be saved for next time.
+ phase.setValue(currentPhase);
+ }
+
+ protected double generateBL(MultiTable multiTable, double currentPhase,
+ double positivePhaseIncrement, double flevel, int i) {
+ /* Calculate table level then use it for lookup. */
+ return multiTable.calculateSawtooth(currentPhase, positivePhaseIncrement, flevel);
+ }
+
+}
diff --git a/src/com/jsyn/unitgen/SawtoothOscillatorDPW.java b/src/com/jsyn/unitgen/SawtoothOscillatorDPW.java
new file mode 100644
index 0000000..92d36d8
--- /dev/null
+++ b/src/com/jsyn/unitgen/SawtoothOscillatorDPW.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+/**
+ * Sawtooth DPW oscillator (a sawtooth with reduced aliasing). Based on a paper by Antti Huovilainen
+ * and Vesa Valimaki:
+ * http://www.scribd.com/doc/33863143/New-Approaches-to-Digital-Subtractive-Synthesis
+ *
+ * @author Phil Burk and Lisa Tolentino (C) 2009 Mobileer Inc
+ */
+public class SawtoothOscillatorDPW extends UnitOscillator {
+ // At a very low frequency, switch from DPW to raw sawtooth.
+ private static final double VERY_LOW_FREQUENCY = 2.0 * 0.1 / 44100.0;
+ private double z1;
+ private double z2;
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] frequencies = frequency.getValues();
+ double[] amplitudes = amplitude.getValues();
+ double[] outputs = output.getValues();
+
+ // Variables have a single value.
+ double currentPhase = phase.getValue();
+
+ for (int i = start; i < limit; i++) {
+ /* Generate raw sawtooth phaser. */
+ double phaseIncrement = convertFrequencyToPhaseIncrement(frequencies[i]);
+ currentPhase = incrementWrapPhase(currentPhase, phaseIncrement);
+
+ /* Square the raw sawtooth. */
+ double squared = currentPhase * currentPhase;
+ // Differentiate using a delayed value.
+ double diffed = squared - z2;
+ z2 = z1;
+ z1 = squared;
+
+ /* Calculate scaling based on phaseIncrement */
+ double pinc = phaseIncrement;
+ // Absolute value.
+ if (pinc < 0.0) {
+ pinc = 0.0 - pinc;
+ }
+
+ double dpw;
+ // If the frequency is very low then just use the raw sawtooth.
+ // This avoids divide by zero problems and scaling problems.
+ if (pinc < VERY_LOW_FREQUENCY) {
+ dpw = currentPhase;
+ } else {
+ dpw = diffed * 0.25 / pinc;
+ }
+
+ outputs[i] = amplitudes[i] * dpw;
+ }
+
+ // Value needs to be saved for next time.
+ phase.setValue(currentPhase);
+ }
+
+}
diff --git a/src/com/jsyn/unitgen/SchmidtTrigger.java b/src/com/jsyn/unitgen/SchmidtTrigger.java
new file mode 100644
index 0000000..64129ff
--- /dev/null
+++ b/src/com/jsyn/unitgen/SchmidtTrigger.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.ports.UnitOutputPort;
+
+/**
+ * SchmidtTrigger unit.
+ * <P>
+ * Output logic level value with hysteresis. Transition high when input exceeds setLevel. Only go
+ * low when input is below resetLevel. This can be used to reject low level noise on the input
+ * signal. The default values for setLevel and resetLevel are both 0.0. Setting setLevel to 0.1 and
+ * resetLevel to -0.1 will give some hysteresis. The outputPulse is a single sample wide pulse set
+ * when the output transitions from low to high.
+ *
+ * <PRE>
+ * if (output == 0.0)
+ * output = (input &gt; setLevel) ? 1.0 : 0.0;
+ * else if (output &gt; 0.0)
+ * output = (input &lt;= resetLevel) ? 0.0 : 1.0;
+ * else
+ * output = previous_output;
+ * </PRE>
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ * @see Compare
+ */
+public class SchmidtTrigger extends UnitFilter {
+ public UnitInputPort setLevel;
+ public UnitInputPort resetLevel;
+ public UnitOutputPort outputPulse;
+
+ /* Define Unit Ports used by connect() and set(). */
+ public SchmidtTrigger() {
+ addPort(setLevel = new UnitInputPort("SetLevel"));
+ addPort(resetLevel = new UnitInputPort("ResetLevel"));
+ addPort(input = new UnitInputPort("Input"));
+ addPort(outputPulse = new UnitOutputPort("OutputPulse"));
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] inPtr = input.getValues();
+ double[] pulsePtr = outputPulse.getValues();
+ double[] outPtr = output.getValues();
+ double[] setPtr = setLevel.getValues();
+ double[] resetPtr = resetLevel.getValues();
+
+ double outputValue = outPtr[0];
+ boolean state = (outputValue > UnitGenerator.FALSE);
+ for (int i = start; i < limit; i++) {
+ pulsePtr[i] = UnitGenerator.FALSE;
+ if (state) {
+ if (inPtr[i] <= resetPtr[i]) {
+ state = false;
+ outputValue = UnitGenerator.FALSE;
+ }
+ } else {
+ if (inPtr[i] > setPtr[i]) {
+ state = true;
+ outputValue = UnitGenerator.TRUE;
+ pulsePtr[i] = UnitGenerator.TRUE; /* Single impulse. */
+ }
+ }
+ outPtr[i] = outputValue;
+ }
+ }
+}
diff --git a/src/com/jsyn/unitgen/Select.java b/src/com/jsyn/unitgen/Select.java
new file mode 100644
index 0000000..14186a8
--- /dev/null
+++ b/src/com/jsyn/unitgen/Select.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2004 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.ports.UnitOutputPort;
+
+/**
+ * SelectUnit unit. Select InputA or InputB based on value on Select port.
+ *
+ * <pre>
+ * output = ( select > 0.0 ) ? inputB : inputA;
+ *
+ * <pre>
+ *
+ * @author (C) 2004-2009 Phil Burk, SoftSynth.com
+ */
+
+public class Select extends UnitGenerator {
+ public UnitInputPort inputA;
+ public UnitInputPort inputB;
+ public UnitInputPort select;
+ public UnitOutputPort output;
+
+ public Select() {
+ addPort(inputA = new UnitInputPort("InputA"));
+ addPort(inputB = new UnitInputPort("InputB"));
+ addPort(select = new UnitInputPort("Select"));
+ addPort(output = new UnitOutputPort("Output"));
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] inputAs = inputA.getValues();
+ double[] inputBs = inputB.getValues();
+ double[] selects = select.getValues();
+ double[] outputs = output.getValues();
+
+ for (int i = start; i < limit; i++) {
+ outputs[i] = (selects[i] > UnitGenerator.FALSE) ? inputBs[i] : inputAs[i];
+ }
+ }
+}
diff --git a/src/com/jsyn/unitgen/SequentialDataReader.java b/src/com/jsyn/unitgen/SequentialDataReader.java
new file mode 100644
index 0000000..901767b
--- /dev/null
+++ b/src/com/jsyn/unitgen/SequentialDataReader.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitDataQueuePort;
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.ports.UnitOutputPort;
+
+/**
+ * Base class for reading a sample or envelope.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ */
+public abstract class SequentialDataReader extends UnitGenerator {
+ public UnitDataQueuePort dataQueue;
+ public UnitInputPort amplitude;
+ public UnitOutputPort output;
+
+ /* Define Unit Ports used by connect() and set(). */
+ public SequentialDataReader() {
+ addPort(dataQueue = new UnitDataQueuePort("Data"));
+ addPort(amplitude = new UnitInputPort("Amplitude", UnitOscillator.DEFAULT_AMPLITUDE));
+ }
+}
diff --git a/src/com/jsyn/unitgen/SequentialDataWriter.java b/src/com/jsyn/unitgen/SequentialDataWriter.java
new file mode 100644
index 0000000..5a5febb
--- /dev/null
+++ b/src/com/jsyn/unitgen/SequentialDataWriter.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitDataQueuePort;
+import com.jsyn.ports.UnitInputPort;
+
+/**
+ * Base class for writing to a sample.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ */
+public abstract class SequentialDataWriter extends UnitGenerator {
+ public UnitDataQueuePort dataQueue;
+ public UnitInputPort input;
+
+ public SequentialDataWriter() {
+ addPort(dataQueue = new UnitDataQueuePort("Data"));
+ }
+}
diff --git a/src/com/jsyn/unitgen/SineOscillator.java b/src/com/jsyn/unitgen/SineOscillator.java
new file mode 100644
index 0000000..8c49ead
--- /dev/null
+++ b/src/com/jsyn/unitgen/SineOscillator.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+/**
+ * Sine oscillator generates a frequency controlled sine wave. It is implemented using a fast Taylor
+ * expansion.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ */
+public class SineOscillator extends UnitOscillator {
+ public SineOscillator() {
+ }
+
+ public SineOscillator(double freq) {
+ frequency.set(freq);
+ }
+
+ public SineOscillator(double freq, double amp) {
+ frequency.set(freq);
+ amplitude.set(amp);
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] frequencies = frequency.getValues();
+ double[] amplitudes = amplitude.getValues();
+ double[] outputs = output.getValues();
+ double currentPhase = phase.getValue();
+
+ for (int i = start; i < limit; i++) {
+ /* Generate sawtooth phasor to provide phase for sine generation. */
+ double phaseIncrement = convertFrequencyToPhaseIncrement(frequencies[i]);
+ currentPhase = incrementWrapPhase(currentPhase, phaseIncrement);
+ if (true) {
+ double value = fastSin(currentPhase);
+ outputs[i] = value * amplitudes[i];
+ } else {
+ // Slower but more accurate implementation.
+ outputs[i] = Math.sin(currentPhase * Math.PI) * amplitudes[i];
+ }
+ }
+
+ phase.setValue(currentPhase);
+ }
+
+ /**
+ * Calculate sine using Taylor expansion. Do not use values outside the range.
+ *
+ * @param currentPhase in the range of -1.0 to +1.0 for one cycle
+ */
+ public static double fastSin(double currentPhase) {
+ // Factorial constants so code is easier to read.
+ final double IF3 = 1.0 / (2 * 3);
+ final double IF5 = IF3 / (4 * 5);
+ final double IF7 = IF5 / (6 * 7);
+ final double IF9 = IF7 / (8 * 9);
+ final double IF11 = IF9 / (10 * 11);
+
+ /* Wrap phase back into region where results are more accurate. */
+ double yp = (currentPhase > 0.5) ? 1.0 - currentPhase : ((currentPhase < (-0.5)) ? (-1.0)
+ - currentPhase : currentPhase);
+
+ double x = yp * Math.PI;
+ double x2 = (x * x);
+ /* Taylor expansion out to x**11/11! factored into multiply-adds */
+ double fastsin = x
+ * (x2 * (x2 * (x2 * (x2 * ((x2 * (-IF11)) + IF9) - IF7) + IF5) - IF3) + 1);
+ return fastsin;
+ }
+}
diff --git a/src/com/jsyn/unitgen/SineOscillatorPhaseModulated.java b/src/com/jsyn/unitgen/SineOscillatorPhaseModulated.java
new file mode 100644
index 0000000..7631dff
--- /dev/null
+++ b/src/com/jsyn/unitgen/SineOscillatorPhaseModulated.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitInputPort;
+
+/**
+ * Sine oscillator with a phase modulation input. Phase modulation is similar to frequency
+ * modulation but is easier to use in some ways.
+ *
+ * <pre>
+ * output = sin(PI * (phase + modulation))
+ * </pre>
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ */
+public class SineOscillatorPhaseModulated extends SineOscillator {
+ public UnitInputPort modulation;
+
+ /* Define Unit Ports used by connect() and set(). */
+ public SineOscillatorPhaseModulated() {
+ super();
+ addPort(modulation = new UnitInputPort("Modulation"));
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] frequencies = frequency.getValues();
+ double[] amplitudes = amplitude.getValues();
+ double[] outputs = output.getValues();
+ double[] modulations = modulation.getValues();
+ double currentPhase = phase.getValue();
+
+ for (int i = start; i < limit; i++) {
+ /* Generate sawtooth phaser to provide phase for sine generation. */
+ double phaseIncrement = convertFrequencyToPhaseIncrement(frequencies[i]);
+ currentPhase = incrementWrapPhase(currentPhase, phaseIncrement);
+ double modulatedPhase = currentPhase + modulations[i];
+ double value;
+ if (false) {
+ // TODO Compare benchmarks.
+ while (modulatedPhase >= 1.0) {
+ modulatedPhase -= 2.0;
+ }
+ while (modulatedPhase < -1.0) {
+ modulatedPhase += 2.0;
+ }
+ value = fastSin(modulatedPhase);
+ } else {
+ value = Math.sin(modulatedPhase * Math.PI);
+ }
+ outputs[i] = value * amplitudes[i];
+ // System.out.format("Sine: freq = %10.4f , amp = %8.5f, out = %8.5f, phase = %8.5f, frame = %8d\n",
+ // frequencies[i], amplitudes[i],outputs[i],currentPhase,frame++ );
+ }
+
+ phase.setValue(currentPhase);
+ }
+
+}
diff --git a/src/com/jsyn/unitgen/SpectralFFT.java b/src/com/jsyn/unitgen/SpectralFFT.java
new file mode 100644
index 0000000..f3e881a
--- /dev/null
+++ b/src/com/jsyn/unitgen/SpectralFFT.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2013 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import java.util.Arrays;
+
+import com.jsyn.data.SpectralWindow;
+import com.jsyn.data.Spectrum;
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.ports.UnitSpectralOutputPort;
+import com.softsynth.math.FourierMath;
+
+/**
+ * Periodically transform the input signal using an FFT. Output complete spectra.
+ *
+ * @author Phil Burk (C) 2013 Mobileer Inc
+ * @version 016
+ * @see SpectralIFFT
+ * @see Spectrum
+ * @see SpectralFilter
+ */
+public class SpectralFFT extends UnitGenerator {
+ public UnitInputPort input;
+ /**
+ * Provides complete complex spectra when the FFT completes.
+ */
+ public UnitSpectralOutputPort output;
+ private double[] buffer;
+ private int cursor;
+ private SpectralWindow window = RectangularWindow.getInstance();
+ private int sizeLog2;
+ private int offset;
+ private boolean running;
+
+ /* Define Unit Ports used by connect() and set(). */
+ public SpectralFFT() {
+ this(Spectrum.DEFAULT_SIZE_LOG_2);
+ }
+
+ /**
+ * @param sizeLog2 for example, pass 10 to get a 1024 bin FFT
+ */
+ public SpectralFFT(int sizeLog2) {
+ addPort(input = new UnitInputPort("Input"));
+ addPort(output = new UnitSpectralOutputPort("Output", 1 << sizeLog2));
+ setSizeLog2(sizeLog2);
+ }
+
+ /**
+ * Please do not change the size of the FFT while JSyn is running.
+ *
+ * @param sizeLog2 for example, pass 9 to get a 512 bin FFT
+ */
+ public void setSizeLog2(int sizeLog2) {
+ this.sizeLog2 = sizeLog2;
+ output.setSize(1 << sizeLog2);
+ buffer = output.getSpectrum().getReal();
+ cursor = 0;
+ }
+
+ public int getSizeLog2() {
+ return sizeLog2;
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ if (!running) {
+ int mask = (1 << sizeLog2) - 1;
+ if (((getSynthesisEngine().getFrameCount() - offset) & mask) == 0) {
+ running = true;
+ cursor = 0;
+ }
+ }
+ // Don't use "else" because "running" may have changed in above block.
+ if (running) {
+ double[] inputs = input.getValues();
+ for (int i = start; i < limit; i++) {
+ buffer[cursor] = inputs[i] * window.get(cursor);
+ ++cursor;
+ // When it is full, do the FFT.
+ if (cursor == buffer.length) {
+ Spectrum spectrum = output.getSpectrum();
+ Arrays.fill(spectrum.getImaginary(), 0.0);
+ FourierMath.fft(buffer.length, spectrum.getReal(), spectrum.getImaginary());
+ output.advance();
+ cursor = 0;
+ }
+ }
+ }
+ }
+
+ public SpectralWindow getWindow() {
+ return window;
+ }
+
+ /**
+ * Multiply input data by this window before doing the FFT. The default is a RectangularWindow.
+ */
+ public void setWindow(SpectralWindow window) {
+ this.window = window;
+ }
+
+ /**
+ * The FFT will be performed on a frame that is a multiple of the size plus this offset.
+ *
+ * @param offset
+ */
+ public void setOffset(int offset) {
+ this.offset = offset;
+ }
+
+ public int getOffset() {
+ return offset;
+ }
+
+}
diff --git a/src/com/jsyn/unitgen/SpectralFilter.java b/src/com/jsyn/unitgen/SpectralFilter.java
new file mode 100644
index 0000000..758c8e7
--- /dev/null
+++ b/src/com/jsyn/unitgen/SpectralFilter.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2014 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.data.SpectralWindow;
+import com.jsyn.data.SpectralWindowFactory;
+import com.jsyn.data.Spectrum;
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.ports.UnitOutputPort;
+import com.jsyn.ports.UnitSpectralInputPort;
+import com.jsyn.ports.UnitSpectralOutputPort;
+
+/**
+ * Process a signal using multiple overlapping FFT and IFFT pairs. For passthrough, you can connect
+ * the spectral outputs to the spectral inputs. Or you can connect one or more SpectralProcessors
+ * between them.
+ *
+ * <pre>
+ * for (int i = 0; i &lt; numFFTs; i++) {
+ * filter.getSpectralOutput(i).connect(processors[i].input);
+ * processors[i].output.connect(filter.getSpectralInput(i));
+ * }
+ * </pre>
+ *
+ * See the example program "HearSpectralFilter.java". Note that this spectral API is experimental
+ * and may change at any time.
+ *
+ * @author Phil Burk (C) 2014 Mobileer Inc
+ * @see SpectralProcessor
+ */
+public class SpectralFilter extends Circuit implements UnitSink, UnitSource {
+ public UnitInputPort input;
+ public UnitOutputPort output;
+
+ private SpectralFFT[] ffts;
+ private SpectralIFFT[] iffts;
+ private PassThrough inlet; // fan out to FFTs
+ private PassThrough sum; // mix output of IFFTs
+
+ /**
+ * Create a default sized filter with 2 FFT/IFFT pairs and a sizeLog2 of
+ * Spectrum.DEFAULT_SIZE_LOG_2.
+ */
+ public SpectralFilter() {
+ this(2, Spectrum.DEFAULT_SIZE_LOG_2);
+ }
+
+ /**
+ * @param numFFTs number of FFT/IFFT pairs for the overlap and add
+ * @param sizeLog2 for example, use 10 to get a 1024 bin FFT, 12 for 4096
+ */
+ public SpectralFilter(int numFFTs, int sizeLog2) {
+ add(inlet = new PassThrough());
+ add(sum = new PassThrough());
+ ffts = new SpectralFFT[numFFTs];
+ iffts = new SpectralIFFT[numFFTs];
+ int offset = (1 << sizeLog2) / numFFTs;
+ for (int i = 0; i < numFFTs; i++) {
+ add(ffts[i] = new SpectralFFT(sizeLog2));
+ inlet.output.connect(ffts[i].input);
+ ffts[i].setOffset(i * offset);
+
+ add(iffts[i] = new SpectralIFFT());
+ iffts[i].output.connect(sum.input);
+ }
+ setWindow(SpectralWindowFactory.getHammingWindow(sizeLog2));
+
+ addPort(input = inlet.input);
+ addPort(output = sum.output);
+ }
+
+ public SpectralWindow getWindow() {
+ return ffts[0].getWindow();
+ }
+
+ /**
+ * Specify one window to be used for all FFTs and IFFTs. The window should be the same size as
+ * the FFTs.
+ *
+ * @param window default is HammingWindow
+ * @see SpectralWindowFactory
+ */
+ public void setWindow(SpectralWindow window) {
+ // Use the same window everywhere.
+ for (int i = 0; i < ffts.length; i++) {
+ ffts[i].setWindow(window); // TODO review, both sides or just one
+ iffts[i].setWindow(window);
+ }
+ }
+
+ @Override
+ public UnitOutputPort getOutput() {
+ return output;
+ }
+
+ @Override
+ public UnitInputPort getInput() {
+ return input;
+ }
+
+ /**
+ * @param i
+ * @return the output of the indexed FFT
+ */
+ public UnitSpectralOutputPort getSpectralOutput(int i) {
+ return ffts[i].output;
+ }
+
+ /**
+ * @param i
+ * @return the input of the indexed IFFT
+ */
+ public UnitSpectralInputPort getSpectralInput(int i) {
+ return iffts[i].input;
+ }
+}
diff --git a/src/com/jsyn/unitgen/SpectralIFFT.java b/src/com/jsyn/unitgen/SpectralIFFT.java
new file mode 100644
index 0000000..c040e52
--- /dev/null
+++ b/src/com/jsyn/unitgen/SpectralIFFT.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2013 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.data.SpectralWindow;
+import com.jsyn.data.Spectrum;
+import com.jsyn.ports.UnitOutputPort;
+import com.jsyn.ports.UnitSpectralInputPort;
+import com.softsynth.math.FourierMath;
+
+/**
+ * Periodically transform the input signal using an Inverse FFT.
+ *
+ * @author Phil Burk (C) 2013 Mobileer Inc
+ * @version 016
+ * @see SpectralFFT
+ */
+public class SpectralIFFT extends UnitGenerator {
+ public UnitSpectralInputPort input;
+ public UnitOutputPort output;
+ private Spectrum localSpectrum;
+ private double[] buffer;
+ private int cursor;
+ private SpectralWindow window = RectangularWindow.getInstance();
+
+ /* Define Unit Ports used by connect() and set(). */
+ public SpectralIFFT() {
+ addPort(output = new UnitOutputPort());
+ addPort(input = new UnitSpectralInputPort("Input"));
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] outputs = output.getValues();
+
+ if (buffer == null) {
+ if (input.isAvailable()) {
+ Spectrum spectrum = input.getSpectrum();
+ int size = spectrum.size();
+ localSpectrum = new Spectrum(size);
+ buffer = localSpectrum.getReal();
+ cursor = 0;
+ } else {
+ for (int i = start; i < limit; i++) {
+ outputs[i] = 0.0;
+ }
+ }
+ }
+
+ if (buffer != null) {
+ for (int i = start; i < limit; i++) {
+ if (cursor == 0) {
+ Spectrum spectrum = input.getSpectrum();
+ spectrum.copyTo(localSpectrum);
+ FourierMath.ifft(buffer.length, localSpectrum.getReal(),
+ localSpectrum.getImaginary());
+ }
+
+ outputs[i] = buffer[cursor] * window.get(cursor);
+ cursor += 1;
+ if (cursor == buffer.length) {
+ cursor = 0;
+ }
+ }
+ }
+ }
+
+ public SpectralWindow getWindow() {
+ return window;
+ }
+
+ /**
+ * Multiply output data by this window after doing the FFT. The default is a RectangularWindow.
+ */
+ public void setWindow(SpectralWindow window) {
+ this.window = window;
+ }
+}
diff --git a/src/com/jsyn/unitgen/SpectralProcessor.java b/src/com/jsyn/unitgen/SpectralProcessor.java
new file mode 100644
index 0000000..de96877
--- /dev/null
+++ b/src/com/jsyn/unitgen/SpectralProcessor.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2014 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.data.Spectrum;
+import com.jsyn.ports.UnitSpectralInputPort;
+import com.jsyn.ports.UnitSpectralOutputPort;
+
+/**
+ * This is a base class for implementing your own spectral processing units. You need to implement
+ * the processSpectrum() method.
+ *
+ * @author Phil Burk (C) 2014 Mobileer Inc
+ * @see Spectrum
+ */
+public abstract class SpectralProcessor extends UnitGenerator {
+ public UnitSpectralInputPort input;
+ public UnitSpectralOutputPort output;
+ private int counter;
+
+ /* Define Unit Ports used by connect() and set(). */
+ public SpectralProcessor() {
+ addPort(output = new UnitSpectralOutputPort());
+ addPort(input = new UnitSpectralInputPort());
+ }
+
+ /* Define Unit Ports used by connect() and set(). */
+ public SpectralProcessor(int size) {
+ addPort(output = new UnitSpectralOutputPort(size));
+ addPort(input = new UnitSpectralInputPort());
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ for (int i = start; i < limit; i++) {
+ if (counter == 0) {
+ if (input.isAvailable()) {
+ Spectrum inputSpectrum = input.getSpectrum();
+ Spectrum outputSpectrum = output.getSpectrum();
+ processSpectrum(inputSpectrum, outputSpectrum);
+
+ output.advance();
+ counter = inputSpectrum.size() - 1;
+ }
+ } else {
+ counter--;
+ }
+ }
+ }
+
+ /**
+ * Define this method to implement your own processor.
+ *
+ * @param inputSpectrum
+ * @param outputSpectrum
+ */
+ public abstract void processSpectrum(Spectrum inputSpectrum, Spectrum outputSpectrum);
+
+}
diff --git a/src/com/jsyn/unitgen/SquareOscillator.java b/src/com/jsyn/unitgen/SquareOscillator.java
new file mode 100644
index 0000000..aaca2d0
--- /dev/null
+++ b/src/com/jsyn/unitgen/SquareOscillator.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+/**
+ * Simple square wave oscillator.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ */
+public class SquareOscillator extends UnitOscillator {
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] frequencies = frequency.getValues();
+ double[] amplitudes = amplitude.getValues();
+ double[] outputs = output.getValues();
+
+ // Variables have a single value.
+ double currentPhase = phase.getValue();
+
+ for (int i = start; i < limit; i++) {
+ /* Generate sawtooth phasor to provide phase for square generation. */
+ double phaseIncrement = convertFrequencyToPhaseIncrement(frequencies[i]);
+ currentPhase = incrementWrapPhase(currentPhase, phaseIncrement);
+
+ double ampl = amplitudes[i];
+ // Either full negative or positive amplitude.
+ outputs[i] = (currentPhase < 0.0) ? -ampl : ampl;
+ }
+
+ // Value needs to be saved for next time.
+ phase.setValue(currentPhase);
+ }
+
+}
diff --git a/src/com/jsyn/unitgen/SquareOscillatorBL.java b/src/com/jsyn/unitgen/SquareOscillatorBL.java
new file mode 100644
index 0000000..cfe8541
--- /dev/null
+++ b/src/com/jsyn/unitgen/SquareOscillatorBL.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.engine.MultiTable;
+
+/**
+ * Band-limited square wave oscillator. This requires more CPU than a SquareOscillator but is less
+ * noisy at high frequencies.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ */
+public class SquareOscillatorBL extends SawtoothOscillatorBL {
+ @Override
+ protected double generateBL(MultiTable multiTable, double currentPhase,
+ double positivePhaseIncrement, double flevel, int i) {
+ double val1 = multiTable.calculateSawtooth(currentPhase, positivePhaseIncrement, flevel);
+
+ /* Generate second sawtooth so we can add them together. */
+ double phase2 = currentPhase + 1.0; /* 180 degrees out of phase. */
+ if (phase2 >= 1.0)
+ phase2 -= 2.0;
+ double val2 = multiTable.calculateSawtooth(phase2, positivePhaseIncrement, flevel);
+
+ /*
+ * Need to adjust amplitude based on positive phaseInc. little less than half at
+ * Nyquist/2.0!
+ */
+ final double STARTAMP = 0.92; /* Derived by viewing waveforms with TJ_SEEOSC */
+ double scale = STARTAMP - positivePhaseIncrement;
+ return scale * (val1 - val2);
+ }
+}
diff --git a/src/com/jsyn/unitgen/StereoStreamWriter.java b/src/com/jsyn/unitgen/StereoStreamWriter.java
new file mode 100644
index 0000000..072974b
--- /dev/null
+++ b/src/com/jsyn/unitgen/StereoStreamWriter.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import java.io.IOException;
+
+import com.jsyn.io.AudioOutputStream;
+import com.jsyn.ports.UnitInputPort;
+
+/**
+ * Write two samples per audio frame to an AudioOutputStream as interleaved samples.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ */
+public class StereoStreamWriter extends UnitStreamWriter {
+ public StereoStreamWriter() {
+ addPort(input = new UnitInputPort(2, "Input"));
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] leftInputs = input.getValues(0);
+ double[] rightInputs = input.getValues(1);
+ AudioOutputStream output = outputStream;
+ if (output != null) {
+ try {
+ for (int i = start; i < limit; i++) {
+ output.write(leftInputs[i]);
+ output.write(rightInputs[i]);
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ output = null;
+ }
+ }
+ }
+
+}
diff --git a/src/com/jsyn/unitgen/StochasticGrainScheduler.java b/src/com/jsyn/unitgen/StochasticGrainScheduler.java
new file mode 100644
index 0000000..1f79877
--- /dev/null
+++ b/src/com/jsyn/unitgen/StochasticGrainScheduler.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2011 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.util.PseudoRandom;
+
+/**
+ * Use a random function to schedule grains.
+ *
+ * @author Phil Burk (C) 2011 Mobileer Inc
+ */
+public class StochasticGrainScheduler implements GrainScheduler {
+ PseudoRandom pseudoRandom = new PseudoRandom();
+
+ @Override
+ public double nextDuration(double duration) {
+ return duration;
+ }
+
+ @Override
+ public double nextGap(double duration, double density) {
+ if (density < 0.00000001) {
+ density = 0.00000001;
+ }
+ double gapRange = duration * (1.0 - density) / density;
+ return pseudoRandom.random() * gapRange;
+ }
+
+}
diff --git a/src/com/jsyn/unitgen/Subtract.java b/src/com/jsyn/unitgen/Subtract.java
new file mode 100644
index 0000000..d9ca035
--- /dev/null
+++ b/src/com/jsyn/unitgen/Subtract.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+/**
+ * This unit performs a signed subtraction on its two inputs.
+ *
+ * <pre>
+ * output = inputA - inputB
+ * </pre>
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ * @version 016
+ * @see MultiplyAdd
+ * @see Subtract
+ */
+public class Subtract extends UnitBinaryOperator {
+ @Override
+ public void generate(int start, int limit) {
+ double[] aValues = inputA.getValues();
+ double[] bValues = inputB.getValues();
+ double[] outputs = output.getValues();
+
+ for (int i = start; i < limit; i++) {
+ outputs[i] = aValues[i] - bValues[i];
+ }
+ }
+}
diff --git a/src/com/jsyn/unitgen/TriangleOscillator.java b/src/com/jsyn/unitgen/TriangleOscillator.java
new file mode 100644
index 0000000..ada2d6e
--- /dev/null
+++ b/src/com/jsyn/unitgen/TriangleOscillator.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+/**
+ * Simple triangle wave oscillator.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ */
+public class TriangleOscillator extends UnitOscillator {
+ int frame;
+
+ public TriangleOscillator() {
+ super();
+ phase.setValue(-0.5);
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+
+ double[] frequencies = frequency.getValues();
+ double[] amplitudes = amplitude.getValues();
+ double[] outputs = output.getValues();
+
+ // Variables have a single value.
+ double currentPhase = phase.getValue();
+
+ for (int i = start; i < limit; i++) {
+ /* Generate sawtooth phasor to provide phase for triangle generation. */
+ double phaseIncrement = convertFrequencyToPhaseIncrement(frequencies[i]);
+ currentPhase = incrementWrapPhase(currentPhase, phaseIncrement);
+
+ /* Map phase to triangle waveform. */
+ /* 0 - 0.999 => 0.5-p => +0.5 - -0.5 */
+ /* -1.0 - 0.0 => 0.5+p => -0.5 - +0.5 */
+ double triangle = (currentPhase >= 0.0) ? (0.5 - currentPhase) : (0.5 + currentPhase);
+
+ outputs[i] = triangle * 2.0 * amplitudes[i];
+ }
+
+ // Value needs to be saved for next time.
+ phase.setValue(currentPhase);
+ }
+
+}
diff --git a/src/com/jsyn/unitgen/TunableFilter.java b/src/com/jsyn/unitgen/TunableFilter.java
new file mode 100644
index 0000000..1724ec1
--- /dev/null
+++ b/src/com/jsyn/unitgen/TunableFilter.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * Aug 26, 2009
+ * com.jsyn.engine.units.TunableFilter.java
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitInputPort;
+
+/**
+ * A UnitFilter with a frequency port.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc Translated from 'C' to Java by Lisa
+ * Tolenti.
+ */
+public abstract class TunableFilter extends UnitFilter {
+
+ private static final double DEFAULT_FREQUENCY = 400;
+ public UnitInputPort frequency;
+
+ public TunableFilter() {
+ addPort(frequency = new UnitInputPort("Frequency"));
+ frequency.setup(40.0, DEFAULT_FREQUENCY, 6000.0);
+ }
+
+}
diff --git a/src/com/jsyn/unitgen/TwoInDualOut.java b/src/com/jsyn/unitgen/TwoInDualOut.java
new file mode 100644
index 0000000..a8fea48
--- /dev/null
+++ b/src/com/jsyn/unitgen/TwoInDualOut.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2004 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.ports.UnitOutputPort;
+
+/**
+ * This unit combines two discrete inputs into a dual (stereo) output.
+ *
+ * <pre>
+ * output[0] = inputA
+ * output[1] = inputB
+ * </pre>
+ *
+ * @author (C) 2004-2009 Phil Burk, SoftSynth.com
+ */
+
+public class TwoInDualOut extends UnitGenerator {
+ public UnitInputPort inputA;
+ public UnitInputPort inputB;
+ public UnitOutputPort output;
+
+ public TwoInDualOut() {
+ addPort(inputA = new UnitInputPort("InputA"));
+ addPort(inputB = new UnitInputPort("InputB"));
+ addPort(output = new UnitOutputPort(2, "OutputB"));
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] inputAs = inputA.getValues();
+ double[] inputBs = inputB.getValues();
+ double[] output0s = output.getValues(0);
+ double[] output1s = output.getValues(1);
+
+ for (int i = start; i < limit; i++) {
+ output0s[i] = inputAs[i];
+ output1s[i] = inputBs[i];
+ }
+ }
+}
diff --git a/src/com/jsyn/unitgen/UnitBinaryOperator.java b/src/com/jsyn/unitgen/UnitBinaryOperator.java
new file mode 100644
index 0000000..c5675ff
--- /dev/null
+++ b/src/com/jsyn/unitgen/UnitBinaryOperator.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.ports.UnitOutputPort;
+
+/**
+ * Base class for binary arithmetic operators like Add and Compare.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public abstract class UnitBinaryOperator extends UnitGenerator {
+ public UnitInputPort inputA;
+ public UnitInputPort inputB;
+ public UnitOutputPort output;
+
+ /* Define Unit Ports used by connect() and set(). */
+ public UnitBinaryOperator() {
+ addPort(inputA = new UnitInputPort("InputA"));
+ addPort(inputB = new UnitInputPort("InputB"));
+ addPort(output = new UnitOutputPort("Output"));
+ }
+
+ @Override
+ public abstract void generate(int start, int limit);
+}
diff --git a/src/com/jsyn/unitgen/UnitFilter.java b/src/com/jsyn/unitgen/UnitFilter.java
new file mode 100644
index 0000000..49976ba
--- /dev/null
+++ b/src/com/jsyn/unitgen/UnitFilter.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.ports.UnitOutputPort;
+
+/**
+ * Base class for all filters.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ */
+public abstract class UnitFilter extends UnitGenerator implements UnitSink, UnitSource {
+ public UnitInputPort input;
+ public UnitOutputPort output;
+
+ /* Define Unit Ports used by connect() and set(). */
+ public UnitFilter() {
+ addPort(input = new UnitInputPort("Input"));
+ addPort(output = new UnitOutputPort("Output"));
+ }
+
+ @Override
+ public UnitInputPort getInput() {
+ return input;
+ }
+
+ @Override
+ public UnitOutputPort getOutput() {
+ return output;
+ }
+
+}
diff --git a/src/com/jsyn/unitgen/UnitGate.java b/src/com/jsyn/unitgen/UnitGate.java
new file mode 100644
index 0000000..59144c2
--- /dev/null
+++ b/src/com/jsyn/unitgen/UnitGate.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2012 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitGatePort;
+import com.jsyn.ports.UnitOutputPort;
+
+/**
+ * Base class for other envelopes.
+ *
+ * @author Phil Burk (C) 2012 Mobileer Inc
+ */
+public abstract class UnitGate extends UnitGenerator implements UnitSource {
+ /**
+ * Input that triggers the envelope. Use amplitude port if you want to connect a signal to be
+ * modulated by the envelope.
+ */
+ public UnitGatePort input;
+ public UnitOutputPort output;
+
+ public UnitGate() {
+ addPort(input = new UnitGatePort("Input"));
+ addPort(output = new UnitOutputPort("Output"));
+ }
+
+ @Override
+ public UnitOutputPort getOutput() {
+ return output;
+ }
+
+ /**
+ * Specify a unit to be disabled when the envelope finishes.
+ *
+ * @param unit
+ */
+ public void setupAutoDisable(UnitGenerator unit) {
+ input.setupAutoDisable(unit);
+ }
+
+}
diff --git a/src/com/jsyn/unitgen/UnitGenerator.java b/src/com/jsyn/unitgen/UnitGenerator.java
new file mode 100644
index 0000000..6d71270
--- /dev/null
+++ b/src/com/jsyn/unitgen/UnitGenerator.java
@@ -0,0 +1,325 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import java.io.PrintStream;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.logging.Logger;
+
+import com.jsyn.Synthesizer;
+import com.jsyn.engine.SynthesisEngine;
+import com.jsyn.ports.ConnectableInput;
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.ports.UnitOutputPort;
+import com.jsyn.ports.UnitPort;
+import com.softsynth.shared.time.TimeStamp;
+
+/**
+ * Base class for all unit generators.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ */
+public abstract class UnitGenerator {
+ protected static final double VERY_SMALL_FLOAT = 1.0e-26;
+ public static final double FALSE = 0.0;
+ public static final double TRUE = 1.0;
+ protected SynthesisEngine synthesisEngine;
+ private LinkedHashMap<String, UnitPort> ports = new LinkedHashMap<String, UnitPort>();
+ private Circuit circuit;
+ private long lastFrameCount;
+ private boolean enabled = true;
+ private static int nextId;
+ private int id = nextId++;
+ private int frameRate;
+ private double framePeriod;
+
+ static Logger logger = Logger.getLogger(UnitGenerator.class.getName());
+
+ public int getId() {
+ return id;
+ }
+
+ public int getFrameRate() {
+ // return frameRate;
+ return synthesisEngine.getFrameRate();
+ }
+
+ public double getFramePeriod() {
+ // return framePeriod; // TODO - Why does OldJSynTestSuite fail if I use this!
+ return synthesisEngine.getFramePeriod();
+ }
+
+ public void addPort(UnitPort port) {
+ port.setUnitGenerator(this);
+ // Store in a hash table by name.
+ ports.put(port.getName().toLowerCase(), port);
+ }
+
+ public void addPort(UnitPort port, String name) {
+ port.setName(name);
+ addPort(port);
+ }
+
+ public UnitPort getPortByName(String portName) {
+ return ports.get(portName.toLowerCase());
+ }
+
+ public Collection<UnitPort> getPorts() {
+ return ports.values();
+ }
+
+ /**
+ * Perform essential synthesis function.
+ *
+ * @param start offset into port buffers
+ * @param limit limit offset into port buffers for loop
+ */
+ public abstract void generate(int start, int limit);
+
+ /**
+ * Generate a full block.
+ */
+ public void generate() {
+ generate(0, Synthesizer.FRAMES_PER_BLOCK);
+ }
+
+ /**
+ * @return the synthesisEngine
+ */
+ public SynthesisEngine getSynthesisEngine() {
+ return synthesisEngine;
+ }
+
+ /**
+ * @return the Synthesizer
+ */
+ public Synthesizer getSynthesizer() {
+ return synthesisEngine;
+ }
+
+ /**
+ * @param synthesisEngine the synthesisEngine to set
+ */
+ public void setSynthesisEngine(SynthesisEngine synthesisEngine) {
+ if ((this.synthesisEngine != null) && (this.synthesisEngine != synthesisEngine)) {
+ throw new RuntimeException("Unit synthesisEngine already set.");
+ }
+ this.synthesisEngine = synthesisEngine;
+ }
+
+ public UnitGenerator getTopUnit() {
+ UnitGenerator unit = this;
+ // Climb to top of circuit hierarchy.
+ while (unit.circuit != null) {
+ unit = unit.circuit;
+ }
+ logger.fine("getTopUnit " + this + " => " + unit);
+ return unit;
+ }
+
+ protected void autoStop() {
+ synthesisEngine.autoStopUnit(getTopUnit());
+ }
+
+ /** Calculate signal based on halflife of an exponential decay. */
+ public double convertHalfLifeToMultiplier(double halfLife) {
+ if (halfLife < (2.0 * getFramePeriod())) {
+ return 1.0;
+ } else {
+ // Strangely enough, this code is valid for both PeakFollower
+ // and AsymptoticRamp.
+ return 1.0 - Math.pow(0.5, 1.0 / (halfLife * getSynthesisEngine().getFrameRate()));
+ }
+ }
+
+ protected double incrementWrapPhase(double currentPhase, double phaseIncrement) {
+ currentPhase += phaseIncrement;
+
+ if (currentPhase >= 1.0)
+ currentPhase -= 2.0;
+ else if (currentPhase < -1.0)
+ currentPhase += 2.0;
+ return currentPhase;
+ }
+
+ /** Calculate rate based on phase going from 0.0 to 1.0 in time. */
+ protected double convertTimeToRate(double time) {
+ double period2X = synthesisEngine.getInverseNyquist();
+ if (time < period2X) {
+ return 1.0;
+ } else {
+ return getFramePeriod() / time;
+ }
+ }
+
+ /** Flatten output ports so we don't output a changing signal when stopped. */
+ public void flattenOutputs() {
+ for (UnitPort port : ports.values()) {
+ if (port instanceof UnitOutputPort) {
+ ((UnitOutputPort) port).flatten();
+ }
+ }
+ }
+
+ public void setCircuit(Circuit circuit) {
+ if ((this.circuit != null) && (circuit != null)) {
+ throw new RuntimeException("Unit is already in a circuit.");
+ }
+ // logger.info( "setCircuit in unit " + this + " with circuit " + circuit );
+ this.circuit = circuit;
+ }
+
+ public Circuit getCircuit() {
+ return circuit;
+ }
+
+ public void pullData(long frameCount, int start, int limit) {
+ // Don't generate twice in case the paths merge.
+ if (enabled && (frameCount > lastFrameCount)) {
+ // Do this first to block recursion when there is a feedback loop.
+ lastFrameCount = frameCount;
+ // Then pull from all the units that are upstream.
+ for (UnitPort port : ports.values()) {
+ if (port instanceof ConnectableInput) {
+ ((ConnectableInput) port).pullData(frameCount, start, limit);
+ }
+ }
+ // Finally generate using outputs of the upstream units.
+ generate(start, limit);
+ }
+ }
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ /**
+ * If enabled, then a unit will execute if its output is connected to another unit that is
+ * executed. If not enabled then it will not execute and will not pull data from units that are
+ * connected to its inputs. Disabling a unit at the output of a tree of units can be used to
+ * turn off the entire tree, thus saving CPU cycles.
+ *
+ * @param enabled
+ * @see UnitGate.setupAutoDisabled
+ * @see start
+ */
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ if (!enabled) {
+ flattenOutputs();
+ }
+ }
+
+ /**
+ * Start executing this unit directly by adding it to a "run list" of units in the synthesis
+ * engine. This method is normally only called for the final unit in a chain, for example a
+ * LineOut. When that final unit executes it will "pull" data from any units connected to its
+ * inputs. Those units will then pull data their inputs until the entire chain is executed. If
+ * units are connected in a circle then this will be detected and the infinite recursion will be
+ * blocked.
+ *
+ * @see setEnabled
+ */
+ public void start() {
+ if (getSynthesisEngine() == null) {
+ throw new RuntimeException("This " + this.getClass().getName()
+ + " was not add()ed to a Synthesizer.");
+ }
+ getSynthesisEngine().startUnit(this);
+ }
+
+ /**
+ * Start a unit at the specified time.
+ *
+ * @param time
+ * @see start
+ */
+ public void start(double time) {
+ start(new TimeStamp(time));
+ }
+
+ /**
+ * Start a unit at the specified time.
+ *
+ * @param timeStamp
+ * @see start
+ */
+ public void start(TimeStamp timeStamp) {
+ if (getSynthesisEngine() == null) {
+ throw new RuntimeException("This " + this.getClass().getName()
+ + " was not add()ed to a Synthesizer.");
+ }
+ getSynthesisEngine().startUnit(this, timeStamp);
+ }
+
+ /**
+ * Stop a unit at the specified time.
+ *
+ * @param time
+ * @see start
+ */
+ public void stop(double time) {
+ stop(new TimeStamp(time));
+ }
+
+ public void stop() {
+ getSynthesisEngine().stopUnit(this);
+ }
+
+ public void stop(TimeStamp timeStamp) {
+ getSynthesisEngine().stopUnit(this, timeStamp);
+ }
+
+ public void setFrameRate(int rate) {
+ this.frameRate = rate;
+ this.framePeriod = 1.0 / rate;
+ }
+
+ /** Needed by UnitSink */
+ public UnitGenerator getUnitGenerator() {
+ return this;
+ }
+
+ /** Needed by UnitVoice */
+ public void setPort(String portName, double value, TimeStamp timeStamp) {
+ UnitInputPort port = (UnitInputPort) getPortByName(portName);
+ // System.out.println("setPort " + port );
+ if (port == null) {
+ logger.warning("port was null for name " + portName + ", " + this.getClass().getName());
+ } else {
+ port.set(value, timeStamp);
+ }
+ }
+
+ public void printConnections() {
+ printConnections(System.out);
+ }
+
+ public void printConnections(PrintStream out) {
+ printConnections(out, 0);
+ }
+
+ public void printConnections(PrintStream out, int level) {
+ for (UnitPort port : getPorts()) {
+ if (port instanceof UnitInputPort) {
+ ((UnitInputPort) port).printConnections(out, level);
+ }
+ }
+ }
+
+}
diff --git a/src/com/jsyn/unitgen/UnitOscillator.java b/src/com/jsyn/unitgen/UnitOscillator.java
new file mode 100644
index 0000000..4c02f09
--- /dev/null
+++ b/src/com/jsyn/unitgen/UnitOscillator.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.ports.UnitOutputPort;
+import com.jsyn.ports.UnitVariablePort;
+import com.softsynth.shared.time.TimeStamp;
+
+/**
+ * Base class for all oscillators.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ */
+public abstract class UnitOscillator extends UnitGenerator implements UnitVoice {
+ /** Frequency in Hertz. */
+ public UnitInputPort frequency;
+ public UnitInputPort amplitude;
+ public UnitVariablePort phase;
+ public UnitOutputPort output;
+
+ public static final double DEFAULT_FREQUENCY = 440.0;
+ public static final double DEFAULT_AMPLITUDE = 0x7FFF / (double) 0x8000;
+
+ /* Define Unit Ports used by connect() and set(). */
+ public UnitOscillator() {
+ addPort(frequency = new UnitInputPort("Frequency"));
+ frequency.setup(40.0, DEFAULT_FREQUENCY, 8000.0);
+ addPort(amplitude = new UnitInputPort("Amplitude", DEFAULT_AMPLITUDE));
+ addPort(phase = new UnitVariablePort("Phase"));
+ addPort(output = new UnitOutputPort("Output"));
+ }
+
+ /**
+ * Convert a frequency in Hertz to a phaseIncrement in the range -1.0 to +1.0
+ */
+ public double convertFrequencyToPhaseIncrement(double freq) {
+ double phaseIncrement;
+ try {
+ phaseIncrement = freq * synthesisEngine.getInverseNyquist();
+ } catch (NullPointerException e) {
+ throw new NullPointerException(
+ "Null Synth! You probably forgot to add this unit to the Synthesizer!");
+ }
+ // Clip to range.
+ phaseIncrement = (phaseIncrement > 1.0) ? 1.0 : ((phaseIncrement < -1.0) ? -1.0
+ : phaseIncrement);
+ return phaseIncrement;
+ }
+
+ @Override
+ public UnitOutputPort getOutput() {
+ return output;
+ }
+
+ public void noteOn(double freq, double ampl) {
+ frequency.set(freq);
+ amplitude.set(ampl);
+ }
+
+ public void noteOff() {
+ amplitude.set(0.0);
+ }
+
+ @Override
+ public void noteOff(TimeStamp timeStamp) {
+ amplitude.set(0.0, timeStamp);
+ }
+
+ @Override
+ public void noteOn(double freq, double ampl, TimeStamp timeStamp) {
+ frequency.set(freq, timeStamp);
+ amplitude.set(ampl, timeStamp);
+ }
+
+ @Override
+ public void usePreset(int presetIndex) {
+ }
+}
diff --git a/src/com/jsyn/unitgen/UnitSink.java b/src/com/jsyn/unitgen/UnitSink.java
new file mode 100644
index 0000000..3e0f55e
--- /dev/null
+++ b/src/com/jsyn/unitgen/UnitSink.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitInputPort;
+import com.softsynth.shared.time.TimeStamp;
+
+/**
+ * Interface for unit generators that have an input.
+ *
+ * @author Phil Burk, (C) 2009 Mobileer Inc
+ */
+public interface UnitSink {
+ public UnitInputPort getInput();
+
+ /**
+ * Begin execution of this unit by the Synthesizer. The input will pull data from any output
+ * port that is connected from it.
+ */
+ public void start();
+
+ public void start(TimeStamp timeStamp);
+
+ public void stop();
+
+ public void stop(TimeStamp timeStamp);
+
+ public UnitGenerator getUnitGenerator();
+}
diff --git a/src/com/jsyn/unitgen/UnitSource.java b/src/com/jsyn/unitgen/UnitSource.java
new file mode 100644
index 0000000..5ee8134
--- /dev/null
+++ b/src/com/jsyn/unitgen/UnitSource.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitOutputPort;
+
+/**
+ * Interface for things that have that have an output and an associated UnitGenerator.
+ *
+ * @author Phil Burk, (C) 2009 Mobileer Inc
+ */
+public interface UnitSource {
+ public UnitOutputPort getOutput();
+
+ public UnitGenerator getUnitGenerator();
+}
diff --git a/src/com/jsyn/unitgen/UnitStreamWriter.java b/src/com/jsyn/unitgen/UnitStreamWriter.java
new file mode 100644
index 0000000..ecc231d
--- /dev/null
+++ b/src/com/jsyn/unitgen/UnitStreamWriter.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.io.AudioOutputStream;
+import com.jsyn.ports.UnitInputPort;
+
+/**
+ * Base class for writing to an AudioOutputStream.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ */
+public abstract class UnitStreamWriter extends UnitGenerator implements UnitSink {
+ protected AudioOutputStream outputStream;
+ public UnitInputPort input;
+
+ public AudioOutputStream getOutputStream() {
+ return outputStream;
+ }
+
+ public void setOutputStream(AudioOutputStream outputStream) {
+ this.outputStream = outputStream;
+ }
+
+ @Override
+ public UnitInputPort getInput() {
+ return input;
+ }
+}
diff --git a/src/com/jsyn/unitgen/UnitVoice.java b/src/com/jsyn/unitgen/UnitVoice.java
new file mode 100644
index 0000000..3f5e6ef
--- /dev/null
+++ b/src/com/jsyn/unitgen/UnitVoice.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2011 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.util.Instrument;
+import com.jsyn.util.VoiceDescription;
+import com.softsynth.shared.time.TimeStamp;
+
+/**
+ * A voice that can be allocated and played by the VoiceAllocator.
+ *
+ * @author Phil Burk (C) 2011 Mobileer Inc
+ * @see VoiceDescription
+ * @see Instrument
+ */
+public interface UnitVoice extends UnitSource {
+ /**
+ * Play whatever you consider to be a note on this voice. Do not be constrained by traditional
+ * definitions of notes or music.
+ *
+ * @param frequency in Hz related to the perceived pitch of the note.
+ * @param amplitude generally between 0.0 and 1.0
+ * @param timeStamp when to play the note
+ */
+ void noteOn(double frequency, double amplitude, TimeStamp timeStamp);
+
+ void noteOff(TimeStamp timeStamp);
+
+ /**
+ * Typically a UnitVoice will be a subclass of UnitGenerator, which just returns "this".
+ */
+ @Override
+ public UnitGenerator getUnitGenerator();
+
+ /**
+ * Looks up a port using its name and sets the value.
+ *
+ * @param portName
+ * @param value
+ * @param timeStamp
+ */
+ void setPort(String portName, double value, TimeStamp timeStamp);
+
+ void usePreset(int presetIndex);
+}
diff --git a/src/com/jsyn/unitgen/Unzipper.java b/src/com/jsyn/unitgen/Unzipper.java
new file mode 100644
index 0000000..c776ffb
--- /dev/null
+++ b/src/com/jsyn/unitgen/Unzipper.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2014 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+/**
+ * Used inside UnitGenerators for fast smoothing of inputs.
+ *
+ * @author Phil Burk (C) 2014 Mobileer Inc
+ */
+public class Unzipper {
+ private double target;
+ private double delta;
+ private double current;
+ private int counter;
+ // About 30 msec. Power of 2 so divide should be faster.
+ private static final int NUM_STEPS = 1024;
+
+ public double smooth(double input) {
+ if (input != target) {
+ target = input;
+ delta = (target - current) / NUM_STEPS;
+ counter = NUM_STEPS;
+ }
+ if (counter > 0) {
+ if (--counter == 0) {
+ current = target;
+ } else {
+ current += delta;
+ }
+ }
+ return current;
+ }
+}
diff --git a/src/com/jsyn/unitgen/VariableRateDataReader.java b/src/com/jsyn/unitgen/VariableRateDataReader.java
new file mode 100644
index 0000000..2ef163c
--- /dev/null
+++ b/src/com/jsyn/unitgen/VariableRateDataReader.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitInputPort;
+
+public abstract class VariableRateDataReader extends SequentialDataReader {
+ /** A scaler for playback rate. Nominally 1.0. */
+ public UnitInputPort rate;
+
+ public VariableRateDataReader() {
+ super();
+ addPort(rate = new UnitInputPort("Rate", 1.0));
+ }
+}
diff --git a/src/com/jsyn/unitgen/VariableRateMonoReader.java b/src/com/jsyn/unitgen/VariableRateMonoReader.java
new file mode 100644
index 0000000..3af660e
--- /dev/null
+++ b/src/com/jsyn/unitgen/VariableRateMonoReader.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.data.FloatSample;
+import com.jsyn.data.SegmentedEnvelope;
+import com.jsyn.data.ShortSample;
+import com.jsyn.ports.UnitOutputPort;
+
+/**
+ * This reader can play any SequentialData and will interpolate between adjacent values. It can play
+ * both {@link SegmentedEnvelope envelopes} and {@link FloatSample samples}. <code><pre>
+ // Queue an envelope to the dataQueue port.
+ ampEnv.dataQueue.queue( ampEnvelope );
+</pre></code>
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ * @see FloatSample
+ * @see ShortSample
+ * @see SegmentedEnvelope
+ */
+public class VariableRateMonoReader extends VariableRateDataReader {
+ private double phase; // ranges from 0.0 to 1.0
+ private double baseIncrement;
+ private double source;
+ private double current;
+ private double target;
+ private boolean starved;
+ private boolean ranout;
+
+ public VariableRateMonoReader() {
+ super();
+ addPort(output = new UnitOutputPort("Output"));
+ starved = true;
+ baseIncrement = 1.0;
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] amplitudes = amplitude.getValues();
+ double[] rates = rate.getValues();
+ double[] outputs = output.getValues();
+
+ for (int i = start; i < limit; i++) {
+ // Decrement phase and advance through queued data until phase back
+ // in range.
+ if (phase >= 1.0) {
+ while (phase >= 1.0) {
+ source = target;
+ phase -= 1.0;
+ baseIncrement = advanceToNextFrame();
+ }
+ } else if ((i == 0) && (starved || !dataQueue.isTargetValid())) {
+ // A starved condition can only be cured at the beginning of a
+ // block.
+ source = target = current;
+ phase = 0.0;
+ baseIncrement = advanceToNextFrame();
+ }
+
+ // Interpolate along line segment.
+ current = ((target - source) * phase) + source;
+ outputs[i] = current * amplitudes[i];
+
+ double phaseIncrement = baseIncrement * rates[i];
+ phase += limitPhaseIncrement(phaseIncrement);
+ }
+
+ if (ranout) {
+ ranout = false;
+ if (dataQueue.testAndClearAutoStop()) {
+ autoStop();
+ }
+ }
+ }
+
+ public double limitPhaseIncrement(double phaseIncrement) {
+ return phaseIncrement;
+ }
+
+ private double advanceToNextFrame() {
+ // Fire callbacks before getting next value because we already got the
+ // target value previously.
+ dataQueue.firePendingCallbacks();
+ if (dataQueue.hasMore()) {
+ starved = false;
+ target = dataQueue.readNextMonoDouble(getFramePeriod());
+
+ // calculate phase increment;
+ return getFramePeriod() * dataQueue.getNormalizedRate();
+ } else {
+ starved = true;
+ ranout = true;
+ phase = 0.0;
+ return 0.0;
+ }
+ }
+
+}
diff --git a/src/com/jsyn/unitgen/VariableRateStereoReader.java b/src/com/jsyn/unitgen/VariableRateStereoReader.java
new file mode 100644
index 0000000..0f9fce8
--- /dev/null
+++ b/src/com/jsyn/unitgen/VariableRateStereoReader.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitOutputPort;
+
+/**
+ * This reader can play any SequentialData and will interpolate between adjacent values. It can play
+ * both envelopes and samples.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ */
+public class VariableRateStereoReader extends VariableRateDataReader {
+ private double phase;
+ private double baseIncrement;
+ private double source0;
+ private double current0;
+ private double target0;
+ private double source1;
+ private double current1;
+ private double target1;
+ private boolean starved;
+ private boolean ranout;
+
+ public VariableRateStereoReader() {
+ dataQueue.setNumChannels(2);
+ addPort(output = new UnitOutputPort(2, "Output"));
+ starved = true;
+ baseIncrement = 1.0;
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] amplitudes = amplitude.getValues();
+ double[] rates = rate.getValues();
+ double[] output0s = output.getValues(0);
+ double[] output1s = output.getValues(1);
+
+ for (int i = start; i < limit; i++) {
+ // Decrement phase and advance through queued data until phase back
+ // in range.
+ if (phase >= 1.0) {
+ while (phase >= 1.0) {
+ source0 = target0;
+ source1 = target1;
+ phase -= 1.0;
+ baseIncrement = advanceToNextFrame();
+ }
+ } else if ((i == 0) && (starved || !dataQueue.isTargetValid())) {
+ // A starved condition can only be cured at the beginning of a block.
+ source0 = target0 = current0;
+ source1 = target1 = current1;
+ phase = 0.0;
+ baseIncrement = advanceToNextFrame();
+ }
+
+ // Interpolate along line segment.
+ current0 = ((target0 - source0) * phase) + source0;
+ output0s[i] = current0 * amplitudes[i];
+ current1 = ((target1 - source1) * phase) + source1;
+ output1s[i] = current1 * amplitudes[i];
+
+ double phaseIncrement = baseIncrement * rates[i];
+ phase += limitPhaseIncrement(phaseIncrement);
+ }
+
+ if (ranout) {
+ ranout = false;
+ if (dataQueue.testAndClearAutoStop()) {
+ autoStop();
+ }
+ }
+ }
+
+ public double limitPhaseIncrement(double phaseIncrement) {
+ return phaseIncrement;
+ }
+
+ private double advanceToNextFrame() {
+ dataQueue.firePendingCallbacks();
+ if (dataQueue.hasMore()) {
+ starved = false;
+
+ dataQueue.beginFrame(getFramePeriod());
+ target0 = dataQueue.readCurrentChannelDouble(0);
+ target1 = dataQueue.readCurrentChannelDouble(1);
+ dataQueue.endFrame();
+
+ // calculate phase increment;
+ return synthesisEngine.getFramePeriod() * dataQueue.getNormalizedRate();
+ } else {
+ starved = true;
+ ranout = true;
+ phase = 0.0;
+ return 0.0;
+ }
+ }
+
+}
diff --git a/src/com/jsyn/unitgen/WhiteNoise.java b/src/com/jsyn/unitgen/WhiteNoise.java
new file mode 100644
index 0000000..b708e92
--- /dev/null
+++ b/src/com/jsyn/unitgen/WhiteNoise.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 1997 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.ports.UnitOutputPort;
+import com.jsyn.util.PseudoRandom;
+
+/**
+ * WhiteNoise unit. This unit uses a pseudo-random number generator to produce white noise. The
+ * energy in a white noise signal is distributed evenly across the spectrum. A new random number is
+ * generated every frame.
+ *
+ * @author (C) 1997-2011 Phil Burk, Mobileer Inc
+ * @see RedNoise
+ */
+public class WhiteNoise extends UnitGenerator implements UnitSource {
+ private PseudoRandom randomNum;
+ public UnitInputPort amplitude;
+ public UnitOutputPort output;
+
+ public WhiteNoise() {
+ randomNum = new PseudoRandom();
+ addPort(amplitude = new UnitInputPort("Amplitude", UnitOscillator.DEFAULT_AMPLITUDE));
+ addPort(output = new UnitOutputPort("Output"));
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] amplitudes = amplitude.getValues();
+ double[] outputs = output.getValues();
+
+ for (int i = start; i < limit; i++) {
+ outputs[i] = randomNum.nextRandomDouble() * amplitudes[i];
+ }
+ }
+
+ @Override
+ public UnitOutputPort getOutput() {
+ return output;
+ }
+}
diff --git a/src/com/jsyn/unitgen/ZeroCrossingCounter.java b/src/com/jsyn/unitgen/ZeroCrossingCounter.java
new file mode 100644
index 0000000..6cd36ea
--- /dev/null
+++ b/src/com/jsyn/unitgen/ZeroCrossingCounter.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 1997 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.ports.UnitOutputPort;
+
+/**
+ * Count zero crossings. Handy for unit tests.
+ *
+ * @author (C) 1997-2011 Phil Burk, Mobileer Inc
+ */
+public class ZeroCrossingCounter extends UnitGenerator {
+ private static final double THRESHOLD = 0.0001;
+ public UnitInputPort input;
+ public UnitOutputPort output;
+
+ private long count;
+ private boolean armed;
+
+ /* Define Unit Ports used by connect() and set(). */
+ public ZeroCrossingCounter() {
+ addPort(input = new UnitInputPort("Input"));
+ addPort(output = new UnitOutputPort("Output"));
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] inputs = input.getValues();
+ double[] outputs = output.getValues();
+
+ for (int i = start; i < limit; i++) {
+ double value = inputs[i];
+ if (value < -THRESHOLD) {
+ armed = true;
+ } else if (armed & (value > THRESHOLD)) {
+ ++count;
+ armed = false;
+ }
+ outputs[i] = value;
+ }
+ }
+
+ public long getCount() {
+ return count;
+ }
+}
diff --git a/src/com/jsyn/util/AudioSampleLoader.java b/src/com/jsyn/util/AudioSampleLoader.java
new file mode 100644
index 0000000..b665933
--- /dev/null
+++ b/src/com/jsyn/util/AudioSampleLoader.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+
+import com.jsyn.data.FloatSample;
+
+public interface AudioSampleLoader {
+ /**
+ * Load a FloatSample from a File object.
+ */
+ public FloatSample loadFloatSample(File fileIn) throws IOException;
+
+ /**
+ * Load a FloatSample from an InputStream. This is handy when loading Resources from a JAR file.
+ */
+ public FloatSample loadFloatSample(InputStream inputStream) throws IOException;
+
+ /**
+ * Load a FloatSample from a URL.. This is handy when loading Resources from a website.
+ */
+ public FloatSample loadFloatSample(URL url) throws IOException;
+
+}
diff --git a/src/com/jsyn/util/AudioStreamReader.java b/src/com/jsyn/util/AudioStreamReader.java
new file mode 100644
index 0000000..5a725c3
--- /dev/null
+++ b/src/com/jsyn/util/AudioStreamReader.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util;
+
+import com.jsyn.Synthesizer;
+import com.jsyn.io.AudioFifo;
+import com.jsyn.io.AudioInputStream;
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.unitgen.MonoStreamWriter;
+import com.jsyn.unitgen.StereoStreamWriter;
+import com.jsyn.unitgen.UnitStreamWriter;
+
+/**
+ * Reads audio signals from the background engine to a foreground application through an AudioFifo.
+ * Connect to the input port returned by getInput().
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class AudioStreamReader implements AudioInputStream {
+ private UnitStreamWriter streamWriter;
+ private AudioFifo fifo;
+
+ public AudioStreamReader(Synthesizer synth, int samplesPerFrame) {
+ if (samplesPerFrame == 1) {
+ streamWriter = new MonoStreamWriter();
+ } else if (samplesPerFrame == 2) {
+ streamWriter = new StereoStreamWriter();
+ } else {
+ throw new IllegalArgumentException("Only 1 or 2 samplesPerFrame supported.");
+ }
+ synth.add(streamWriter);
+
+ fifo = new AudioFifo();
+ fifo.setWriteWaitEnabled(!synth.isRealTime());
+ fifo.setReadWaitEnabled(true);
+ fifo.allocate(32 * 1024);
+ streamWriter.setOutputStream(fifo);
+ streamWriter.start();
+ }
+
+ public UnitInputPort getInput() {
+ return streamWriter.input;
+ }
+
+ /** How many values are available to read without blocking? */
+ @Override
+ public int available() {
+ return fifo.available();
+ }
+
+ @Override
+ public void close() {
+ fifo.close();
+ }
+
+ @Override
+ public double read() {
+ return fifo.read();
+ }
+
+ @Override
+ public int read(double[] buffer) {
+ return fifo.read(buffer);
+ }
+
+ @Override
+ public int read(double[] buffer, int start, int count) {
+ return fifo.read(buffer, start, count);
+ }
+
+}
diff --git a/src/com/jsyn/util/AutoCorrelator.java b/src/com/jsyn/util/AutoCorrelator.java
new file mode 100644
index 0000000..5512abb
--- /dev/null
+++ b/src/com/jsyn/util/AutoCorrelator.java
@@ -0,0 +1,329 @@
+/*
+ * Copyright 2004 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util;
+
+/**
+ * Calculate period of a repeated waveform in an array. This algorithm is based on a normalized
+ * auto-correlation function as dewscribed in: "A Smarter Way to Find Pitch" by Philip McLeod and
+ * Geoff Wyvill
+ *
+ * @author (C) 2004 Mobileer, PROPRIETARY and CONFIDENTIAL
+ */
+public class AutoCorrelator implements SignalCorrelator {
+ // A higher number will reject suboctaves more.
+ private static final float SUB_OCTAVE_REJECTION_FACTOR = 0.0005f;
+ // We can focus our analysis on the maxima
+ private static final int STATE_SEEKING_NEGATIVE = 0;
+ private static final int STATE_SEEKING_POSITIVE = 1;
+ private static final int STATE_SEEKING_MAXIMUM = 2;
+ private static final int[] tauAdvanceByState = {
+ 4, 2, 1
+ };
+ private int state;
+
+ private float[] buffer;
+ // double buffer the diffs so we can view them
+ private float[] diffs;
+ private float[] diffs1;
+ private float[] diffs2;
+ private int cursor = -1;
+ private int tau;
+
+ private float sumProducts;
+ private float sumSquares;
+ private float localMaximum;
+ private int localPosition;
+ private float bestMaximum;
+ private int bestPosition;
+ private int peakCounter;
+ // This factor was found empirically to reduce a systematic offset in the pitch.
+ private float pitchCorrectionFactor = 0.99988f;
+
+ // Results of analysis.
+ private double period;
+ private double confidence;
+ private int minPeriod = 2;
+ private boolean bufferValid;
+ private double previousSample = 0.0;
+ private int maxWindowSize;
+ private float noiseThreshold = 0.001f;
+
+ public AutoCorrelator(int numFrames) {
+ buffer = new float[numFrames];
+ maxWindowSize = buffer.length / 2;
+ diffs1 = new float[2 + numFrames / 2];
+ diffs2 = new float[diffs1.length];
+ diffs = diffs1;
+ period = minPeriod;
+ reset();
+ }
+
+ // Scan assuming we will not wrap around the buffer.
+ private void rawDeltaScan(int last1, int last2, int count, int stride) {
+ for (int k = 0; k < count; k += stride) {
+ float d1 = buffer[last1 - k];
+ float d2 = buffer[last2 - k];
+ sumProducts += d1 * d2;
+ sumSquares += ((d1 * d1) + (d2 * d2));
+ }
+ }
+
+ // Do correlation when we know the splitLast will wrap around.
+ private void splitDeltaScan(int last1, int splitLast, int count, int stride) {
+ int c1 = splitLast;
+ rawDeltaScan(last1, splitLast, c1, stride);
+ rawDeltaScan(last1 - c1, buffer.length - 1, count - c1, stride);
+ }
+
+ private void checkDeltaScan(int last1, int last2, int count, int stride) {
+ if (count > last2) {
+ int c1 = last2;
+ // Use recursion with reverse indexes to handle a double split.
+ checkDeltaScan(last2, last1, c1, stride);
+ checkDeltaScan(buffer.length - 1, last1 - c1, count - c1, stride);
+ } else if (count > last1) {
+ splitDeltaScan(last2, last1, count, stride);
+ } else {
+ rawDeltaScan(last1, last2, count, stride);
+ }
+ }
+
+ // Perform correlation. Handle circular buffer wrap around.
+ // Normalized square difference function between -1.0 and +1.0.
+ private float topScan(int last1, int tau, int count, int stride) {
+ final float minimumResult = 0.00000001f;
+
+ int last2 = last1 - tau;
+ if (last2 < 0) {
+ last2 += buffer.length;
+ }
+ sumProducts = 0.0f;
+ sumSquares = 0.0f;
+ checkDeltaScan(last1, last2, count, stride);
+ // Prevent divide by zero.
+ if (sumSquares < minimumResult) {
+ return minimumResult;
+ }
+ float correction = (float) Math.pow(pitchCorrectionFactor, tau);
+ float result = (float) (2.0 * sumProducts / sumSquares) * correction;
+
+ return result;
+ }
+
+ // Prepare for a new calculation.
+ private void reset() {
+ switchDiffs();
+ int i = 0;
+ for (; i < minPeriod; i++) {
+ diffs[i] = 1.0f;
+ }
+ for (; i < diffs.length; i++) {
+ diffs[i] = 0.0f;
+ }
+ tau = minPeriod;
+ state = STATE_SEEKING_NEGATIVE;
+ peakCounter = 0;
+ bestMaximum = -1.0f;
+ bestPosition = -1;
+ }
+
+ // Analyze new diff result. Incremental peak detection.
+ private void nextPeakAnalysis(int index) {
+ // Scale low frequency correlation down to reduce suboctave matching.
+ // Note that this has a side effect of reducing confidence value for low frequency sounds.
+ float value = diffs[index] * (1.0f - (index * SUB_OCTAVE_REJECTION_FACTOR));
+ switch (state) {
+ case STATE_SEEKING_NEGATIVE:
+ if (value < -0.01f) {
+ state = STATE_SEEKING_POSITIVE;
+ }
+ break;
+ case STATE_SEEKING_POSITIVE:
+ if (value > 0.2f) {
+ state = STATE_SEEKING_MAXIMUM;
+ localMaximum = value;
+ localPosition = index;
+ }
+ break;
+ case STATE_SEEKING_MAXIMUM:
+ if (value > localMaximum) {
+ localMaximum = value;
+ localPosition = index;
+ } else if (value < -0.1f) {
+ peakCounter += 1;
+ if (localMaximum > bestMaximum) {
+ bestMaximum = localMaximum;
+ bestPosition = localPosition;
+ }
+ state = STATE_SEEKING_POSITIVE;
+ }
+ break;
+ }
+ }
+
+ /**
+ * Generate interpolated maximum from index of absolute maximum using three point analysis.
+ */
+ private double findPreciseMaximum(int indexMax) {
+ if (indexMax < 3) {
+ return 3.0;
+ }
+ if (indexMax == (diffs.length - 1)) {
+ return indexMax;
+ }
+ // Get 3 adjacent values.
+ double d1 = diffs[indexMax - 1];
+ double d2 = diffs[indexMax];
+ double d3 = diffs[indexMax + 1];
+
+ return interpolatePeak(d1, d2, d3) + indexMax;
+ }
+
+ // return offset between -1.0 and +1.0 from center
+ protected static double interpolatePeak(double d1, double d2, double d3) {
+ // System.out.println( " d1 = " + d1 + ", d2 = " + d2 + ", d3 = "
+ // + d3 );
+ // Make sure d2 is a maximum or result will blow up.
+ if (d1 > d2)
+ return -1.0;
+ else if (d3 > d2)
+ return 1.0;
+
+ // The interpolated maximum should be the same
+ // point where the line between slopes crosses zero.
+ double y2 = d3 - d2;
+ double y1 = d2 - d1;
+ // System.out.println(" y1 = " + y1 + ", y2 = " + y2 );
+ // Derive equations for interpolated maximum.
+ // y = ax + b
+ // when y is zero, x = -b/a
+ // y2 = a*x2 + b
+ // y1 = a*x1 + b
+ // b = y2-a*x2
+ // y1 = a*x1 + (y2 - a*x2)
+ // y1 - y2 = a*(x1 - x2) ; x1 and x2 are one from each other
+ // y1 - y2 = a*(-1)
+ // a = y2 - y1
+ // for zero crossing:
+ // 0 = ax+b
+ // -ax = b
+ // x = -b / a
+ // = -(y2-a*x2)/a
+ // = (a*x2 -y2)/a
+ // = x2 - y2/a
+ // = x2 - (y2/(y2-y1))
+ double x2 = 0.5;
+ double precise = x2 - (y2 / (y2 - y1));
+ return precise;
+ }
+
+ // Calculate a little more for each sample.
+ // This spreads the CPU load out more evenly.
+ private boolean incrementalAnalysis() {
+ boolean updated = false;
+ if (bufferValid) {
+ // int windowSize = maxWindowSize;
+ // Interpolate between tau and maxWindowsSize based on confidence.
+ // If confidence is low then use bigger window.
+ int windowSize = (int) ((tau * confidence) + (maxWindowSize * (1.0 - confidence)));
+
+ int stride = 1;
+ // int stride = (windowSize / 32) + 1;
+
+ diffs[tau] = topScan(cursor, tau, windowSize, stride);
+
+ // Check to see if the signal is strong enough to analyze.
+ // Look at sumPeriods on first correlation.
+ if ((tau == minPeriod) && (sumProducts < noiseThreshold)) {
+ // Update if we are dropping to zero confidence.
+ boolean result = (confidence > 0.0);
+ confidence = 0.0;
+ return result;
+ }
+
+ nextPeakAnalysis(tau);
+
+ // Reuse calculated values if we are not near a peak.
+ tau += 1;
+ int advance = tauAdvanceByState[state] - 1;
+ while ((advance > 0) && (tau < diffs.length)) {
+ diffs[tau] = diffs[tau - 1];
+ tau++;
+ advance--;
+ }
+
+ if ((peakCounter >= 4) || (tau >= maxWindowSize)) {
+ if (bestMaximum > 0.0) {
+ period = findPreciseMaximum(bestPosition);
+ // clip into range 0.0 to 1.0, low values are really bogus
+ confidence = (bestMaximum < 0.0) ? 0.0 : bestMaximum;
+ } else {
+ confidence = 0.0;
+ }
+ updated = true;
+ reset();
+ }
+ }
+ return updated;
+ }
+
+ @Override
+ public float[] getDiffs() {
+ // Return diffs that are not currently being used
+ return (diffs == diffs1) ? diffs2 : diffs1;
+ }
+
+ private void switchDiffs() {
+ diffs = (diffs == diffs1) ? diffs2 : diffs1;
+ }
+
+ @Override
+ public boolean addSample(double value) {
+ boolean updated = false;
+ double average = (value + previousSample) * 0.5;
+ previousSample = value;
+
+ cursor += 1;
+ if (cursor == buffer.length) {
+ cursor = 0;
+ bufferValid = true;
+ }
+ buffer[cursor] = (float) average;
+
+ updated = incrementalAnalysis();
+
+ return updated;
+ }
+
+ @Override
+ public double getPeriod() {
+ return period;
+ }
+
+ @Override
+ public double getConfidence() {
+ return confidence;
+ }
+
+ public float getPitchCorrectionFactor() {
+ return pitchCorrectionFactor;
+ }
+
+ public void setPitchCorrectionFactor(float pitchCorrectionFactor) {
+ this.pitchCorrectionFactor = pitchCorrectionFactor;
+ }
+}
diff --git a/src/com/jsyn/util/Instrument.java b/src/com/jsyn/util/Instrument.java
new file mode 100644
index 0000000..8a53304
--- /dev/null
+++ b/src/com/jsyn/util/Instrument.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2011 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util;
+
+import com.softsynth.shared.time.TimeStamp;
+
+/**
+ * A note player that references one or more voices by a noteNumber. This is similar to the MIDI
+ * protocol that references voices by an integer pitch or keyIndex.
+ *
+ * @author Phil Burk (C) 2011 Mobileer Inc
+ */
+public interface Instrument {
+ // This will be applied to the voice when noteOn is called.
+ void usePreset(int presetIndex, TimeStamp timeStamp);
+
+ public void noteOn(int tag, double frequency, double amplitude, TimeStamp timeStamp);
+
+ public void noteOff(int tag, TimeStamp timeStamp);
+
+ public void setPort(int tag, String portName, double value, TimeStamp timeStamp);
+
+ public void allNotesOff(TimeStamp timeStamp);
+}
diff --git a/src/com/jsyn/util/InstrumentLibrary.java b/src/com/jsyn/util/InstrumentLibrary.java
new file mode 100644
index 0000000..65113dc
--- /dev/null
+++ b/src/com/jsyn/util/InstrumentLibrary.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2011 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util;
+
+import com.jsyn.swing.InstrumentBrowser;
+
+/**
+ * A library of instruments that can be used to play notes.
+ *
+ * @author Phil Burk (C) 2011 Mobileer Inc
+ * @see InstrumentBrowser
+ */
+
+public interface InstrumentLibrary {
+ public String getName();
+
+ public VoiceDescription[] getVoiceDescriptions();
+}
diff --git a/src/com/jsyn/util/JavaSoundSampleLoader.java b/src/com/jsyn/util/JavaSoundSampleLoader.java
new file mode 100644
index 0000000..9376c96
--- /dev/null
+++ b/src/com/jsyn/util/JavaSoundSampleLoader.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2011 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+
+import javax.sound.sampled.AudioFormat;
+import javax.sound.sampled.AudioInputStream;
+import javax.sound.sampled.AudioSystem;
+import javax.sound.sampled.UnsupportedAudioFileException;
+
+import com.jsyn.data.FloatSample;
+
+/**
+ * Internal class for loading audio samples. Use SampleLoader instead.
+ *
+ * @author Phil Burk (C) 2011 Mobileer Inc
+ */
+class JavaSoundSampleLoader implements AudioSampleLoader {
+ /**
+ * Load a FloatSample from a File object.
+ */
+ @Override
+ public FloatSample loadFloatSample(File fileIn) throws IOException {
+ try {
+ return loadFloatSample(AudioSystem.getAudioInputStream(fileIn));
+ } catch (UnsupportedAudioFileException e) {
+ throw new IOException(e);
+ }
+ }
+
+ /**
+ * Load a FloatSample from an InputStream. This is handy when loading Resources from a JAR file.
+ */
+ @Override
+ public FloatSample loadFloatSample(InputStream inputStream) throws IOException {
+ try {
+ return loadFloatSample(AudioSystem.getAudioInputStream(inputStream));
+ } catch (UnsupportedAudioFileException e) {
+ throw new IOException(e);
+ }
+ }
+
+ /**
+ * Load a FloatSample from a URL.. This is handy when loading Resources from a website.
+ */
+ @Override
+ public FloatSample loadFloatSample(URL url) throws IOException {
+ try {
+ return loadFloatSample(AudioSystem.getAudioInputStream(url));
+ } catch (UnsupportedAudioFileException e) {
+ throw new IOException(e);
+ }
+ }
+
+ private FloatSample loadFloatSample(javax.sound.sampled.AudioInputStream audioInputStream)
+ throws IOException, UnsupportedAudioFileException {
+ float[] floatData = null;
+ FloatSample sample = null;
+ int bytesPerFrame = audioInputStream.getFormat().getFrameSize();
+ if (bytesPerFrame == AudioSystem.NOT_SPECIFIED) {
+ // some audio formats may have unspecified frame size
+ // in that case we may read any amount of bytes
+ bytesPerFrame = 1;
+ }
+ AudioFormat format = audioInputStream.getFormat();
+ System.out.println("Format = " + format);
+ if (format.getEncoding() == AudioFormat.Encoding.PCM_SIGNED) {
+ floatData = loadSignedPCM(audioInputStream);
+ }
+ sample = new FloatSample(floatData, format.getChannels());
+ sample.setFrameRate(format.getFrameRate());
+ return sample;
+ }
+
+ private float[] loadSignedPCM(AudioInputStream audioInputStream) throws IOException,
+ UnsupportedAudioFileException {
+ int totalSamplesRead = 0;
+ AudioFormat format = audioInputStream.getFormat();
+ int numFrames = (int) audioInputStream.getFrameLength();
+ int numSamples = format.getChannels() * numFrames;
+ float[] data = new float[numSamples];
+ final int bytesPerFrame = format.getFrameSize();
+ // Set an arbitrary buffer size of 1024 frames.
+ int numBytes = 1024 * bytesPerFrame;
+ byte[] audioBytes = new byte[numBytes];
+ int numBytesRead = 0;
+ int numFramesRead = 0;
+ // Try to read numBytes bytes from the file.
+ while ((numBytesRead = audioInputStream.read(audioBytes)) != -1) {
+ int bytesRemainder = numBytesRead % bytesPerFrame;
+ if (bytesRemainder != 0) {
+ // TODO Read until you get enough data.
+ throw new IOException("Read partial block of sample data!");
+ }
+
+ if (audioInputStream.getFormat().getSampleSizeInBits() == 16) {
+ if (format.isBigEndian()) {
+ SampleLoader.decodeBigI16ToF32(audioBytes, 0, numBytesRead, data,
+ totalSamplesRead);
+ } else {
+ SampleLoader.decodeLittleI16ToF32(audioBytes, 0, numBytesRead, data,
+ totalSamplesRead);
+ }
+ } else if (audioInputStream.getFormat().getSampleSizeInBits() == 24) {
+ if (format.isBigEndian()) {
+ SampleLoader.decodeBigI24ToF32(audioBytes, 0, numBytesRead, data,
+ totalSamplesRead);
+ } else {
+ SampleLoader.decodeLittleI24ToF32(audioBytes, 0, numBytesRead, data,
+ totalSamplesRead);
+ }
+ } else if (audioInputStream.getFormat().getSampleSizeInBits() == 32) {
+ if (format.isBigEndian()) {
+ SampleLoader.decodeBigI32ToF32(audioBytes, 0, numBytesRead, data,
+ totalSamplesRead);
+ } else {
+ SampleLoader.decodeLittleI32ToF32(audioBytes, 0, numBytesRead, data,
+ totalSamplesRead);
+ }
+ } else {
+ throw new UnsupportedAudioFileException(
+ "Only 16, 24 or 32 bit PCM samples supported.");
+ }
+
+ // Calculate the number of frames actually read.
+ numFramesRead = numBytesRead / bytesPerFrame;
+ totalSamplesRead += numFramesRead * format.getChannels();
+ }
+ return data;
+ }
+
+}
diff --git a/src/com/jsyn/util/JavaTools.java b/src/com/jsyn/util/JavaTools.java
new file mode 100644
index 0000000..65e3dd1
--- /dev/null
+++ b/src/com/jsyn/util/JavaTools.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util;
+
+public class JavaTools {
+
+ @SuppressWarnings("rawtypes")
+ public static Class loadClass(String className, boolean verbose) {
+ Class newClass = null;
+ try {
+ newClass = Class.forName(className);
+ } catch (Throwable e) {
+ if (verbose)
+ System.out.println("Caught " + e);
+ }
+ if (newClass == null) {
+ try {
+ ClassLoader systemLoader = ClassLoader.getSystemClassLoader();
+ newClass = Class.forName(className, true, systemLoader);
+ } catch (Throwable e) {
+ if (verbose)
+ System.out.println("Caught " + e);
+ }
+ }
+ return newClass;
+ }
+
+ /**
+ * First try Class.forName(). If this fails, try Class.forName() using
+ * ClassLoader.getSystemClassLoader().
+ *
+ * @return Class or null
+ */
+ @SuppressWarnings("rawtypes")
+ public static Class loadClass(String className) {
+ /**
+ * First try Class.forName(). If this fails, try Class.forName() using
+ * ClassLoader.getSystemClassLoader().
+ *
+ * @return Class or null
+ */
+ return loadClass(className, true);
+ }
+
+}
diff --git a/src/com/jsyn/util/PolyphonicInstrument.java b/src/com/jsyn/util/PolyphonicInstrument.java
new file mode 100644
index 0000000..8501554
--- /dev/null
+++ b/src/com/jsyn/util/PolyphonicInstrument.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2011 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util;
+
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.ports.UnitOutputPort;
+import com.jsyn.ports.UnitPort;
+import com.jsyn.unitgen.Circuit;
+import com.jsyn.unitgen.Multiply;
+import com.jsyn.unitgen.PassThrough;
+import com.jsyn.unitgen.UnitGenerator;
+import com.jsyn.unitgen.UnitSource;
+import com.jsyn.unitgen.UnitVoice;
+import com.softsynth.shared.time.TimeStamp;
+
+/**
+ * The API for this class is likely to change. Please comment on its usefulness.
+ *
+ * @author Phil Burk (C) 2011 Mobileer Inc
+ */
+
+public class PolyphonicInstrument extends Circuit implements UnitSource, Instrument {
+ private Multiply mixer;
+ private UnitVoice[] voices;
+ private VoiceAllocator voiceAllocator;
+ public UnitInputPort amplitude;
+
+ public PolyphonicInstrument(UnitVoice[] voices) {
+ this.voices = voices;
+ voiceAllocator = new VoiceAllocator(voices);
+ add(mixer = new Multiply());
+ // Mix all the voices to one output.
+ for (UnitVoice voice : voices) {
+ UnitGenerator unit = voice.getUnitGenerator();
+ boolean wasEnabled = unit.isEnabled();
+ // This overrides the enabled property of the voice.
+ add(unit);
+ voice.getOutput().connect(mixer.inputA);
+ // restore
+ unit.setEnabled(wasEnabled);
+ }
+
+ addPort(amplitude = mixer.inputB, "Amplitude");
+ amplitude.setup(0.0001, 0.4, 2.0);
+ }
+
+ /**
+ * Connect a PassThrough unit to the input ports of the voices so that they can be controlled
+ * together using a single port. Note that this will prevent their individual use. So the
+ * "Frequency" and "Amplitude" ports are excluded. Note that this method is a bit funky and is
+ * likely to change.
+ */
+ public void exportAllInputPorts() {
+ // Iterate through the ports.
+ for (UnitPort port : voices[0].getUnitGenerator().getPorts()) {
+ if (port instanceof UnitInputPort) {
+ UnitInputPort inputPort = (UnitInputPort) port;
+ String voicePortName = inputPort.getName();
+ // FIXME Need better way to identify ports that are per note.
+ if (!voicePortName.equals("Frequency") && !voicePortName.equals("Amplitude")) {
+ exportNamedInputPort(voicePortName);
+ }
+ }
+ }
+ }
+
+ /**
+ * Create a UnitInputPort for the circuit that is connected to the named port on each voice
+ * through a PassThrough unit. This allows you to control all of the voices at once.
+ *
+ * @param portName
+ * @see exportAllInputPorts
+ */
+ public void exportNamedInputPort(String portName) {
+ UnitInputPort voicePort = null;
+ PassThrough fanout = new PassThrough();
+ for (UnitVoice voice : voices) {
+ voicePort = (UnitInputPort) voice.getUnitGenerator().getPortByName(portName);
+ fanout.output.connect(voicePort);
+ }
+ if (voicePort != null) {
+ addPort(fanout.input, portName);
+ fanout.input.setup(voicePort);
+ }
+ }
+
+ @Override
+ public UnitOutputPort getOutput() {
+ return mixer.output;
+ }
+
+ // FIXME - no timestamp on UnitVoice
+ @Override
+ public void usePreset(int presetIndex) {
+ usePreset(presetIndex, getSynthesisEngine().createTimeStamp());
+ }
+
+ // FIXME - no timestamp on UnitVoice
+ @Override
+ public void usePreset(int presetIndex, TimeStamp timeStamp) {
+ // Apply preset to all voices.
+ for (UnitVoice voice : voices) {
+ voice.usePreset(presetIndex);
+ }
+ // Then copy values from first voice to instrument.
+ for (UnitPort port : voices[0].getUnitGenerator().getPorts()) {
+ if (port instanceof UnitInputPort) {
+ UnitInputPort inputPort = (UnitInputPort) port;
+ // FIXME Need better way to identify ports that are per note.
+ UnitInputPort fanPort = (UnitInputPort) getPortByName(inputPort.getName());
+ if ((fanPort != null) && (fanPort != amplitude)) {
+ fanPort.set(inputPort.get());
+ }
+ }
+ }
+ }
+
+ @Override
+ public void noteOn(int tag, double frequency, double amplitude, TimeStamp timeStamp) {
+ voiceAllocator.noteOn(tag, frequency, amplitude, timeStamp);
+ }
+
+ @Override
+ public void noteOff(int tag, TimeStamp timeStamp) {
+ voiceAllocator.noteOff(tag, timeStamp);
+ }
+
+ @Override
+ public void setPort(int tag, String portName, double value, TimeStamp timeStamp) {
+ voiceAllocator.setPort(tag, portName, value, timeStamp);
+ }
+
+ @Override
+ public void allNotesOff(TimeStamp timeStamp) {
+ voiceAllocator.allNotesOff(timeStamp);
+ }
+
+ public synchronized boolean isOn(int tag) {
+ return voiceAllocator.isOn(tag);
+ }
+}
diff --git a/src/com/jsyn/util/PseudoRandom.java b/src/com/jsyn/util/PseudoRandom.java
new file mode 100644
index 0000000..b7f619c
--- /dev/null
+++ b/src/com/jsyn/util/PseudoRandom.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * Sep 9, 2009
+ * com.jsyn.engine.units.SynthRandom.java
+ */
+
+package com.jsyn.util;
+
+import java.util.Random;
+
+/**
+ * Pseudo-random numbers using predictable and fast linear-congruential method.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc Translated from 'C' to Java by Lisa
+ * Tolenti.
+ */
+public class PseudoRandom {
+ // We must shift 1L or else we get a negative number!
+ private static final double INT_TO_DOUBLE = (1.0 / (1L << 31));
+ private long seed = 99887766;
+
+ /**
+ * Create an instance of SynthRandom.
+ */
+ public PseudoRandom() {
+ this(new Random().nextInt());
+ }
+
+ /**
+ * Create an instance of PseudoRandom.
+ */
+ public PseudoRandom(int seed) {
+ setSeed(seed);
+ }
+
+ public void setSeed(int seed) {
+ this.seed = ((long) seed) & 0xFFFFFFFF;
+ }
+
+ public int getSeed() {
+ return (int) seed;
+ }
+
+ /**
+ * Returns the next random double from 0.0 to 1.0
+ *
+ * @return value from 0.0 to 1.0
+ */
+ public double random() {
+ int positiveInt = nextRandomInteger() & 0x7FFFFFFF;
+ return positiveInt * INT_TO_DOUBLE;
+ }
+
+ /**
+ * Returns the next random double from -1.0 to 1.0
+ *
+ * @return value from -1.0 to 1.0
+ */
+ public double nextRandomDouble() {
+ return nextRandomInteger() * INT_TO_DOUBLE;
+ }
+
+ /** Calculate random 32 bit number using linear-congruential method. */
+ public int nextRandomInteger() {
+ long rand = (seed * 196314165) + 907633515;
+ seed = rand & 0x00000000FFFFFFFFL;
+ return (int) rand;
+ }
+
+ public int choose(int range) {
+ long bigRandom = nextRandomInteger() & 0x7FFFFFFF;
+ long temp = bigRandom * range;
+ return (int) (temp >> 31);
+ }
+}
diff --git a/src/com/jsyn/util/RecursiveSequenceGenerator.java b/src/com/jsyn/util/RecursiveSequenceGenerator.java
new file mode 100644
index 0000000..53dbdf9
--- /dev/null
+++ b/src/com/jsyn/util/RecursiveSequenceGenerator.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright 1997 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util;
+
+import java.util.Random;
+
+/**
+ * Generate a sequence of integers based on a recursive mining of previous material. Notes are
+ * generated by one of the following formula:
+ *
+ * <pre>
+ * <code>
+ * value[n] = value[n-delay] + offset;
+ * </code>
+ * </pre>
+ *
+ * The parameters delay and offset are randomly generated. This algorithm was first developed in
+ * 1977 for a class project in FORTRAN. It was ported to Forth for HMSL in the late 80's. It was
+ * then ported to Java for JSyn in 1997.
+ *
+ * @author Phil Burk (C) 1997,2011 Mobileer Inc
+ */
+public class RecursiveSequenceGenerator {
+ private int delay = 1;
+ private int maxValue;
+ private int maxInterval;
+ private double desiredDensity = 0.5;
+
+ private int offset;
+ private int values[];
+ private boolean enables[];
+ private int cursor;
+ private int countdown = -1;
+ private double actualDensity;
+ private int beatsPerMeasure = 8;
+ private Random random;
+
+ public RecursiveSequenceGenerator() {
+ this(25, 7, 64);
+ }
+
+ public RecursiveSequenceGenerator(int maxValue, int maxInterval, int arraySize) {
+ values = new int[arraySize];
+ enables = new boolean[arraySize];
+ this.maxValue = maxValue;
+ this.maxInterval = maxInterval;
+ for (int i = 0; i < values.length; i++) {
+ values[i] = maxValue / 2;
+ enables[i] = isNextEnabled(false);
+ }
+ }
+
+ /** Set density of notes. 0.0 to 1.0 */
+ public void setDensity(double density) {
+ desiredDensity = density;
+ }
+
+ public double getDensity() {
+ return desiredDensity;
+ }
+
+ /** Set maximum for generated value. */
+ public void setMaxValue(int maxValue) {
+ this.maxValue = maxValue;
+ }
+
+ public int getMaxValue() {
+ return maxValue;
+ }
+
+ /** Set maximum for generated value. */
+ public void setMaxInterval(int maxInterval) {
+ this.maxInterval = maxInterval;
+ }
+
+ public int getMaxInterval() {
+ return maxInterval;
+ }
+
+ /* Determine whether next in sequence should occur. */
+ public boolean isNextEnabled(boolean preferance) {
+ /* Calculate note density using low pass IIR filter. */
+ double newDensity = (actualDensity * 0.9) + (preferance ? 0.1 : 0.0);
+ /* Invert enable to push density towards desired level, with hysteresis. */
+ if (preferance && (newDensity > ((desiredDensity * 0.7) + 0.3)))
+ preferance = false;
+ else if (!preferance && (newDensity < (desiredDensity * 0.7)))
+ preferance = true;
+ actualDensity = (actualDensity * 0.9) + (preferance ? 0.1 : 0.0);
+ return preferance;
+ }
+
+ public int randomPowerOf2(int maxExp) {
+ return (1 << (int) (random.nextDouble() * (maxExp + 1)));
+ }
+
+ /** Random number evenly distributed from -maxInterval to +maxInterval */
+ public int randomEvenInterval() {
+ return (int) (random.nextDouble() * ((maxInterval * 2) + 1)) - maxInterval;
+ }
+
+ void calcNewOffset() {
+ offset = randomEvenInterval();
+ }
+
+ public void randomize() {
+
+ delay = randomPowerOf2(4);
+ calcNewOffset();
+ // System.out.println("NewSeq: delay = " + delay + ", offset = " +
+ // offset );
+ }
+
+ /** Change parameters based on random countdown. */
+ public int next() {
+ // If this sequence is finished, start a new one.
+ if (countdown-- < 0) {
+ randomize();
+ countdown = randomPowerOf2(3);
+ }
+ return nextValue();
+ }
+
+ /** Change parameters using a probability based on beatIndex. */
+ public int next(int beatIndex) {
+ int beatMod = beatIndex % beatsPerMeasure;
+ switch (beatMod) {
+ case 0:
+ if (Math.random() < 0.90)
+ randomize();
+ break;
+ case 2:
+ case 6:
+ if (Math.random() < 0.15)
+ randomize();
+ break;
+ case 4:
+ if (Math.random() < 0.30)
+ randomize();
+ break;
+ default:
+ if (Math.random() < 0.07)
+ randomize();
+ break;
+ }
+ return nextValue();
+ }
+
+ /** Generate nextValue based on current delay and offset */
+ public int nextValue() {
+ // Generate index into circular value buffer.
+ int idx = (cursor - delay);
+ if (idx < 0)
+ idx += values.length;
+
+ // Generate new value. Calculate new offset if too high or low.
+ int nextVal = 0;
+ int timeout = 100;
+ while (timeout > 0) {
+ nextVal = values[idx] + offset;
+ if ((nextVal >= 0) && (nextVal < maxValue))
+ break;
+ // Prevent endless loops when maxValue changes.
+ if (nextVal > (maxValue + maxInterval - 1)) {
+ nextVal = maxValue;
+ break;
+ }
+ calcNewOffset();
+ timeout--;
+ // System.out.println("NextVal = " + nextVal + ", offset = " +
+ // offset );
+ }
+ if (timeout <= 0) {
+ System.err.println("RecursiveSequence: nextValue timed out. offset = " + offset);
+ nextVal = maxValue / 2;
+ offset = 0;
+ }
+
+ // Save new value in circular buffer.
+ values[cursor] = nextVal;
+
+ boolean playIt = enables[cursor] = isNextEnabled(enables[idx]);
+ cursor++;
+ if (cursor >= values.length)
+ cursor = 0;
+
+ // System.out.println("nextVal = " + nextVal );
+
+ return playIt ? nextVal : -1;
+ }
+
+ public Random getRandom() {
+ return random;
+ }
+
+ public void setRandom(Random random) {
+ this.random = random;
+ }
+
+}
diff --git a/src/com/jsyn/util/SampleLoader.java b/src/com/jsyn/util/SampleLoader.java
new file mode 100644
index 0000000..611a7d6
--- /dev/null
+++ b/src/com/jsyn/util/SampleLoader.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright 2011 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+
+import com.jsyn.data.FloatSample;
+import com.jsyn.util.soundfile.CustomSampleLoader;
+
+/**
+ * Load a FloatSample from various sources. The default loader uses custom code to load WAV or AIF
+ * files. Supported data formats are 16, 24 and 32 bit PCM, and 32-bit float. Compressed formats
+ * such as unsigned 8-bit, uLaw, A-Law and MP3 are not support. If you need to load one of those
+ * files try setJavaSoundPreferred(true). Or convert it to a supported format using Audacity or Sox
+ * or some other sample file tool. Here is an example of loading a sample from a file.
+ *
+ * <pre>
+ * <code>
+ * File sampleFile = new File("guitar.wav");
+ * FloatSample sample = SampleLoader.loadFloatSample( sampleFile );
+ * </code>
+ * </pre>
+ *
+ * @author Phil Burk (C) 2011 Mobileer Inc
+ */
+public class SampleLoader {
+ private static boolean javaSoundPreferred = false;
+ private static final String JS_LOADER_NAME = "com.jsyn.util.JavaSoundSampleLoader";
+
+ /**
+ * Try to create an implementation of AudioSampleLoader.
+ *
+ * @return A device supported on this platform.
+ */
+ private static AudioSampleLoader createLoader() {
+ AudioSampleLoader loader = null;
+ try {
+ if (javaSoundPreferred) {
+ loader = (AudioSampleLoader) JavaTools.loadClass(JS_LOADER_NAME).newInstance();
+ } else {
+ loader = new CustomSampleLoader();
+ }
+ } catch (InstantiationException e) {
+ e.printStackTrace();
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ }
+ return loader;
+ }
+
+ /**
+ * Load a FloatSample from a File object.
+ */
+ public static FloatSample loadFloatSample(File fileIn) throws IOException {
+ AudioSampleLoader loader = SampleLoader.createLoader();
+ return loader.loadFloatSample(fileIn);
+ }
+
+ /**
+ * Load a FloatSample from an InputStream. This is handy when loading Resources from a JAR file.
+ */
+ public static FloatSample loadFloatSample(InputStream inputStream) throws IOException {
+ AudioSampleLoader loader = SampleLoader.createLoader();
+ return loader.loadFloatSample(inputStream);
+ }
+
+ /**
+ * Load a FloatSample from a URL.. This is handy when loading Resources from a website.
+ */
+ public static FloatSample loadFloatSample(URL url) throws IOException {
+ AudioSampleLoader loader = SampleLoader.createLoader();
+ return loader.loadFloatSample(url);
+ }
+
+ public static boolean isJavaSoundPreferred() {
+ return javaSoundPreferred;
+ }
+
+ /**
+ * If set true then the audio file parser from JavaSound will be used. Note that JavaSound
+ * cannot load audio files containing floating point data. But it may be able to load some
+ * compressed data formats such as uLaw.
+ *
+ * @note JavaSound is not supported on Android.
+ * @param javaSoundPreferred
+ */
+ public static void setJavaSoundPreferred(boolean javaSoundPreferred) {
+ SampleLoader.javaSoundPreferred = javaSoundPreferred;
+ }
+
+ /**
+ * Decode 24 bit samples from a BigEndian byte array into a float array. The samples will be
+ * normalized into the range -1.0 to +1.0.
+ *
+ * @param audioBytes raw data from an audio file
+ * @param offset first element of byte array
+ * @param numBytes number of bytes to process
+ * @param data array to be filled with floats
+ * @param outputOffset first element of float array to be filled
+ */
+ public static void decodeBigI24ToF32(byte[] audioBytes, int offset, int numBytes, float[] data,
+ int outputOffset) {
+ int lastByte = offset + numBytes;
+ int byteCursor = offset;
+ int floatCursor = outputOffset;
+ while (byteCursor < lastByte) {
+ int hi = ((audioBytes[byteCursor++]) & 0x00FF);
+ int mid = ((audioBytes[byteCursor++]) & 0x00FF);
+ int lo = ((audioBytes[byteCursor++]) & 0x00FF);
+ int value = (hi << 24) | (mid << 16) | (lo << 8);
+ data[floatCursor++] = value * (1.0f / Integer.MAX_VALUE);
+ }
+ }
+
+ public static void decodeBigI16ToF32(byte[] audioBytes, int offset, int numBytes, float[] data,
+ int outputOffset) {
+ int lastByte = offset + numBytes;
+ int byteCursor = offset;
+ int floatCursor = outputOffset;
+ while (byteCursor < lastByte) {
+ int hi = ((audioBytes[byteCursor++]) & 0x00FF);
+ int lo = ((audioBytes[byteCursor++]) & 0x00FF);
+ short value = (short) ((hi << 8) | lo);
+ data[floatCursor++] = value * (1.0f / 32768);
+ }
+ }
+
+ public static void decodeBigF32ToF32(byte[] audioBytes, int offset, int numBytes, float[] data,
+ int outputOffset) {
+ int lastByte = offset + numBytes;
+ int byteCursor = offset;
+ int floatCursor = outputOffset;
+ while (byteCursor < lastByte) {
+ int bits = audioBytes[byteCursor++];
+ bits = (bits << 8) | ((audioBytes[byteCursor++]) & 0x00FF);
+ bits = (bits << 8) | ((audioBytes[byteCursor++]) & 0x00FF);
+ bits = (bits << 8) | ((audioBytes[byteCursor++]) & 0x00FF);
+ data[floatCursor++] = Float.intBitsToFloat(bits);
+ }
+ }
+
+ public static void decodeBigI32ToF32(byte[] audioBytes, int offset, int numBytes, float[] data,
+ int outputOffset) {
+ int lastByte = offset + numBytes;
+ int byteCursor = offset;
+ int floatCursor = outputOffset;
+ while (byteCursor < lastByte) {
+ int value = audioBytes[byteCursor++]; // MSB
+ value = (value << 8) | ((audioBytes[byteCursor++]) & 0x00FF);
+ value = (value << 8) | ((audioBytes[byteCursor++]) & 0x00FF);
+ value = (value << 8) | ((audioBytes[byteCursor++]) & 0x00FF);
+ data[floatCursor++] = value * (1.0f / Integer.MAX_VALUE);
+ }
+ }
+
+ public static void decodeLittleF32ToF32(byte[] audioBytes, int offset, int numBytes,
+ float[] data, int outputOffset) {
+ int lastByte = offset + numBytes;
+ int byteCursor = offset;
+ int floatCursor = outputOffset;
+ while (byteCursor < lastByte) {
+ int bits = ((audioBytes[byteCursor++]) & 0x00FF); // LSB
+ bits += ((audioBytes[byteCursor++]) & 0x00FF) << 8;
+ bits += ((audioBytes[byteCursor++]) & 0x00FF) << 16;
+ bits += (audioBytes[byteCursor++]) << 24;
+ data[floatCursor++] = Float.intBitsToFloat(bits);
+ }
+ }
+
+ public static void decodeLittleI32ToF32(byte[] audioBytes, int offset, int numBytes,
+ float[] data, int outputOffset) {
+ int lastByte = offset + numBytes;
+ int byteCursor = offset;
+ int floatCursor = outputOffset;
+ while (byteCursor < lastByte) {
+ int value = ((audioBytes[byteCursor++]) & 0x00FF);
+ value += ((audioBytes[byteCursor++]) & 0x00FF) << 8;
+ value += ((audioBytes[byteCursor++]) & 0x00FF) << 16;
+ value += (audioBytes[byteCursor++]) << 24;
+ data[floatCursor++] = value * (1.0f / Integer.MAX_VALUE);
+ }
+ }
+
+ public static void decodeLittleI24ToF32(byte[] audioBytes, int offset, int numBytes,
+ float[] data, int outputOffset) {
+ int lastByte = offset + numBytes;
+ int byteCursor = offset;
+ int floatCursor = outputOffset;
+ while (byteCursor < lastByte) {
+ int lo = ((audioBytes[byteCursor++]) & 0x00FF);
+ int mid = ((audioBytes[byteCursor++]) & 0x00FF);
+ int hi = ((audioBytes[byteCursor++]) & 0x00FF);
+ int value = (hi << 24) | (mid << 16) | (lo << 8);
+ data[floatCursor++] = value * (1.0f / Integer.MAX_VALUE);
+ }
+ }
+
+ public static void decodeLittleI16ToF32(byte[] audioBytes, int offset, int numBytes,
+ float[] data, int outputOffset) {
+ int lastByte = offset + numBytes;
+ int byteCursor = offset;
+ int floatCursor = outputOffset;
+ while (byteCursor < lastByte) {
+ int lo = ((audioBytes[byteCursor++]) & 0x00FF);
+ int hi = ((audioBytes[byteCursor++]) & 0x00FF);
+ short value = (short) ((hi << 8) | lo);
+ float sample = value * (1.0f / 32768);
+ data[floatCursor++] = sample;
+ }
+ }
+
+}
diff --git a/src/com/jsyn/util/SignalCorrelator.java b/src/com/jsyn/util/SignalCorrelator.java
new file mode 100644
index 0000000..ebdd46b
--- /dev/null
+++ b/src/com/jsyn/util/SignalCorrelator.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2011 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util;
+
+/**
+ * Interface used to evaluate various algorithms for pitch detection.
+ *
+ * @author Phil Burk (C) 2011 Mobileer Inc
+ */
+public interface SignalCorrelator {
+ /**
+ * Add a sample to be analyzed. The samples will generally be held in a circular buffer.
+ *
+ * @param value
+ * @return true if a new period value has been generated
+ */
+ public boolean addSample(double value);
+
+ /**
+ * @return the estimated period of the waveform in samples
+ */
+ public double getPeriod();
+
+ /**
+ * Measure of how confident the analyzer is of the last result.
+ *
+ * @return quality of the estimate between 0.0 and 1.0
+ */
+ public double getConfidence();
+
+ /** For internal debugging. */
+ public float[] getDiffs();
+
+}
diff --git a/src/com/jsyn/util/StreamingThread.java b/src/com/jsyn/util/StreamingThread.java
new file mode 100644
index 0000000..682f476
--- /dev/null
+++ b/src/com/jsyn/util/StreamingThread.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2011 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util;
+
+import java.io.IOException;
+
+import com.jsyn.io.AudioInputStream;
+import com.jsyn.io.AudioOutputStream;
+
+/**
+ * Read from an AudioInputStream and write to an AudioOutputStream as a background thread.
+ *
+ * @author Phil Burk (C) 2011 Mobileer Inc
+ */
+public class StreamingThread extends Thread {
+ private AudioInputStream inputStream;
+ private AudioOutputStream outputStream;
+ private int framesPerBuffer = 1024;
+ private volatile boolean go = true;
+ private TransportModel transportModel;
+ private long framePosition;
+ private long maxFrames;
+ private int samplesPerFrame = 1;
+
+ public StreamingThread(AudioInputStream inputStream, AudioOutputStream outputStream) {
+ this.inputStream = inputStream;
+ this.outputStream = outputStream;
+ }
+
+ @Override
+ public void run() {
+ double[] buffer = new double[framesPerBuffer * samplesPerFrame];
+ try {
+ transportModel.firePositionChanged(framePosition);
+ transportModel.fireStateChanged(TransportModel.STATE_RUNNING);
+ int framesToRead = geteFramesToRead(buffer);
+ while (go && (framesToRead > 0)) {
+ int samplesToRead = framesToRead * samplesPerFrame;
+ while (samplesToRead > 0) {
+ int samplesRead = inputStream.read(buffer, 0, samplesToRead);
+ outputStream.write(buffer, 0, samplesRead);
+ samplesToRead -= samplesRead;
+ }
+ framePosition += framesToRead;
+ transportModel.firePositionChanged(framePosition);
+ framesToRead = geteFramesToRead(buffer);
+ }
+ transportModel.fireStateChanged(TransportModel.STATE_STOPPED);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private int geteFramesToRead(double[] buffer) {
+ if (maxFrames > 0) {
+ long numToRead = maxFrames - framePosition;
+ if (numToRead < 0) {
+ return 0;
+ } else if (numToRead > framesPerBuffer) {
+ numToRead = framesPerBuffer;
+ }
+ return (int) numToRead;
+ } else {
+ return framesPerBuffer;
+ }
+ }
+
+ public int getFramesPerBuffer() {
+ return framesPerBuffer;
+ }
+
+ /**
+ * Only call this before the thread has started.
+ *
+ * @param framesPerBuffer
+ */
+ public void setFramesPerBuffer(int framesPerBuffer) {
+ this.framesPerBuffer = framesPerBuffer;
+ }
+
+ public void requestStop() {
+ go = false;
+ }
+
+ public TransportModel getTransportModel() {
+ return transportModel;
+ }
+
+ public void setTransportModel(TransportModel transportModel) {
+ this.transportModel = transportModel;
+ }
+
+ /**
+ * @param maxFrames
+ */
+ public void setMaxFrames(long maxFrames) {
+ this.maxFrames = maxFrames;
+ }
+
+ public int getSamplesPerFrame() {
+ return samplesPerFrame;
+ }
+
+ public void setSamplesPerFrame(int samplesPerFrame) {
+ this.samplesPerFrame = samplesPerFrame;
+ }
+}
diff --git a/src/com/jsyn/util/TransportListener.java b/src/com/jsyn/util/TransportListener.java
new file mode 100644
index 0000000..3c8b048
--- /dev/null
+++ b/src/com/jsyn/util/TransportListener.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util;
+
+public interface TransportListener {
+ /**
+ * @param transportModel
+ * @param framePosition position in frames
+ */
+ void positionChanged(TransportModel transportModel, long framePosition);
+
+ /**
+ * @param transportModel
+ * @param state for example TransportModel.STATE_STOPPED
+ */
+ void stateChanged(TransportModel transportModel, int state);
+}
diff --git a/src/com/jsyn/util/TransportModel.java b/src/com/jsyn/util/TransportModel.java
new file mode 100644
index 0000000..bcc75be
--- /dev/null
+++ b/src/com/jsyn/util/TransportModel.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util;
+
+import java.util.concurrent.CopyOnWriteArrayList;
+
+public class TransportModel {
+ public static final int STATE_STOPPED = 0;
+ public static final int STATE_PAUSED = 1;
+ public static final int STATE_RUNNING = 2;
+
+ private CopyOnWriteArrayList<TransportListener> listeners = new CopyOnWriteArrayList<TransportListener>();
+ private int state = STATE_STOPPED;
+ private long position;
+
+ public void addTransportListener(TransportListener listener) {
+ listeners.add(listener);
+ }
+
+ public void removeTransportListener(TransportListener listener) {
+ listeners.remove(listener);
+ }
+
+ public void setState(int newState) {
+ state = newState;
+ fireStateChanged(newState);
+ }
+
+ public int getState() {
+ return state;
+ }
+
+ public void setPosition(long newPosition) {
+ position = newPosition;
+ firePositionChanged(newPosition);
+ }
+
+ public long getPosition() {
+ return position;
+ }
+
+ public void fireStateChanged(int newState) {
+ for (TransportListener listener : listeners) {
+ listener.stateChanged(this, newState);
+ }
+ }
+
+ public void firePositionChanged(long newPosition) {
+ for (TransportListener listener : listeners) {
+ listener.positionChanged(this, newPosition);
+ }
+ }
+}
diff --git a/src/com/jsyn/util/VoiceAllocator.java b/src/com/jsyn/util/VoiceAllocator.java
new file mode 100644
index 0000000..af37b91
--- /dev/null
+++ b/src/com/jsyn/util/VoiceAllocator.java
@@ -0,0 +1,238 @@
+/*
+ * Copyright 2011 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util;
+
+import com.jsyn.Synthesizer;
+import com.jsyn.unitgen.UnitVoice;
+import com.softsynth.shared.time.ScheduledCommand;
+import com.softsynth.shared.time.TimeStamp;
+
+/**
+ * Allocate voices based on an integer tag. The tag could, for example, be a MIDI note number. Or a
+ * tag could be an int that always increments. Use the same tag to refer to a voice for noteOn() and
+ * noteOff(). If no new voices are available then a voice in use will be stolen.
+ *
+ * @author Phil Burk (C) 2011 Mobileer Inc
+ */
+public class VoiceAllocator implements Instrument {
+ private int maxVoices;
+ private VoiceTracker[] trackers;
+ private long tick;
+ private Synthesizer synthesizer;
+ private int presetIndex = -1;
+
+ /**
+ * Create an allocator for the array of UnitVoices. The array must be full of instantiated
+ * UnitVoices that are connected to some kind of mixer.
+ *
+ * @param voices
+ */
+ public VoiceAllocator(UnitVoice[] voices) {
+ maxVoices = voices.length;
+ trackers = new VoiceTracker[maxVoices];
+ for (int i = 0; i < maxVoices; i++) {
+ trackers[i] = new VoiceTracker();
+ trackers[i].voice = voices[i];
+ }
+ }
+
+ public Synthesizer getSynthesizer() {
+ if (synthesizer == null) {
+ synthesizer = trackers[0].voice.getUnitGenerator().getSynthesizer();
+ }
+ return synthesizer;
+ }
+
+ private class VoiceTracker {
+ UnitVoice voice;
+ int tag = -1;
+ int presetIndex = -1;
+ long when;
+ boolean on;
+
+ public void off() {
+ on = false;
+ when = tick++;
+ }
+ }
+
+ /**
+ * @return number of UnitVoices passed to the allocator.
+ */
+ public int getVoiceCount() {
+ return maxVoices;
+ }
+
+ private VoiceTracker findVoice(int tag) {
+ for (VoiceTracker tracker : trackers) {
+ if (tracker.tag == tag) {
+ return tracker;
+ }
+ }
+ return null;
+ }
+
+ private VoiceTracker stealVoice() {
+ VoiceTracker bestOff = null;
+ VoiceTracker bestOn = null;
+ for (VoiceTracker tracker : trackers) {
+ if (tracker.voice == null) {
+ return tracker;
+ }
+ // If we have a bestOff voice then don't even bother with on voices.
+ else if (bestOff != null) {
+ // Older off voice?
+ if (!tracker.on && (tracker.when < bestOff.when)) {
+ bestOff = tracker;
+ }
+ } else if (tracker.on) {
+ if (bestOn == null) {
+ bestOn = tracker;
+ } else if (tracker.when < bestOn.when) {
+ bestOn = tracker;
+ }
+ } else {
+ bestOff = tracker;
+ }
+ }
+ if (bestOff != null) {
+ return bestOff;
+ } else {
+ return bestOn;
+ }
+ }
+
+ /**
+ * Allocate a Voice associated with this tag. It will first pick a voice already assigned to
+ * that tag. Next it will pick the oldest voice that is off. Next it will pick the oldest voice
+ * that is on. If you are using timestamps to play the voice in the future then you should use
+ * the noteOn() noteOff() and setPort() methods.
+ *
+ * @param tag
+ * @return Voice that is most available.
+ */
+ protected synchronized UnitVoice allocate(int tag) {
+ VoiceTracker tracker = allocateTracker(tag);
+ return tracker.voice;
+ }
+
+ private VoiceTracker allocateTracker(int tag) {
+ VoiceTracker tracker = findVoice(tag);
+ if (tracker == null) {
+ tracker = stealVoice();
+ }
+ tracker.tag = tag;
+ tracker.when = tick++;
+ tracker.on = true;
+ return tracker;
+ }
+
+ protected synchronized boolean isOn(int tag) {
+ VoiceTracker tracker = findVoice(tag);
+ if (tracker != null) {
+ return tracker.on;
+ }
+ return false;
+ }
+
+ protected synchronized UnitVoice off(int tag) {
+ VoiceTracker tracker = findVoice(tag);
+ if (tracker != null) {
+ tracker.off();
+ return tracker.voice;
+ }
+ return null;
+ }
+
+ /** Turn off all the note currently on. */
+ @Override
+ public void allNotesOff(TimeStamp timeStamp) {
+ getSynthesizer().scheduleCommand(timeStamp, new ScheduledCommand() {
+ @Override
+ public void run() {
+ for (VoiceTracker tracker : trackers) {
+ if (tracker.on) {
+ tracker.voice.noteOff(getSynthesizer().createTimeStamp());
+ tracker.off();
+ }
+ }
+ }
+ });
+ }
+
+ /**
+ * Play a note on the voice and associate it with the given tag. if needed a new voice will be
+ * allocated and an old voice may be turned off.
+ */
+ @Override
+ public void noteOn(final int tag, final double frequency, final double amplitude,
+ TimeStamp timeStamp) {
+ getSynthesizer().scheduleCommand(timeStamp, new ScheduledCommand() {
+ @Override
+ public void run() {
+ VoiceTracker voiceTracker = allocateTracker(tag);
+ if (presetIndex != voiceTracker.presetIndex) {
+ voiceTracker.voice.usePreset(presetIndex);
+ }
+ voiceTracker.voice.noteOn(frequency, amplitude, getSynthesizer().createTimeStamp());
+ }
+ });
+ }
+
+ /** Turn off the voice associated with the given tag if allocated. */
+ @Override
+ public void noteOff(final int tag, TimeStamp timeStamp) {
+ getSynthesizer().scheduleCommand(timeStamp, new ScheduledCommand() {
+ @Override
+ public void run() {
+ VoiceTracker voiceTracker = findVoice(tag);
+ if (voiceTracker != null) {
+ voiceTracker.voice.noteOff(getSynthesizer().createTimeStamp());
+ off(tag);
+ }
+ }
+ });
+ }
+
+ /** Set a port on the voice associated with the given tag if allocated. */
+ @Override
+ public void setPort(final int tag, final String portName, final double value,
+ TimeStamp timeStamp) {
+ getSynthesizer().scheduleCommand(timeStamp, new ScheduledCommand() {
+ @Override
+ public void run() {
+ VoiceTracker voiceTracker = findVoice(tag);
+ if (voiceTracker != null) {
+ voiceTracker.voice.setPort(portName, value, getSynthesizer().createTimeStamp());
+ }
+ }
+ });
+ }
+
+ @Override
+ public void usePreset(final int presetIndex, TimeStamp timeStamp) {
+ getSynthesizer().scheduleCommand(timeStamp, new ScheduledCommand() {
+ @Override
+ public void run() {
+ for (VoiceTracker tracker : trackers) {
+ tracker.voice.usePreset(presetIndex);
+ }
+ }
+ });
+ }
+
+}
diff --git a/src/com/jsyn/util/VoiceDescription.java b/src/com/jsyn/util/VoiceDescription.java
new file mode 100644
index 0000000..b7be044
--- /dev/null
+++ b/src/com/jsyn/util/VoiceDescription.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2011 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util;
+
+import com.jsyn.unitgen.UnitVoice;
+
+/**
+ * Describe a voice so that a user can pick it out of an InstrumentLibrary.
+ *
+ * @author Phil Burk (C) 2011 Mobileer Inc
+ * @see PolyphonicInstrument
+ */
+public abstract class VoiceDescription {
+ private String name;
+ private String[] presetNames;
+
+ public VoiceDescription(String name, String[] presetNames) {
+ this.name = name;
+ this.presetNames = presetNames;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public int getPresetCount() {
+ return presetNames.length;
+ }
+
+ public String[] getPresetNames() {
+ return presetNames;
+ }
+
+ public abstract String[] getTags(int presetIndex);
+
+ /**
+ * Instantiate one of these voices. You may want to call usePreset(n) on the voice after
+ * instantiating it.
+ *
+ * @return a voice
+ */
+ public abstract UnitVoice createUnitVoice();
+
+ public abstract String getVoiceClassName();
+
+ @Override
+ public String toString() {
+ return name + "[" + getPresetCount() + "]";
+ }
+}
diff --git a/src/com/jsyn/util/WaveFileWriter.java b/src/com/jsyn/util/WaveFileWriter.java
new file mode 100644
index 0000000..32e9995
--- /dev/null
+++ b/src/com/jsyn/util/WaveFileWriter.java
@@ -0,0 +1,293 @@
+/*
+ * Copyright 2011 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.RandomAccessFile;
+
+import com.jsyn.io.AudioOutputStream;
+
+/**
+ * Write audio data to a WAV file.
+ *
+ * <pre>
+ * <code>
+ * WaveFileWriter writer = new WaveFileWriter(file);
+ * writer.setFrameRate(22050);
+ * writer.setBitsPerSample(24);
+ * writer.write(floatArray);
+ * writer.close();
+ * </code>
+ * </pre>
+ *
+ * @author Phil Burk (C) 2011 Mobileer Inc
+ */
+public class WaveFileWriter implements AudioOutputStream {
+ private static final short WAVE_FORMAT_PCM = 1;
+ private OutputStream outputStream;
+ private long riffSizePosition = 0;
+ private long dataSizePosition = 0;
+ private int frameRate = 44100;
+ private int samplesPerFrame = 1;
+ private int bitsPerSample = 16;
+ private int bytesWritten;
+ private File outputFile;
+ private boolean headerWritten = false;
+ private final static int PCM24_MIN = -(1 << 23);
+ private final static int PCM24_MAX = (1 << 23) - 1;
+
+ /**
+ * Create a writer that will write to the specified file.
+ *
+ * @param outputFile
+ * @throws FileNotFoundException
+ */
+ public WaveFileWriter(File outputFile) throws FileNotFoundException {
+ this.outputFile = outputFile;
+ FileOutputStream fileOut = new FileOutputStream(outputFile);
+ outputStream = new BufferedOutputStream(fileOut);
+ }
+
+ /**
+ * @param frameRate default is 44100
+ */
+ public void setFrameRate(int frameRate) {
+ this.frameRate = frameRate;
+ }
+
+ public int getFrameRate() {
+ return frameRate;
+ }
+
+ /** For stereo, set this to 2. Default is 1. */
+ public void setSamplesPerFrame(int samplesPerFrame) {
+ this.samplesPerFrame = samplesPerFrame;
+ }
+
+ public int getSamplesPerFrame() {
+ return samplesPerFrame;
+ }
+
+ /** Only 16 or 24 bit samples supported at the moment. Default is 16. */
+ public void setBitsPerSample(int bits) {
+ if ((bits != 16) && (bits != 24)) {
+ throw new IllegalArgumentException("Only 16 or 24 bits per sample allowed. Not " + bits);
+ }
+ bitsPerSample = bits;
+ }
+
+ public int getBitsPerSample() {
+ return bitsPerSample;
+ }
+
+ @Override
+ public void close() throws IOException {
+ outputStream.close();
+ fixSizes();
+ }
+
+ /** Write entire buffer of audio samples to the WAV file. */
+ @Override
+ public void write(double[] buffer) throws IOException {
+ write(buffer, 0, buffer.length);
+ }
+
+ /** Write audio to the WAV file. */
+ public void write(float[] buffer) throws IOException {
+ write(buffer, 0, buffer.length);
+ }
+
+ /** Write single audio data value to the WAV file. */
+ @Override
+ public void write(double value) throws IOException {
+ if (!headerWritten) {
+ writeHeader();
+ }
+
+ if (bitsPerSample == 24) {
+ writePCM24(value);
+ } else {
+ writePCM16(value);
+ }
+ }
+
+ private void writePCM24(double value) throws IOException {
+ // Offset before casting so that we can avoid using floor().
+ // Also round by adding 0.5 so that very small signals go to zero.
+ double temp = (PCM24_MAX * value) + 0.5 - PCM24_MIN;
+ int sample = ((int) temp) + PCM24_MIN;
+ // clip to 24-bit range
+ if (sample > PCM24_MAX) {
+ sample = PCM24_MAX;
+ } else if (sample < PCM24_MIN) {
+ sample = PCM24_MIN;
+ }
+ // encode as little-endian
+ writeByte(sample); // little end
+ writeByte(sample >> 8); // middle
+ writeByte(sample >> 16); // big end
+ }
+
+ private void writePCM16(double value) throws IOException {
+ // Offset before casting so that we can avoid using floor().
+ // Also round by adding 0.5 so that very small signals go to zero.
+ double temp = (Short.MAX_VALUE * value) + 0.5 - Short.MIN_VALUE;
+ int sample = ((int) temp) + Short.MIN_VALUE;
+ if (sample > Short.MAX_VALUE) {
+ sample = Short.MAX_VALUE;
+ } else if (sample < Short.MIN_VALUE) {
+ sample = Short.MIN_VALUE;
+ }
+ writeByte(sample); // little end
+ writeByte(sample >> 8); // big end
+ }
+
+ /** Write audio to the WAV file. */
+ @Override
+ public void write(double[] buffer, int start, int count) throws IOException {
+ for (int i = 0; i < count; i++) {
+ write(buffer[start + i]);
+ }
+ }
+
+ /** Write audio to the WAV file. */
+ public void write(float[] buffer, int start, int count) throws IOException {
+ for (int i = 0; i < count; i++) {
+ write(buffer[start + i]);
+ }
+ }
+
+ // Write lower 8 bits. Upper bits ignored.
+ private void writeByte(int b) throws IOException {
+ outputStream.write(b);
+ bytesWritten += 1;
+ }
+
+ /**
+ * Write a 32 bit integer to the stream in Little Endian format.
+ */
+ public void writeIntLittle(int n) throws IOException {
+ writeByte(n);
+ writeByte(n >> 8);
+ writeByte(n >> 16);
+ writeByte(n >> 24);
+ }
+
+ /**
+ * Write a 16 bit integer to the stream in Little Endian format.
+ */
+ public void writeShortLittle(short n) throws IOException {
+ writeByte(n);
+ writeByte(n >> 8);
+ }
+
+ /**
+ * Write a simple WAV header for PCM data.
+ */
+ private void writeHeader() throws IOException {
+ writeRiffHeader();
+ writeFormatChunk();
+ writeDataChunkHeader();
+ outputStream.flush();
+ headerWritten = true;
+ }
+
+ /**
+ * Write a 'RIFF' file header and a 'WAVE' ID to the WAV file.
+ */
+ private void writeRiffHeader() throws IOException {
+ writeByte('R');
+ writeByte('I');
+ writeByte('F');
+ writeByte('F');
+ riffSizePosition = bytesWritten;
+ writeIntLittle(Integer.MAX_VALUE);
+ writeByte('W');
+ writeByte('A');
+ writeByte('V');
+ writeByte('E');
+ }
+
+ /**
+ * Write an 'fmt ' chunk to the WAV file containing the given information.
+ */
+ public void writeFormatChunk() throws IOException {
+ int bytesPerSample = (bitsPerSample + 7) / 8;
+
+ writeByte('f');
+ writeByte('m');
+ writeByte('t');
+ writeByte(' ');
+ writeIntLittle(16); // chunk size
+ writeShortLittle(WAVE_FORMAT_PCM);
+ writeShortLittle((short) samplesPerFrame);
+ writeIntLittle(frameRate);
+ // bytes/second
+ writeIntLittle(frameRate * samplesPerFrame * bytesPerSample);
+ // block align
+ writeShortLittle((short) (samplesPerFrame * bytesPerSample));
+ writeShortLittle((short) bitsPerSample);
+ }
+
+ /**
+ * Write a 'data' chunk header to the WAV file. This should be followed by call to
+ * writeShortLittle() to write the data to the chunk.
+ */
+ public void writeDataChunkHeader() throws IOException {
+ writeByte('d');
+ writeByte('a');
+ writeByte('t');
+ writeByte('a');
+ dataSizePosition = bytesWritten;
+ writeIntLittle(Integer.MAX_VALUE); // size
+ }
+
+ /**
+ * Fix RIFF and data chunk sizes based on final size. Assume data chunk is the last chunk.
+ */
+ private void fixSizes() throws IOException {
+ RandomAccessFile randomFile = new RandomAccessFile(outputFile, "rw");
+ try {
+ // adjust RIFF size
+ long end = bytesWritten;
+ int riffSize = (int) (end - riffSizePosition) - 4;
+ randomFile.seek(riffSizePosition);
+ writeRandomIntLittle(randomFile, riffSize);
+ // adjust data size
+ int dataSize = (int) (end - dataSizePosition) - 4;
+ randomFile.seek(dataSizePosition);
+ writeRandomIntLittle(randomFile, dataSize);
+ } finally {
+ randomFile.close();
+ }
+ }
+
+ private void writeRandomIntLittle(RandomAccessFile randomFile, int n) throws IOException {
+ byte[] buffer = new byte[4];
+ buffer[0] = (byte) n;
+ buffer[1] = (byte) (n >> 8);
+ buffer[2] = (byte) (n >> 16);
+ buffer[3] = (byte) (n >> 24);
+ randomFile.write(buffer);
+ }
+
+}
diff --git a/src/com/jsyn/util/WaveRecorder.java b/src/com/jsyn/util/WaveRecorder.java
new file mode 100644
index 0000000..059863b
--- /dev/null
+++ b/src/com/jsyn/util/WaveRecorder.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2011 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+import com.jsyn.Synthesizer;
+import com.jsyn.ports.UnitInputPort;
+
+/**
+ * Connect a unit generator to the input. Then start() recording. The signal will be written to a
+ * WAV format file that can be read by other programs.
+ *
+ * @author Phil Burk (C) 2011 Mobileer Inc
+ */
+public class WaveRecorder {
+ private AudioStreamReader reader;
+ private WaveFileWriter writer;
+ private StreamingThread thread;
+ private Synthesizer synth;
+ private TransportModel transportModel = new TransportModel();
+ private double maxRecordingTime;
+
+ /**
+ * Create a stereo 16-bit recorder.
+ *
+ * @param synth
+ * @param outputFile
+ * @throws FileNotFoundException
+ */
+ public WaveRecorder(Synthesizer synth, File outputFile) throws FileNotFoundException {
+ this(synth, outputFile, 2, 16);
+ }
+
+ public WaveRecorder(Synthesizer synth, File outputFile, int samplesPerFrame)
+ throws FileNotFoundException {
+ this(synth, outputFile, samplesPerFrame, 16);
+ }
+
+ /**
+ * @param synth
+ * @param outputFile
+ * @param samplesPerFrame 1 for mono, 2 for stereo
+ * @param bitsPerSample 16 or 24
+ * @throws FileNotFoundException
+ */
+ public WaveRecorder(Synthesizer synth, File outputFile, int samplesPerFrame, int bitsPerSample)
+ throws FileNotFoundException {
+ this.synth = synth;
+ reader = new AudioStreamReader(synth, samplesPerFrame);
+
+ writer = new WaveFileWriter(outputFile);
+ writer.setFrameRate(synth.getFrameRate());
+ writer.setSamplesPerFrame(samplesPerFrame);
+ writer.setBitsPerSample(bitsPerSample);
+ }
+
+ public UnitInputPort getInput() {
+ return reader.getInput();
+ }
+
+ public void start() {
+ stop();
+ thread = new StreamingThread(reader, writer);
+ thread.setTransportModel(transportModel);
+ thread.setSamplesPerFrame(writer.getSamplesPerFrame());
+ updateMaxRecordingTime();
+ thread.start();
+ }
+
+ public void stop() {
+ if (thread != null) {
+ thread.requestStop();
+ try {
+ thread.join(500);
+ } catch (InterruptedException e) {
+ }
+ thread = null;
+ }
+ }
+
+ /** Close and disconnect any connected inputs. */
+ public void close() throws IOException {
+ stop();
+ if (writer != null) {
+ writer.close();
+ writer = null;
+ }
+ if (reader != null) {
+ reader.close();
+ for (int i = 0; i < reader.getInput().getNumParts(); i++) {
+ reader.getInput().disconnectAll(i);
+ }
+ reader = null;
+ }
+ }
+
+ public void addTransportListener(TransportListener listener) {
+ transportModel.addTransportListener(listener);
+ }
+
+ public void removeTransportListener(TransportListener listener) {
+ transportModel.removeTransportListener(listener);
+ }
+
+ public void setMaxRecordingTime(double maxRecordingTime) {
+ this.maxRecordingTime = maxRecordingTime;
+ updateMaxRecordingTime();
+ }
+
+ private void updateMaxRecordingTime() {
+ StreamingThread streamingThread = thread;
+ if (streamingThread != null) {
+ long maxFrames = (long) (maxRecordingTime * synth.getFrameRate());
+ streamingThread.setMaxFrames(maxFrames);
+ }
+ }
+}
diff --git a/src/com/jsyn/util/soundfile/AIFFFileParser.java b/src/com/jsyn/util/soundfile/AIFFFileParser.java
new file mode 100644
index 0000000..d2df08f
--- /dev/null
+++ b/src/com/jsyn/util/soundfile/AIFFFileParser.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util.soundfile;
+
+import java.io.EOFException;
+import java.io.IOException;
+
+import com.jsyn.data.FloatSample;
+import com.jsyn.data.SampleMarker;
+import com.jsyn.util.SampleLoader;
+
+public class AIFFFileParser extends AudioFileParser {
+ private static final String SUPPORTED_FORMATS = "Only 16 and 24 bit PCM or 32-bit float AIF files supported.";
+ static final int AIFF_ID = ('A' << 24) | ('I' << 16) | ('F' << 8) | 'F';
+ static final int AIFC_ID = ('A' << 24) | ('I' << 16) | ('F' << 8) | 'C';
+ static final int COMM_ID = ('C' << 24) | ('O' << 16) | ('M' << 8) | 'M';
+ static final int SSND_ID = ('S' << 24) | ('S' << 16) | ('N' << 8) | 'D';
+ static final int MARK_ID = ('M' << 24) | ('A' << 16) | ('R' << 8) | 'K';
+ static final int INST_ID = ('I' << 24) | ('N' << 16) | ('S' << 8) | 'T';
+ static final int NONE_ID = ('N' << 24) | ('O' << 16) | ('N' << 8) | 'E';
+ static final int FL32_ID = ('F' << 24) | ('L' << 16) | ('3' << 8) | '2';
+ static final int FL32_ID_LC = ('f' << 24) | ('l' << 16) | ('3' << 8) | '2';
+
+ int sustainBeginID = -1;
+ int sustainEndID = -1;
+ int releaseBeginID = -1;
+ int releaseEndID = -1;
+ boolean typeFloat = false;
+
+ @Override
+ FloatSample finish() throws IOException {
+ setLoops();
+
+ if ((byteData == null)) {
+ throw new IOException("No data found in audio sample.");
+ }
+ float[] floatData = new float[numFrames * samplesPerFrame];
+ if (bitsPerSample == 16) {
+ SampleLoader.decodeBigI16ToF32(byteData, 0, byteData.length, floatData, 0);
+ } else if (bitsPerSample == 24) {
+ SampleLoader.decodeBigI24ToF32(byteData, 0, byteData.length, floatData, 0);
+ } else if (bitsPerSample == 32) {
+ if (typeFloat) {
+ SampleLoader.decodeBigF32ToF32(byteData, 0, byteData.length, floatData, 0);
+ } else {
+ SampleLoader.decodeBigI32ToF32(byteData, 0, byteData.length, floatData, 0);
+ }
+ } else {
+ throw new IOException(SUPPORTED_FORMATS + " size = " + bitsPerSample);
+ }
+
+ return makeSample(floatData);
+ }
+
+ double read80BitFloat() throws IOException {
+ /*
+ * This is not a full decoding of the 80 bit number but it should suffice for the range we
+ * expect.
+ */
+ byte[] bytes = new byte[10];
+ parser.read(bytes);
+ int exp = ((bytes[0] & 0x3F) << 8) | (bytes[1] & 0xFF);
+ int mant = ((bytes[2] & 0xFF) << 16) | ((bytes[3] & 0xFF) << 8) | (bytes[4] & 0xFF);
+ // System.out.println( "exp = " + exp + ", mant = " + mant );
+ return mant / (double) (1 << (22 - exp));
+ }
+
+ void parseCOMMChunk(IFFParser parser, int ckSize) throws IOException {
+ samplesPerFrame = parser.readShortBig();
+ numFrames = parser.readIntBig();
+ bitsPerSample = parser.readShortBig();
+ frameRate = read80BitFloat();
+ if (ckSize > 18) {
+ int format = parser.readIntBig();
+ // Validate data format.
+ if ((format == FL32_ID) || (format == FL32_ID_LC)) {
+ typeFloat = true;
+ } else if (format == NONE_ID) {
+ typeFloat = false;
+ } else {
+ throw new IOException(SUPPORTED_FORMATS + " format " + IFFParser.IDToString(format));
+ }
+ }
+
+ bytesPerSample = (bitsPerSample + 7) / 8;
+ bytesPerFrame = bytesPerSample * samplesPerFrame;
+ }
+
+ /* parse tuning and multi-sample info */
+ void parseINSTChunk(IFFParser parser, int ckSize) throws IOException {
+ int baseNote = parser.readByte();
+ int detune = parser.readByte();
+ originalPitch = baseNote + (0.01 * detune);
+
+ int lowNote = parser.readByte();
+ int highNote = parser.readByte();
+
+ parser.skip(2); /* lo,hi velocity */
+ int gain = parser.readShortBig();
+
+ int playMode = parser.readShortBig(); /* sustain */
+ sustainBeginID = parser.readShortBig();
+ sustainEndID = parser.readShortBig();
+
+ playMode = parser.readShortBig(); /* release */
+ releaseBeginID = parser.readShortBig();
+ releaseEndID = parser.readShortBig();
+ }
+
+ private void setLoops() {
+ SampleMarker cuePoint = cueMap.get(sustainBeginID);
+ if (cuePoint != null) {
+ sustainBegin = cuePoint.position;
+ }
+ cuePoint = cueMap.get(sustainEndID);
+ if (cuePoint != null) {
+ sustainEnd = cuePoint.position;
+ }
+ }
+
+ void parseSSNDChunk(IFFParser parser, int ckSize) throws IOException {
+ long numRead;
+ // System.out.println("parseSSNDChunk()");
+ int offset = parser.readIntBig();
+ parser.readIntBig(); /* blocksize */
+ parser.skip(offset);
+ dataPosition = parser.getOffset();
+ int numBytes = ckSize - 8 - offset;
+ if (ifLoadData) {
+ byteData = new byte[numBytes];
+ numRead = parser.read(byteData);
+ } else {
+ numRead = parser.skip(numBytes);
+ }
+ if (numRead != numBytes)
+ throw new EOFException("AIFF data chunk too short!");
+ }
+
+ void parseMARKChunk(IFFParser parser, int ckSize) throws IOException {
+ long startOffset = parser.getOffset();
+ int numCuePoints = parser.readShortBig();
+ // System.out.println( "parseCueChunk: numCuePoints = " + numCuePoints
+ // );
+ for (int i = 0; i < numCuePoints; i++) {
+ // Some AIF files have a bogus numCuePoints so check to see if we
+ // are at end.
+ long numInMark = parser.getOffset() - startOffset;
+ if (numInMark >= ckSize) {
+ System.out.println("Reached end of MARK chunk with bogus numCuePoints = "
+ + numCuePoints);
+ break;
+ }
+
+ int uniqueID = parser.readShortBig();
+ int position = parser.readIntBig();
+ int len = parser.read();
+ String markerName = parseString(parser, len);
+ if ((len & 1) == 0) {
+ parser.skip(1); /* skip pad byte */
+ }
+
+ SampleMarker cuePoint = findOrCreateCuePoint(uniqueID);
+ cuePoint.position = position;
+ cuePoint.name = markerName;
+
+ if (IFFParser.debug) {
+ System.out.println("AIFF Marker at " + position + ", " + markerName);
+ }
+ }
+ }
+
+ /**
+ * Called by parse() method to handle FORM chunks in an AIFF specific manner.
+ *
+ * @param ckID four byte chunk ID such as 'data'
+ * @param ckSize size of chunk in bytes
+ * @exception IOException If parsing fails, or IO error occurs.
+ */
+ @Override
+ public void handleForm(IFFParser parser, int ckID, int ckSize, int type) throws IOException {
+ if ((ckID == IFFParser.FORM_ID) && (type != AIFF_ID) && (type != AIFC_ID))
+ throw new IOException("Bad AIFF form type = " + IFFParser.IDToString(type));
+ }
+
+ /**
+ * Called by parse() method to handle chunks in an AIFF specific manner.
+ *
+ * @param ckID four byte chunk ID such as 'data'
+ * @param ckSize size of chunk in bytes
+ * @exception IOException If parsing fails, or IO error occurs.
+ */
+ @Override
+ public void handleChunk(IFFParser parser, int ckID, int ckSize) throws IOException {
+ switch (ckID) {
+ case COMM_ID:
+ parseCOMMChunk(parser, ckSize);
+ break;
+ case SSND_ID:
+ parseSSNDChunk(parser, ckSize);
+ break;
+ case MARK_ID:
+ parseMARKChunk(parser, ckSize);
+ break;
+ case INST_ID:
+ parseINSTChunk(parser, ckSize);
+ break;
+ default:
+ break;
+ }
+ }
+
+}
diff --git a/src/com/jsyn/util/soundfile/AudioFileParser.java b/src/com/jsyn/util/soundfile/AudioFileParser.java
new file mode 100644
index 0000000..e7bb066
--- /dev/null
+++ b/src/com/jsyn/util/soundfile/AudioFileParser.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2001 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util.soundfile;
+
+import java.io.IOException;
+import java.util.HashMap;
+
+import com.jsyn.data.FloatSample;
+import com.jsyn.data.SampleMarker;
+
+/**
+ * Base class for various types of audio specific file parsers.
+ *
+ * @author (C) 2001 Phil Burk, SoftSynth.com
+ */
+
+abstract class AudioFileParser implements ChunkHandler {
+ IFFParser parser;
+ protected byte[] byteData;
+ boolean ifLoadData = true; /* If true, load sound data into memory. */
+ long dataPosition; /*
+ * Number of bytes from beginning of file where sound data resides.
+ */
+ protected int bitsPerSample;
+ protected int bytesPerFrame; // in the file
+ protected int bytesPerSample; // in the file
+ protected HashMap<Integer, SampleMarker> cueMap = new HashMap<Integer, SampleMarker>();
+ protected short samplesPerFrame;
+ protected double frameRate;
+ protected int numFrames;
+ protected double originalPitch = 60.0;
+ protected int sustainBegin = -1;
+ protected int sustainEnd = -1;
+
+ public AudioFileParser() {
+ }
+
+ /**
+ * @return Number of bytes from beginning of stream where sound data resides.
+ */
+ public long getDataPosition() {
+ return dataPosition;
+ }
+
+ /**
+ * This can be read by another thread when load()ing a sample to determine how many bytes have
+ * been read so far.
+ */
+ public synchronized long getNumBytesRead() {
+ IFFParser p = parser; // prevent race
+ if (p != null)
+ return p.getOffset();
+ else
+ return 0;
+ }
+
+ /**
+ * This can be read by another thread when load()ing a sample to determine how many bytes need
+ * to be read.
+ */
+ public synchronized long getFileSize() {
+ IFFParser p = parser; // prevent race
+ if (p != null)
+ return p.getFileSize();
+ else
+ return 0;
+ }
+
+ protected SampleMarker findOrCreateCuePoint(int uniqueID) {
+ SampleMarker cuePoint = cueMap.get(uniqueID);
+ if (cuePoint == null) {
+ cuePoint = new SampleMarker();
+ cueMap.put(uniqueID, cuePoint);
+ }
+ return cuePoint;
+ }
+
+ public FloatSample load(IFFParser parser) throws IOException {
+ this.parser = parser;
+ parser.parseAfterHead(this);
+ return finish();
+ }
+
+ abstract FloatSample finish() throws IOException;
+
+ FloatSample makeSample(float[] floatData) throws IOException {
+ FloatSample floatSample = new FloatSample(floatData, samplesPerFrame);
+
+ floatSample.setChannelsPerFrame(samplesPerFrame);
+ floatSample.setFrameRate(frameRate);
+ floatSample.setPitch(originalPitch);
+
+ if (sustainBegin >= 0) {
+ floatSample.setSustainBegin(sustainBegin);
+ floatSample.setSustainEnd(sustainEnd);
+ }
+
+ for (SampleMarker marker : cueMap.values()) {
+ floatSample.addMarker(marker);
+ }
+
+ /* Set Sustain Loop by assuming first two markers are loop points. */
+ if (floatSample.getMarkerCount() >= 2) {
+ floatSample.setSustainBegin(floatSample.getMarker(0).position);
+ floatSample.setSustainEnd(floatSample.getMarker(1).position);
+ }
+ return floatSample;
+ }
+
+ protected String parseString(IFFParser parser, int textLength) throws IOException {
+ byte[] bar = new byte[textLength];
+ parser.read(bar);
+ return new String(bar);
+ }
+}
diff --git a/src/com/jsyn/util/soundfile/ChunkHandler.java b/src/com/jsyn/util/soundfile/ChunkHandler.java
new file mode 100644
index 0000000..6dfe26d
--- /dev/null
+++ b/src/com/jsyn/util/soundfile/ChunkHandler.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 1997 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util.soundfile;
+
+import java.io.IOException;
+
+/**
+ * Handle IFF Chunks as they are parsed from an IFF or RIFF file.
+ *
+ * @see IFFParser
+ * @see AudioSampleAIFF
+ * @author (C) 1997 Phil Burk, SoftSynth.com
+ */
+interface ChunkHandler {
+ /**
+ * The parser will call this when it encounters a FORM or LIST chunk that contains other chunks.
+ * This handler can either read the form's chunks, or let the parser find them and call
+ * handleChunk().
+ *
+ * @param ID a 4 byte identifier such as FORM_ID that identifies the IFF chunk type.
+ * @param numBytes number of bytes contained in the FORM, not counting the FORM type.
+ * @param type a 4 byte identifier such as AIFF_ID that identifies the FORM type.
+ */
+ public void handleForm(IFFParser parser, int ID, int numBytes, int type) throws IOException;
+
+ /**
+ * The parser will call this when it encounters a chunk that is not a FORM or LIST. This handler
+ * can either read the chunk's, or ignore it. The parser will skip over any unread data. Do NOT
+ * read past the end of the chunk!
+ *
+ * @param ID a 4 byte identifier such as SSND_ID that identifies the IFF chunk type.
+ * @param numBytes number of bytes contained in the chunk, not counting the ID and size field.
+ */
+ public void handleChunk(IFFParser parser, int ID, int numBytes) throws IOException;
+}
diff --git a/src/com/jsyn/util/soundfile/CustomSampleLoader.java b/src/com/jsyn/util/soundfile/CustomSampleLoader.java
new file mode 100644
index 0000000..14efde9
--- /dev/null
+++ b/src/com/jsyn/util/soundfile/CustomSampleLoader.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util.soundfile;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+
+import com.jsyn.data.FloatSample;
+import com.jsyn.util.AudioSampleLoader;
+
+public class CustomSampleLoader implements AudioSampleLoader {
+
+ @Override
+ public FloatSample loadFloatSample(File fileIn) throws IOException {
+ FileInputStream fileStream = new FileInputStream(fileIn);
+ BufferedInputStream inputStream = new BufferedInputStream(fileStream);
+ return loadFloatSample(inputStream);
+ }
+
+ @Override
+ public FloatSample loadFloatSample(URL url) throws IOException {
+ InputStream rawStream = url.openStream();
+ BufferedInputStream inputStream = new BufferedInputStream(rawStream);
+ return loadFloatSample(inputStream);
+ }
+
+ @Override
+ public FloatSample loadFloatSample(InputStream inputStream) throws IOException {
+ AudioFileParser fileParser;
+ IFFParser parser = new IFFParser(inputStream);
+ parser.readHead();
+ if (parser.isRIFF()) {
+ fileParser = new WAVEFileParser();
+ } else if (parser.isIFF()) {
+ fileParser = new AIFFFileParser();
+ } else {
+ throw new IOException("Unsupported audio file type.");
+ }
+ return fileParser.load(parser);
+ }
+
+}
diff --git a/src/com/jsyn/util/soundfile/IFFParser.java b/src/com/jsyn/util/soundfile/IFFParser.java
new file mode 100644
index 0000000..f429657
--- /dev/null
+++ b/src/com/jsyn/util/soundfile/IFFParser.java
@@ -0,0 +1,307 @@
+/*
+ * Copyright 1997 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util.soundfile;
+
+import java.io.EOFException;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Parse Electronic Arts style IFF File. IFF is a file format that allows "chunks" of data to be
+ * placed in a hierarchical file. It was designed by Jerry Morrison at Electronic Arts for the Amiga
+ * computer and is now used extensively by Apple Computer and other companies. IFF is an open
+ * standard.
+ *
+ * @see RIFFParser
+ * @see AudioSampleAIFF
+ * @author (C) 1997 Phil Burk, SoftSynth.com
+ */
+
+class IFFParser extends FilterInputStream {
+ private long numBytesRead = 0;
+ private long totalSize = 0;
+ private int fileId;
+ static boolean debug = false;
+
+ public static final int RIFF_ID = ('R' << 24) | ('I' << 16) | ('F' << 8) | 'F';
+ public static final int LIST_ID = ('L' << 24) | ('I' << 16) | ('S' << 8) | 'T';
+ public static final int FORM_ID = ('F' << 24) | ('O' << 16) | ('R' << 8) | 'M';
+
+ IFFParser(InputStream stream) {
+ super(stream);
+ numBytesRead = 0;
+ }
+
+ /**
+ * Size of file based on outermost chunk size plus 8. Can be used to report progress when
+ * loading samples.
+ *
+ * @return Number of bytes in outer chunk plus header.
+ */
+ public long getFileSize() {
+ return totalSize;
+ }
+
+ /**
+ * Since IFF files use chunks with explicit size, it is important to keep track of how many
+ * bytes have been read from the file. Can be used to report progress when loading samples.
+ *
+ * @return Number of bytes read from stream, or skipped.
+ */
+ public long getOffset() {
+ return numBytesRead;
+ }
+
+ /** @return Next byte from stream. Increment offset by 1. */
+ @Override
+ public int read() throws IOException {
+ numBytesRead++;
+ return super.read();
+ }
+
+ /** @return Next byte array from stream. Increment offset by len. */
+ @Override
+ public int read(byte[] bar) throws IOException {
+ return read(bar, 0, bar.length);
+ }
+
+ /** @return Next byte array from stream. Increment offset by len. */
+ @Override
+ public int read(byte[] bar, int off, int len) throws IOException {
+ // Reading from a URL can return before all the bytes are available.
+ // So we keep reading until we get the whole thing.
+ int cursor = off;
+ int numLeft = len;
+ // keep reading data until we get it all
+ while (numLeft > 0) {
+ int numRead = super.read(bar, cursor, numLeft);
+ if (numRead < 0)
+ return numRead;
+ cursor += numRead;
+ numBytesRead += numRead;
+ numLeft -= numRead;
+ // System.out.println("read " + numRead + ", cursor = " + cursor +
+ // ", len = " + len);
+ }
+ return cursor - off;
+ }
+
+ /** @return Skip forward in stream and add numBytes to offset. */
+ @Override
+ public long skip(long numBytes) throws IOException {
+ numBytesRead += numBytes;
+ return super.skip(numBytes);
+ }
+
+ /** Read 32 bit signed integer assuming Big Endian byte order. */
+ public int readIntBig() throws IOException {
+ int result = read() & 0xFF;
+ result = (result << 8) | (read() & 0xFF);
+ result = (result << 8) | (read() & 0xFF);
+ int data = read();
+ if (data == -1)
+ throw new EOFException("readIntBig() - EOF in middle of word at offset " + numBytesRead);
+ result = (result << 8) | (data & 0xFF);
+ return result;
+ }
+
+ /** Read 32 bit signed integer assuming Little Endian byte order. */
+ public int readIntLittle() throws IOException {
+ int result = read() & 0xFF; // LSB
+ result |= ((read() & 0xFF) << 8);
+ result |= ((read() & 0xFF) << 16);
+ int data = read();
+ if (data == -1)
+ throw new EOFException("readIntLittle() - EOF in middle of word at offset "
+ + numBytesRead);
+ result |= (data << 24);
+ return result;
+ }
+
+ /** Read 16 bit signed short assuming Big Endian byte order. */
+ public short readShortBig() throws IOException {
+ short result = (short) ((read() << 8)); // MSB
+ int data = read();
+ if (data == -1)
+ throw new EOFException("readShortBig() - EOF in middle of word at offset "
+ + numBytesRead);
+ result |= data & 0xFF;
+ return result;
+ }
+
+ /** Read 16 bit signed short assuming Little Endian byte order. */
+ public short readShortLittle() throws IOException {
+ short result = (short) (read() & 0xFF); // LSB
+ int data = read(); // MSB
+ if (data == -1)
+ throw new EOFException("readShortLittle() - EOF in middle of word at offset "
+ + numBytesRead);
+ result |= data << 8;
+ return result;
+ }
+
+ public int readUShortLittle() throws IOException {
+ return (readShortLittle()) & 0x0000FFFF;
+ }
+
+ /** Read 8 bit signed byte. */
+ public byte readByte() throws IOException {
+ return (byte) read();
+ }
+
+ /** Read 32 bit signed int assuming IFF order. */
+ public int readChunkSize() throws IOException {
+ if (isRIFF()) {
+ return readIntLittle();
+ }
+ {
+ return readIntBig();
+ }
+ }
+
+ /** Convert a 4 character IFF ID to a String */
+ public static String IDToString(int ID) {
+ byte bar[] = new byte[4];
+ bar[0] = (byte) (ID >> 24);
+ bar[1] = (byte) (ID >> 16);
+ bar[2] = (byte) (ID >> 8);
+ bar[3] = (byte) ID;
+ return new String(bar);
+ }
+
+ /**
+ * Parse the stream after reading the first ID and pass the forms and chunks to the ChunkHandler
+ */
+ public void parseAfterHead(ChunkHandler handler) throws IOException {
+ int numBytes = readChunkSize();
+ totalSize = numBytes + 8;
+ parseChunk(handler, fileId, numBytes);
+ if (debug)
+ System.out.println("parse() ------- end");
+ }
+
+ /**
+ * Parse the FORM and pass the chunks to the ChunkHandler The cursor should be positioned right
+ * after the type field.
+ */
+ void parseForm(ChunkHandler handler, int ID, int numBytes, int type) throws IOException {
+ if (debug) {
+ System.out.println("IFF: parseForm >>>>>>>>>>>>>>>>>> BEGIN");
+ }
+ while (numBytes > 8) {
+ int ckid = readIntBig();
+ int size = readChunkSize();
+ numBytes -= 8;
+ if (debug) {
+ System.out.println("chunk( " + IDToString(ckid) + ", " + size + " )");
+ }
+ if (size < 0) {
+ throw new IOException("Bad IFF chunk Size: " + IDToString(ckid) + " = 0x"
+ + Integer.toHexString(ckid) + ", Size = " + size);
+ }
+ parseChunk(handler, ckid, size);
+ if ((size & 1) == 1)
+ size++; // even-up
+ numBytes -= size;
+ if (debug) {
+ System.out.println("parseForm: numBytes left in form = " + numBytes);
+ }
+ }
+ if (debug) {
+ System.out.println("IFF: parseForm <<<<<<<<<<<<<<<<<<<< END");
+ }
+
+ if (numBytes > 0) {
+ System.out.println("IFF Parser detected " + numBytes
+ + " bytes of garbage at end of FORM.");
+ skip(numBytes);
+ }
+ }
+
+ /*
+ * Parse one chunk from IFF file. After calling handler, make sure stream is positioned at end
+ * of chunk.
+ */
+ void parseChunk(ChunkHandler handler, int ckid, int numBytes) throws IOException {
+ long startOffset, endOffset;
+ int numRead;
+ startOffset = getOffset();
+ if (isForm(ckid)) {
+ int type = readIntBig();
+ if (debug)
+ System.out.println("parseChunk: form = " + IDToString(ckid) + ", " + numBytes
+ + ", " + IDToString(type));
+ handler.handleForm(this, ckid, numBytes - 4, type);
+ endOffset = getOffset();
+ numRead = (int) (endOffset - startOffset);
+ if (numRead < numBytes)
+ parseForm(handler, ckid, (numBytes - numRead), type);
+ } else {
+ handler.handleChunk(this, ckid, numBytes);
+ }
+ endOffset = getOffset();
+ numRead = (int) (endOffset - startOffset);
+ if (debug) {
+ System.out.println("parseChunk: endOffset = " + endOffset);
+ System.out.println("parseChunk: numRead = " + numRead);
+ }
+ if ((numBytes & 1) == 1)
+ numBytes++; // even-up
+ if (numRead < numBytes)
+ skip(numBytes - numRead);
+ }
+
+ public void readHead() throws IOException {
+ if (debug)
+ System.out.println("parse() ------- begin");
+ numBytesRead = 0;
+ fileId = readIntBig();
+ }
+
+ public boolean isRIFF() {
+ return (fileId == RIFF_ID);
+ }
+
+ public boolean isIFF() {
+ return (fileId == FORM_ID);
+ }
+
+ /**
+ * Does the following chunk ID correspond to a container type like FORM?
+ */
+ public boolean isForm(int ckid) {
+ if (isRIFF()) {
+ switch (ckid) {
+ case LIST_ID:
+ case RIFF_ID:
+ return true;
+ default:
+ return false;
+ }
+ } else {
+ switch (ckid) {
+ case LIST_ID:
+ case FORM_ID:
+ return true;
+ default:
+ return false;
+ }
+ }
+ }
+
+}
diff --git a/src/com/jsyn/util/soundfile/WAVEFileParser.java b/src/com/jsyn/util/soundfile/WAVEFileParser.java
new file mode 100644
index 0000000..ec9350c
--- /dev/null
+++ b/src/com/jsyn/util/soundfile/WAVEFileParser.java
@@ -0,0 +1,334 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util.soundfile;
+
+import java.io.EOFException;
+import java.io.IOException;
+
+import com.jsyn.data.FloatSample;
+import com.jsyn.data.SampleMarker;
+import com.jsyn.util.SampleLoader;
+
+class WAVEFileParser extends AudioFileParser implements ChunkHandler {
+ static final short WAVE_FORMAT_PCM = 1;
+ static final short WAVE_FORMAT_IEEE_FLOAT = 3;
+ static final short WAVE_FORMAT_EXTENSIBLE = (short) 0xFFFE;
+
+ static final byte[] KSDATAFORMAT_SUBTYPE_IEEE_FLOAT = {
+ 3, 0, 0, 0, 0, 0, 16, 0, -128, 0, 0, -86, 0, 56, -101, 113
+ };
+ static final byte[] KSDATAFORMAT_SUBTYPE_PCM = {
+ 1, 0, 0, 0, 0, 0, 16, 0, -128, 0, 0, -86, 0, 56, -101, 113
+ };
+
+ static final int WAVE_ID = ('W' << 24) | ('A' << 16) | ('V' << 8) | 'E';
+ static final int FMT_ID = ('f' << 24) | ('m' << 16) | ('t' << 8) | ' ';
+ static final int DATA_ID = ('d' << 24) | ('a' << 16) | ('t' << 8) | 'a';
+ static final int CUE_ID = ('c' << 24) | ('u' << 16) | ('e' << 8) | ' ';
+ static final int FACT_ID = ('f' << 24) | ('a' << 16) | ('c' << 8) | 't';
+ static final int SMPL_ID = ('s' << 24) | ('m' << 16) | ('p' << 8) | 'l';
+ static final int LTXT_ID = ('l' << 24) | ('t' << 16) | ('x' << 8) | 't';
+ static final int LABL_ID = ('l' << 24) | ('a' << 16) | ('b' << 8) | 'l';
+
+ int samplesPerBlock = 0;
+ int blockAlign = 0;
+ private int numFactSamples = 0;
+ private short format;
+
+ WAVEFileParser() {
+ }
+
+ @Override
+ FloatSample finish() throws IOException {
+ if ((byteData == null)) {
+ throw new IOException("No data found in audio sample.");
+ }
+ float[] floatData = new float[numFrames * samplesPerFrame];
+ if (bitsPerSample == 16) {
+ SampleLoader.decodeLittleI16ToF32(byteData, 0, byteData.length, floatData, 0);
+ } else if (bitsPerSample == 24) {
+ SampleLoader.decodeLittleI24ToF32(byteData, 0, byteData.length, floatData, 0);
+ } else if (bitsPerSample == 32) {
+ if (format == WAVE_FORMAT_IEEE_FLOAT) {
+ SampleLoader.decodeLittleF32ToF32(byteData, 0, byteData.length, floatData, 0);
+ } else if (format == WAVE_FORMAT_PCM) {
+ SampleLoader.decodeLittleI32ToF32(byteData, 0, byteData.length, floatData, 0);
+ } else {
+ throw new IOException("WAV: Unsupported format = " + format);
+ }
+ } else {
+ throw new IOException("WAV: Unsupported bitsPerSample = " + bitsPerSample);
+ }
+
+ return makeSample(floatData);
+ }
+
+ // typedef struct {
+ // long dwIdentifier;
+ // long dwPosition;
+ // ID fccChunk;
+ // long dwChunkStart;
+ // long dwBlockStart;
+ // long dwSampleOffset;
+ // } CuePoint;
+
+ /* Parse various chunks encountered in WAV file. */
+ void parseCueChunk(IFFParser parser, int ckSize) throws IOException {
+ int numCuePoints = parser.readIntLittle();
+ if (IFFParser.debug) {
+ System.out.println("WAV: numCuePoints = " + numCuePoints);
+ }
+ if ((ckSize - 4) != (6 * 4 * numCuePoints))
+ throw new EOFException("Cue chunk too short!");
+ for (int i = 0; i < numCuePoints; i++) {
+ int dwName = parser.readIntLittle(); /* dwName */
+ int position = parser.readIntLittle(); // dwPosition
+ parser.skip(3 * 4); // fccChunk, dwChunkStart, dwBlockStart
+ int sampleOffset = parser.readIntLittle(); // dwPosition
+
+ if (IFFParser.debug) {
+ System.out.println("WAV: parseCueChunk: #" + i + ", dwPosition = " + position
+ + ", dwName = " + dwName + ", dwSampleOffset = " + sampleOffset);
+ }
+ SampleMarker cuePoint = findOrCreateCuePoint(dwName);
+ cuePoint.position = position;
+ }
+ }
+
+ void parseLablChunk(IFFParser parser, int ckSize) throws IOException {
+ int dwName = parser.readIntLittle();
+ int textLength = (ckSize - 4) - 1; // don't read NUL terminator
+ String text = parseString(parser, textLength);
+ if (IFFParser.debug) {
+ System.out.println("WAV: label id = " + dwName + ", text = " + text);
+ }
+ SampleMarker cuePoint = findOrCreateCuePoint(dwName);
+ cuePoint.name = text;
+ }
+
+ void parseLtxtChunk(IFFParser parser, int ckSize) throws IOException {
+ int dwName = parser.readIntLittle();
+ int dwSampleLength = parser.readIntLittle();
+ parser.skip(4 + (4 * 2)); // purpose through codepage
+ int textLength = (ckSize - ((4 * 4) + (4 * 2))) - 1; // don't read NUL
+ // terminator
+ if (textLength > 0) {
+ String text = parseString(parser, textLength);
+ if (IFFParser.debug) {
+ System.out.println("WAV: ltxt id = " + dwName + ", dwSampleLength = "
+ + dwSampleLength + ", text = " + text);
+ }
+ SampleMarker cuePoint = findOrCreateCuePoint(dwName);
+ cuePoint.comment = text;
+ }
+ }
+
+ void parseFmtChunk(IFFParser parser, int ckSize) throws IOException {
+ format = parser.readShortLittle();
+ samplesPerFrame = parser.readShortLittle();
+ frameRate = parser.readIntLittle();
+ parser.readIntLittle(); /* skip dwAvgBytesPerSec */
+ blockAlign = parser.readShortLittle();
+ bitsPerSample = parser.readShortLittle();
+
+ if (IFFParser.debug) {
+ System.out.println("WAV: format = 0x" + Integer.toHexString(format));
+ System.out.println("WAV: bitsPerSample = " + bitsPerSample);
+ System.out.println("WAV: samplesPerFrame = " + samplesPerFrame);
+ }
+ bytesPerFrame = blockAlign;
+ bytesPerSample = bytesPerFrame / samplesPerFrame;
+ samplesPerBlock = (8 * blockAlign) / bitsPerSample;
+
+ if (format == WAVE_FORMAT_EXTENSIBLE) {
+ int extraSize = parser.readShortLittle();
+ short validBitsPerSample = parser.readShortLittle();
+ int channelMask = parser.readIntLittle();
+ byte[] guid = new byte[16];
+ parser.read(guid);
+ if (IFFParser.debug) {
+ System.out.println("WAV: extraSize = " + extraSize);
+ System.out.println("WAV: validBitsPerSample = " + validBitsPerSample);
+ System.out.println("WAV: channelMask = " + channelMask);
+ System.out.print("guid = {");
+ for (int i = 0; i < guid.length; i++) {
+ System.out.print(guid[i] + ", ");
+ }
+ System.out.println("}");
+ }
+ if (matchBytes(guid, KSDATAFORMAT_SUBTYPE_IEEE_FLOAT)) {
+ format = WAVE_FORMAT_IEEE_FLOAT;
+ } else if (matchBytes(guid, KSDATAFORMAT_SUBTYPE_PCM)) {
+ format = WAVE_FORMAT_PCM;
+ }
+ }
+ if ((format != WAVE_FORMAT_PCM) && (format != WAVE_FORMAT_IEEE_FLOAT)) {
+ throw new IOException(
+ "Only WAVE_FORMAT_PCM and WAVE_FORMAT_IEEE_FLOAT supported. format = " + format);
+ }
+ if ((bitsPerSample != 16) && (bitsPerSample != 24) && (bitsPerSample != 32)) {
+ throw new IOException(
+ "Only 16 and 24 bit PCM or 32-bit float WAV files supported. width = "
+ + bitsPerSample);
+ }
+ }
+
+ private boolean matchBytes(byte[] bar1, byte[] bar2) {
+ if (bar1.length != bar2.length)
+ return false;
+ for (int i = 0; i < bar1.length; i++) {
+ if (bar1[i] != bar2[i])
+ return false;
+ }
+ return true;
+ }
+
+ private int convertByteToFrame(int byteOffset) throws IOException {
+ if (blockAlign == 0) {
+ throw new IOException("WAV file has bytesPerBlock = zero");
+ }
+ if (samplesPerFrame == 0) {
+ throw new IOException("WAV file has samplesPerFrame = zero");
+ }
+ int nFrames = (samplesPerBlock * byteOffset) / (samplesPerFrame * blockAlign);
+ return nFrames;
+ }
+
+ private int calculateNumFrames(int numBytes) throws IOException {
+ int nFrames;
+ if (numFactSamples > 0) {
+ // nFrames = numFactSamples / samplesPerFrame;
+ nFrames = numFactSamples; // FIXME which is right
+ } else {
+ nFrames = convertByteToFrame(numBytes);
+ }
+ return nFrames;
+ }
+
+ // Read fraction in range of 0 to 0xFFFFFFFF and
+ // convert to 0.0 to 1.0 range.
+ private double readFraction(IFFParser parser) throws IOException {
+ // Put L at end or we get -1.
+ long maxFraction = 0x0FFFFFFFFL;
+ // Get unsigned fraction. Have to fit in long.
+ long fraction = (parser.readIntLittle()) & maxFraction;
+ return (double) fraction / (double) maxFraction;
+ }
+
+ void parseSmplChunk(IFFParser parser, int ckSize) throws IOException {
+ parser.readIntLittle(); // Manufacturer
+ parser.readIntLittle(); // Product
+ parser.readIntLittle(); // Sample Period
+ int unityNote = parser.readIntLittle();
+ double pitchFraction = readFraction(parser);
+ originalPitch = unityNote + pitchFraction;
+
+ parser.readIntLittle(); // SMPTE Format
+ parser.readIntLittle(); // SMPTE Offset
+ int numLoops = parser.readIntLittle();
+ parser.readIntLittle(); // Sampler Data
+
+ int lastCueID = Integer.MAX_VALUE;
+ for (int i = 0; i < numLoops; i++) {
+ int cueID = parser.readIntLittle();
+ parser.readIntLittle(); // type
+ int loopStartPosition = parser.readIntLittle();
+ // Point to sample one after.
+ int loopEndPosition = parser.readIntLittle() + 1;
+ // TODO handle fractional loop sizes?
+ double endFraction = readFraction(parser);
+ parser.readIntLittle(); // playCount
+
+ // Use lowest numbered cue.
+ if (cueID < lastCueID) {
+ sustainBegin = loopStartPosition;
+ sustainEnd = loopEndPosition;
+ }
+ }
+ }
+
+ void parseFactChunk(IFFParser parser, int ckSize) throws IOException {
+ numFactSamples = parser.readIntLittle();
+ }
+
+ void parseDataChunk(IFFParser parser, int ckSize) throws IOException {
+ long numRead;
+ dataPosition = parser.getOffset();
+ if (ifLoadData) {
+ byteData = new byte[ckSize];
+ numRead = parser.read(byteData);
+ } else {
+ numRead = parser.skip(ckSize);
+ }
+ if (numRead != ckSize) {
+ throw new EOFException("WAV data chunk too short! Read " + numRead + " instead of "
+ + ckSize);
+ }
+ numFrames = calculateNumFrames(ckSize);
+ }
+
+ @Override
+ public void handleForm(IFFParser parser, int ckID, int ckSize, int type) throws IOException {
+ if ((ckID == IFFParser.RIFF_ID) && (type != WAVE_ID))
+ throw new IOException("Bad WAV form type = " + IFFParser.IDToString(type));
+ }
+
+ /**
+ * Called by parse() method to handle chunks in a WAV specific manner.
+ *
+ * @param ckID four byte chunk ID such as 'data'
+ * @param ckSize size of chunk in bytes
+ * @return number of bytes left in chunk
+ */
+ @Override
+ public void handleChunk(IFFParser parser, int ckID, int ckSize) throws IOException {
+ switch (ckID) {
+ case FMT_ID:
+ parseFmtChunk(parser, ckSize);
+ break;
+ case DATA_ID:
+ parseDataChunk(parser, ckSize);
+ break;
+ case CUE_ID:
+ parseCueChunk(parser, ckSize);
+ break;
+ case FACT_ID:
+ parseFactChunk(parser, ckSize);
+ break;
+ case SMPL_ID:
+ parseSmplChunk(parser, ckSize);
+ break;
+ case LABL_ID:
+ parseLablChunk(parser, ckSize);
+ break;
+ case LTXT_ID:
+ parseLtxtChunk(parser, ckSize);
+ break;
+ default:
+ break;
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.softsynth.javasonics.util.AudioSampleLoader#isLittleEndian()
+ */
+ boolean isLittleEndian() {
+ return true;
+ }
+
+}
diff --git a/src/com/softsynth/math/AudioMath.java b/src/com/softsynth/math/AudioMath.java
new file mode 100644
index 0000000..64f064f
--- /dev/null
+++ b/src/com/softsynth/math/AudioMath.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 1998 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.softsynth.math;
+
+/**
+ * Miscellaneous math functions useful in Audio
+ *
+ * @author (C) 1998 Phil Burk
+ */
+public class AudioMath {
+ // use scalar to convert natural log to log_base_10
+ private final static double a2dScalar = 20.0 / Math.log(10.0);
+ public static final int CONCERT_A_PITCH = 69;
+ public static final double CONCERT_A_FREQUENCY = 440.0;
+
+ /**
+ * Convert amplitude to decibels. 1.0 is zero dB. 0.5 is -6.02 dB.
+ */
+ public static double amplitudeToDecibels(double amplitude) {
+ double db = Math.log(amplitude) * a2dScalar;
+ return db;
+ }
+
+ /**
+ * Convert decibels to amplitude. Zero dB is 1.0 and -6.02 dB is 0.5.
+ */
+ public static double decibelsToAmplitude(double decibels) {
+ double amp = Math.pow(10.0, decibels / 20.0);
+ return amp;
+ }
+
+ /**
+ * Calculate MIDI pitch based on frequency in Hertz. Middle C is 60.0.
+ */
+ public static double frequencyToPitch(double frequency) {
+ return CONCERT_A_PITCH + 12 * Math.log(frequency / CONCERT_A_FREQUENCY) / Math.log(2.0);
+ }
+
+ /**
+ * Calculate frequency in Hertz based on MIDI pitch. Middle C is 60.0. You can use fractional
+ * pitches so 60.5 would give you a pitch half way between C and C#.
+ */
+ public static double pitchToFrequency(double pitch) {
+ return CONCERT_A_FREQUENCY * Math.pow(2.0, ((pitch - CONCERT_A_PITCH) * (1.0 / 12.0)));
+ }
+}
diff --git a/src/com/softsynth/math/ChebyshevPolynomial.java b/src/com/softsynth/math/ChebyshevPolynomial.java
new file mode 100644
index 0000000..bc0e854
--- /dev/null
+++ b/src/com/softsynth/math/ChebyshevPolynomial.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 1997 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.softsynth.math;
+
+/**
+ * ChebyshevPolynomial<br>
+ * Used to generate data for waveshaping table oscillators.
+ *
+ * @author Nick Didkovsky (C) 1997 Phil Burk and Nick Didkovsky
+ */
+
+public class ChebyshevPolynomial {
+ static final Polynomial twoX = new Polynomial(2, 0);
+ static final Polynomial one = new Polynomial(1);
+ static final Polynomial oneX = new Polynomial(1, 0);
+
+ /**
+ * Calculates Chebyshev polynomial of specified integer order. Recursively generated using
+ * relation Tk+1(x) = 2xTk(x) - Tk-1(x)
+ *
+ * @return Chebyshev polynomial of specified order
+ */
+ public static Polynomial T(int order) {
+ if (order == 0)
+ return one;
+ else if (order == 1)
+ return oneX;
+ else
+ return Polynomial.minus(Polynomial.mult(T(order - 1), (twoX)), T(order - 2));
+ }
+}
diff --git a/src/com/softsynth/math/FourierMath.java b/src/com/softsynth/math/FourierMath.java
new file mode 100644
index 0000000..82d4ec9
--- /dev/null
+++ b/src/com/softsynth/math/FourierMath.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.softsynth.math;
+
+//Simple Fast Fourier Transform.
+public class FourierMath {
+ static private final int MAX_SIZE_LOG_2 = 16;
+ static BitReverseTable[] reverseTables = new BitReverseTable[MAX_SIZE_LOG_2];
+ static DoubleSineTable[] sineTables = new DoubleSineTable[MAX_SIZE_LOG_2];
+ static FloatSineTable[] floatSineTables = new FloatSineTable[MAX_SIZE_LOG_2];
+
+ private static class DoubleSineTable {
+ double[] sineValues;
+
+ DoubleSineTable(int numBits) {
+ int len = 1 << numBits;
+ sineValues = new double[1 << numBits];
+ for (int i = 0; i < len; i++) {
+ sineValues[i] = Math.sin((i * Math.PI * 2.0) / len);
+ }
+ }
+ }
+
+ private static double[] getDoubleSineTable(int n) {
+ DoubleSineTable sineTable = sineTables[n];
+ if (sineTable == null) {
+ sineTable = new DoubleSineTable(n);
+ sineTables[n] = sineTable;
+ }
+ return sineTable.sineValues;
+ }
+
+ private static class FloatSineTable {
+ float[] sineValues;
+
+ FloatSineTable(int numBits) {
+ int len = 1 << numBits;
+ sineValues = new float[1 << numBits];
+ for (int i = 0; i < len; i++) {
+ sineValues[i] = (float) Math.sin((i * Math.PI * 2.0) / len);
+ }
+ }
+ }
+
+ private static float[] getFloatSineTable(int n) {
+ FloatSineTable sineTable = floatSineTables[n];
+ if (sineTable == null) {
+ sineTable = new FloatSineTable(n);
+ floatSineTables[n] = sineTable;
+ }
+ return sineTable.sineValues;
+ }
+
+ private static class BitReverseTable {
+ int[] reversedBits;
+
+ BitReverseTable(int numBits) {
+ reversedBits = new int[1 << numBits];
+ for (int i = 0; i < reversedBits.length; i++) {
+ reversedBits[i] = reverseBits(i, numBits);
+ }
+ }
+
+ static int reverseBits(int index, int numBits) {
+ int i, rev;
+
+ for (i = rev = 0; i < numBits; i++) {
+ rev = (rev << 1) | (index & 1);
+ index >>= 1;
+ }
+
+ return rev;
+ }
+ }
+
+ private static int[] getReverseTable(int n) {
+ BitReverseTable reverseTable = reverseTables[n];
+ if (reverseTable == null) {
+ reverseTable = new BitReverseTable(n);
+ reverseTables[n] = reverseTable;
+ }
+ return reverseTable.reversedBits;
+ }
+
+ /**
+ * Calculate the amplitude of the sine wave associated with each bin of a complex FFT result.
+ *
+ * @param ar
+ * @param ai
+ * @param magnitudes
+ */
+ public static void calculateMagnitudes(double ar[], double ai[], double[] magnitudes) {
+ for (int i = 0; i < magnitudes.length; ++i) {
+ magnitudes[i] = Math.sqrt((ar[i] * ar[i]) + (ai[i] * ai[i]));
+ }
+ }
+
+ /**
+ * Calculate the amplitude of the sine wave associated with each bin of a complex FFT result.
+ *
+ * @param ar
+ * @param ai
+ * @param magnitudes
+ */
+ public static void calculateMagnitudes(float ar[], float ai[], float[] magnitudes) {
+ for (int i = 0; i < magnitudes.length; ++i) {
+ magnitudes[i] = (float) Math.sqrt((ar[i] * ar[i]) + (ai[i] * ai[i]));
+ }
+ }
+
+ public static void transform(int sign, int n, double ar[], double ai[]) {
+ double scale = Math.sqrt(1.0 / n);
+
+ int numBits = FourierMath.numBits(n);
+ int[] reverseTable = getReverseTable(numBits);
+ double[] sineTable = getDoubleSineTable(numBits);
+ int mask = n - 1;
+ int cosineOffset = n / 4; // phase offset between cos and sin
+
+ int i, j;
+ for (i = 0; i < n; i++) {
+ j = reverseTable[i];
+ if (j >= i) {
+ double tempr = ar[j] * scale;
+ double tempi = ai[j] * scale;
+ ar[j] = ar[i] * scale;
+ ai[j] = ai[i] * scale;
+ ar[i] = tempr;
+ ai[i] = tempi;
+ }
+ }
+
+ int mmax, stride;
+ int numerator = sign * n;
+ for (mmax = 1, stride = 2 * mmax; mmax < n; mmax = stride, stride = 2 * mmax) {
+ int phase = 0;
+ int phaseIncrement = numerator / (2 * mmax);
+ for (int m = 0; m < mmax; ++m) {
+ double wr = sineTable[(phase + cosineOffset) & mask];
+ double wi = sineTable[phase];
+
+ for (i = m; i < n; i += stride) {
+ j = i + mmax;
+ double tr = (wr * ar[j]) - (wi * ai[j]);
+ double ti = (wr * ai[j]) + (wi * ar[j]);
+ ar[j] = ar[i] - tr;
+ ai[j] = ai[i] - ti;
+ ar[i] += tr;
+ ai[i] += ti;
+ }
+
+ phase = (phase + phaseIncrement) & mask;
+ }
+ mmax = stride;
+ }
+ }
+
+ public static void transform(int sign, int n, float ar[], float ai[]) {
+ float scale = (float) Math.sqrt(1.0 / n);
+
+ int numBits = FourierMath.numBits(n);
+ int[] reverseTable = getReverseTable(numBits);
+ float[] sineTable = getFloatSineTable(numBits);
+ int mask = n - 1;
+ int cosineOffset = n / 4; // phase offset between cos and sin
+
+ int i, j;
+ for (i = 0; i < n; i++) {
+ j = reverseTable[i];
+ if (j >= i) {
+ float tempr = ar[j] * scale;
+ float tempi = ai[j] * scale;
+ ar[j] = ar[i] * scale;
+ ai[j] = ai[i] * scale;
+ ar[i] = tempr;
+ ai[i] = tempi;
+ }
+ }
+
+ int mmax, stride;
+ int numerator = sign * n;
+ for (mmax = 1, stride = 2 * mmax; mmax < n; mmax = stride, stride = 2 * mmax) {
+ int phase = 0;
+ int phaseIncrement = numerator / (2 * mmax);
+ for (int m = 0; m < mmax; ++m) {
+ float wr = sineTable[(phase + cosineOffset) & mask];
+ float wi = sineTable[phase];
+
+ for (i = m; i < n; i += stride) {
+ j = i + mmax;
+ float tr = (wr * ar[j]) - (wi * ai[j]);
+ float ti = (wr * ai[j]) + (wi * ar[j]);
+ ar[j] = ar[i] - tr;
+ ai[j] = ai[i] - ti;
+ ar[i] += tr;
+ ai[i] += ti;
+ }
+
+ phase = (phase + phaseIncrement) & mask;
+ }
+ mmax = stride;
+ }
+ }
+
+ /**
+ * Calculate log2(n)
+ *
+ * @param powerOf2 must be a power of two, for example 512 or 1024
+ * @return for example, 9 for an input value of 512
+ */
+ public static int numBits(int powerOf2) {
+ int i;
+ assert ((powerOf2 & (powerOf2 - 1)) == 0); // is it a power of 2?
+ for (i = -1; powerOf2 > 0; powerOf2 = powerOf2 >> 1, i++)
+ ;
+ return i;
+ }
+
+ /**
+ * Calculate an FFT in place, modifying the input arrays.
+ *
+ * @param n
+ * @param ar
+ * @param ai
+ */
+ public static void fft(int n, double ar[], double ai[]) {
+ transform(1, n, ar, ai); // TODO -1 or 1
+ }
+
+ /**
+ * Calculate an inverse FFT in place, modifying the input arrays.
+ *
+ * @param n
+ * @param ar
+ * @param ai
+ */
+ public static void ifft(int n, double ar[], double ai[]) {
+ transform(-1, n, ar, ai); // TODO -1 or 1
+ }
+}
diff --git a/src/com/softsynth/math/JustRatio.java b/src/com/softsynth/math/JustRatio.java
new file mode 100644
index 0000000..f4070b4
--- /dev/null
+++ b/src/com/softsynth/math/JustRatio.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.softsynth.math;
+
+public class JustRatio {
+ public long numerator;
+ public long denominator;
+
+ public JustRatio(long numerator, long denominator) {
+ this.numerator = numerator;
+ this.denominator = denominator;
+ }
+
+ public JustRatio(int numerator, int denominator) {
+ this.numerator = numerator;
+ this.denominator = denominator;
+ }
+
+ public double getValue() {
+ return (double) numerator / denominator;
+ }
+
+ public void invert() {
+ long temp = denominator;
+ denominator = numerator;
+ numerator = temp;
+ }
+
+ @Override
+ public String toString() {
+ return numerator + "/" + denominator;
+ }
+}
diff --git a/src/com/softsynth/math/Polynomial.java b/src/com/softsynth/math/Polynomial.java
new file mode 100644
index 0000000..8670e97
--- /dev/null
+++ b/src/com/softsynth/math/Polynomial.java
@@ -0,0 +1,253 @@
+/*
+ * Copyright 1997 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.softsynth.math;
+
+import java.util.Vector;
+
+/**
+ * Polynomial<br>
+ * Implement polynomial using Vector as coefficient holder. Element index is power of X, value at a
+ * given index is coefficient.<br>
+ * <br>
+ *
+ * @author Nick Didkovsky, (C) 1997 Phil Burk and Nick Didkovsky
+ */
+
+public class Polynomial {
+
+ private Vector terms;
+
+ class DoubleHolder {
+ double value;
+
+ public DoubleHolder(double val) {
+ value = val;
+ }
+
+ public double get() {
+ return value;
+ }
+
+ public void set(double val) {
+ value = val;
+ }
+ }
+
+ /** create a polynomial with no terms */
+ public Polynomial() {
+ terms = new Vector();
+ }
+
+ /** create a polynomial with one term of specified constant */
+ public Polynomial(double c0) {
+ this();
+ appendTerm(c0);
+ }
+
+ /** create a polynomial with two terms with specified coefficients */
+ public Polynomial(double c1, double c0) {
+ this(c0);
+ appendTerm(c1);
+ }
+
+ /** create a polynomial with specified coefficients */
+ public Polynomial(double c2, double c1, double c0) {
+ this(c1, c0);
+ appendTerm(c2);
+ }
+
+ /** create a polynomial with specified coefficients */
+ public Polynomial(double c3, double c2, double c1, double c0) {
+ this(c2, c1, c0);
+ appendTerm(c3);
+ }
+
+ /** create a polynomial with specified coefficients */
+ public Polynomial(double c4, double c3, double c2, double c1, double c0) {
+ this(c3, c2, c1, c0);
+ appendTerm(c4);
+ }
+
+ /**
+ * Append a term with specified coefficient. Power will be next available order (ie if the
+ * polynomial is of order 2, appendTerm will supply the coefficient for x^3
+ */
+ public void appendTerm(double coefficient) {
+ terms.addElement(new DoubleHolder(coefficient));
+ }
+
+ /** Set the coefficient of given term */
+ public void setTerm(double coefficient, int power) {
+ // If setting a term greater than the current order of the polynomial, pad with zero terms
+ int size = terms.size();
+ if (power >= size) {
+ for (int i = 0; i < (power - size + 1); i++) {
+ appendTerm(0);
+ }
+ }
+ ((DoubleHolder) terms.elementAt(power)).set(coefficient);
+ }
+
+ /**
+ * Add the coefficient of given term to the specified coefficient. ex. addTerm(3, 1) add 3x to a
+ * polynomial, addTerm(4, 3) adds 4x^3
+ */
+ public void addTerm(double coefficient, int power) {
+ setTerm(coefficient + get(power), power);
+ }
+
+ /** @return coefficient of nth term (first term=0) */
+ public double get(int power) {
+ if (power >= terms.size())
+ return 0.0;
+ else
+ return ((DoubleHolder) terms.elementAt(power)).get();
+ }
+
+ /** @ return number of terms in this polynomial */
+ public int size() {
+ return terms.size();
+ }
+
+ /**
+ * Add two polynomials together
+ *
+ * @return new Polynomial that is the sum of p1 and p2
+ */
+ public static Polynomial plus(Polynomial p1, Polynomial p2) {
+ Polynomial sum = new Polynomial();
+ for (int i = 0; i < Math.max(p1.size(), p2.size()); i++) {
+ sum.appendTerm(p1.get(i) + p2.get(i));
+ }
+ return sum;
+ }
+
+ /**
+ * Subtract polynomial from another. (First arg - Second arg)
+ *
+ * @return new Polynomial p1 - p2
+ */
+ public static Polynomial minus(Polynomial p1, Polynomial p2) {
+ Polynomial sum = new Polynomial();
+ for (int i = 0; i < Math.max(p1.size(), p2.size()); i++) {
+ sum.appendTerm(p1.get(i) - p2.get(i));
+ }
+ return sum;
+ }
+
+ /**
+ * Multiply two Polynomials
+ *
+ * @return new Polynomial that is the product p1 * p2
+ */
+
+ public static Polynomial mult(Polynomial p1, Polynomial p2) {
+ Polynomial product = new Polynomial();
+ for (int i = 0; i < p1.size(); i++) {
+ for (int j = 0; j < p2.size(); j++) {
+ product.addTerm(p1.get(i) * p2.get(j), i + j);
+ }
+ }
+ return product;
+ }
+
+ /**
+ * Multiply a Polynomial by a scaler
+ *
+ * @return new Polynomial that is the product p1 * p2
+ */
+
+ public static Polynomial mult(double scaler, Polynomial p1) {
+ Polynomial product = new Polynomial();
+ for (int i = 0; i < p1.size(); i++) {
+ product.appendTerm(p1.get(i) * scaler);
+ }
+ return product;
+ }
+
+ /** Evaluate this polynomial for x */
+ public double evaluate(double x) {
+ double result = 0.0;
+ for (int i = 0; i < terms.size(); i++) {
+ result += get(i) * Math.pow(x, i);
+ }
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ String s = "";
+ if (size() == 0)
+ s = "empty polynomial";
+ boolean somethingPrinted = false;
+ for (int i = size() - 1; i >= 0; i--) {
+ if (get(i) != 0.0) {
+ if (somethingPrinted)
+ s += " + ";
+ String coeff = "";
+ // if (get(i) == (int)(get(i)))
+ // coeff = (int)(get(i)) + "";
+ if ((get(i) != 1.0) || (i == 0))
+ coeff += get(i);
+ if (i == 0)
+ s += coeff;
+ else {
+ String power = "";
+ if (i != 1)
+ power = "^" + i;
+ s += coeff + "x" + power;
+ }
+ somethingPrinted = true;
+ }
+ }
+ return s;
+ }
+
+ public static void main(String args[]) {
+ Polynomial p1 = new Polynomial();
+ System.out.println("p1=" + p1);
+ Polynomial p2 = new Polynomial(3);
+ System.out.println("p2=" + p2);
+ Polynomial p3 = new Polynomial(2, 3);
+ System.out.println("p3=" + p3);
+ Polynomial p4 = new Polynomial(1, 2, 3);
+ System.out.println("p4=" + p4);
+ System.out.println("p4*5=" + Polynomial.mult(5.0, p4));
+
+ System.out.println(p4.evaluate(10));
+
+ System.out.println(Polynomial.plus(p4, p1));
+ System.out.println(Polynomial.minus(p4, p3));
+ p4.setTerm(12.2, 5);
+ System.out.println(p4);
+ p4.addTerm(0.8, 5);
+ System.out.println(p4);
+ p4.addTerm(0.8, 7);
+ System.out.println(p4);
+ System.out.println(Polynomial.mult(p3, p2));
+ System.out.println(Polynomial.mult(p3, p3));
+ System.out.println(Polynomial.mult(p2, p2));
+
+ Polynomial t2 = new Polynomial(2, 0, -1); // 2x^2-1, Chebyshev Polynomial of order 2
+ Polynomial t3 = new Polynomial(4, 0, -3, 0); // 4x^3-3x, Chebyshev Polynomial of order 3
+ // Calculate Chebyshev Polynomial of order 4 from relation Tk+1(x) = 2xTk(x) - Tk-1(x)
+ Polynomial t4 = Polynomial.minus(Polynomial.mult(t3, (new Polynomial(2, 0))), t2);
+ System.out.println(t2 + "\n" + t3 + "\n" + t4);
+ // com.softsynth.jmsl.util
+
+ }
+}
diff --git a/src/com/softsynth/math/PolynomialTableData.java b/src/com/softsynth/math/PolynomialTableData.java
new file mode 100644
index 0000000..676c77c
--- /dev/null
+++ b/src/com/softsynth/math/PolynomialTableData.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 1997 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.softsynth.math;
+
+/**
+ * PolynomialTableData<br>
+ * Provides an array of double[] containing data generated by a polynomial.<br>
+ * This is typically used with ChebyshevPolynomial. Input to Polynomial is -1..1, output is -1..1.
+ *
+ * @author Nick Didkovsky (C) 1997 Phil Burk and Nick Didkovsky
+ * @see ChebyshevPolynomial
+ * @see Polynomial
+ */
+
+public class PolynomialTableData {
+
+ double[] data;
+ Polynomial polynomial;
+
+ /**
+ * Constructor which fills double[numFrames] with Polynomial data -1..1<br>
+ * Note that any Polynomial can plug in here, just make sure output is -1..1 when input ranges
+ * from -1..1
+ */
+ public PolynomialTableData(Polynomial polynomial, int numFrames) {
+ data = new double[numFrames];
+ this.polynomial = polynomial;
+ buildData();
+ }
+
+ public double[] getData() {
+ return data;
+ }
+
+ void buildData() {
+ double xInterval = 2.0 / (data.length - 1); // FIXED, added "- 1"
+ double x;
+ for (int i = 0; i < data.length; i++) {
+ x = i * xInterval - 1.0;
+ data[i] = polynomial.evaluate(x);
+ // System.out.println("x = " + x + ", p(x) = " + data[i] );
+ }
+
+ }
+
+ public static void main(String args[]) {
+ PolynomialTableData chebData = new PolynomialTableData(ChebyshevPolynomial.T(2), 8);
+ }
+
+}
diff --git a/src/com/softsynth/math/PrimeFactors.java b/src/com/softsynth/math/PrimeFactors.java
new file mode 100644
index 0000000..5607951
--- /dev/null
+++ b/src/com/softsynth/math/PrimeFactors.java
@@ -0,0 +1,244 @@
+/*
+ * Copyright 2011 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.softsynth.math;
+
+import java.util.ArrayList;
+
+/**
+ * Tool for factoring primes and prime ratios. This class contains a static array of primes
+ * generated using the Sieve of Eratosthenes.
+ *
+ * @author Phil Burk (C) 2011 Mobileer Inc
+ */
+public class PrimeFactors {
+ private static final int SIEVE_SIZE = 1000;
+ private static int[] primes;
+ private final int[] factors;
+
+ static {
+ // Use Sieve of Eratosthenes to fill Prime table
+ boolean[] sieve = new boolean[SIEVE_SIZE];
+ ArrayList<Integer> primeList = new ArrayList<Integer>();
+ int i = 2;
+ while (i < (SIEVE_SIZE / 2)) {
+ if (!sieve[i]) {
+ primeList.add(i);
+ int multiple = 2 * i;
+ while (multiple < SIEVE_SIZE) {
+ sieve[multiple] = true;
+ multiple += i;
+ }
+ }
+ i += 1;
+ }
+ primes = primeListToArray(primeList);
+ }
+
+ private static int[] primeListToArray(ArrayList<Integer> primeList) {
+ int[] primes = new int[primeList.size()];
+ for (int i = 0; i < primes.length; i++) {
+ primes[i] = primeList.get(i);
+ }
+ return primes;
+ }
+
+ public PrimeFactors(int[] factors) {
+ this.factors = factors;
+ }
+
+ public PrimeFactors(int numerator, int denominator) {
+ int[] topFactors = factor(numerator);
+ int[] bottomFactors = factor(denominator);
+ factors = subtract(topFactors, bottomFactors);
+ }
+
+ public PrimeFactors subtract(PrimeFactors pf) {
+ return new PrimeFactors(subtract(factors, pf.factors));
+ }
+
+ public PrimeFactors add(PrimeFactors pf) {
+ return new PrimeFactors(add(factors, pf.factors));
+ }
+
+ public static int[] subtract(int[] factorsA, int[] factorsB) {
+ int max;
+ int min;
+ if (factorsA.length > factorsB.length) {
+ max = factorsA.length;
+ min = factorsB.length;
+ } else {
+
+ min = factorsA.length;
+ max = factorsB.length;
+ }
+ ArrayList<Integer> primeList = new ArrayList<Integer>();
+ int i;
+ for (i = 0; i < min; i++) {
+ primeList.add(factorsA[i] - factorsB[i]);
+ }
+ if (factorsA.length > factorsB.length) {
+ for (; i < max; i++) {
+ primeList.add(factorsA[i]);
+ }
+ } else {
+ for (; i < max; i++) {
+ primeList.add(0 - factorsB[i]);
+ }
+ }
+ trimPrimeList(primeList);
+ return primeListToArray(primeList);
+ }
+
+ public static int[] add(int[] factorsA, int[] factorsB) {
+ int max;
+ int min;
+ if (factorsA.length > factorsB.length) {
+ max = factorsA.length;
+ min = factorsB.length;
+ } else {
+ min = factorsA.length;
+ max = factorsB.length;
+ }
+ ArrayList<Integer> primeList = new ArrayList<Integer>();
+ int i;
+ for (i = 0; i < min; i++) {
+ primeList.add(factorsA[i] + factorsB[i]);
+ }
+ if (factorsA.length > factorsB.length) {
+ for (; i < max; i++) {
+ primeList.add(factorsA[i]);
+ }
+ } else if (factorsB.length > factorsA.length) {
+ for (; i < max; i++) {
+ primeList.add(factorsB[i]);
+ }
+ }
+ trimPrimeList(primeList);
+ return primeListToArray(primeList);
+ }
+
+ private static void trimPrimeList(ArrayList<Integer> primeList) {
+ int i;
+ // trim zero factors off end.
+ for (i = primeList.size() - 1; i >= 0; i--) {
+ if (primeList.get(i) == 0) {
+ primeList.remove(i);
+ } else {
+ break;
+ }
+ }
+ }
+
+ public static int[] factor(int n) {
+ ArrayList<Integer> primeList = new ArrayList<Integer>();
+ int i = 0;
+ int p = primes[i];
+ int exponent = 0;
+ while (n > 1) {
+ // does the prime number divide evenly into n?
+ int d = n / p;
+ int m = d * p;
+ if (m == n) {
+ n = d;
+ exponent += 1;
+ } else {
+ primeList.add(exponent);
+ exponent = 0;
+ i += 1;
+ p = primes[i];
+ }
+ }
+ if (exponent > 0) {
+ primeList.add(exponent);
+ }
+ return primeListToArray(primeList);
+ }
+
+ /**
+ * Get prime from table.
+ *
+ * @warning Do not exceed getPrimeCount()-1.
+ * @param i
+ * @return Nth prime number, the 0th prime is 2
+ */
+ public static int getPrime(int i) {
+ return primes[i];
+ }
+
+ /**
+ * @return the number of primes stored in the table
+ */
+ public static int getPrimeCount() {
+ return primes.length;
+ }
+
+ public JustRatio getJustRatio() {
+ long n = 1;
+ long d = 1;
+ for (int i = 0; i < factors.length; i++) {
+ int exponent = factors[i];
+ int p = primes[i];
+ if (exponent > 0) {
+ for (int k = 0; k < exponent; k++) {
+ n = n * p;
+ }
+ } else if (exponent < 0) {
+ exponent = 0 - exponent;
+ for (int k = 0; k < exponent; k++) {
+ d = d * p;
+ }
+ }
+ }
+ return new JustRatio(n, d);
+ }
+
+ public int[] getFactors() {
+ return factors.clone();
+ }
+
+ @Override
+ public String toString() {
+ StringBuffer buffer = new StringBuffer();
+ printFactors(buffer, 1);
+ buffer.append("/");
+ printFactors(buffer, -1);
+ return buffer.toString();
+ }
+
+ private void printFactors(StringBuffer buffer, int sign) {
+ boolean gotSome = false;
+ for (int i = 0; i < factors.length; i++) {
+ int pf = factors[i] * sign;
+ if (pf > 0) {
+ if (gotSome)
+ buffer.append('*');
+ int prime = primes[i];
+ if (pf == 1) {
+ buffer.append("" + prime);
+ } else if (pf == 2) {
+ buffer.append(prime + "*" + prime);
+ } else if (pf > 2) {
+ buffer.append("(" + prime + "^" + pf + ")");
+ }
+ gotSome = true;
+ }
+ }
+ if (!gotSome) {
+ buffer.append("1");
+ }
+ }
+}
diff --git a/src/com/softsynth/shared/time/ScheduledCommand.java b/src/com/softsynth/shared/time/ScheduledCommand.java
new file mode 100644
index 0000000..5b600a7
--- /dev/null
+++ b/src/com/softsynth/shared/time/ScheduledCommand.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.softsynth.shared.time;
+
+public interface ScheduledCommand {
+ public void run();
+}
diff --git a/src/com/softsynth/shared/time/ScheduledQueue.java b/src/com/softsynth/shared/time/ScheduledQueue.java
new file mode 100644
index 0000000..d074487
--- /dev/null
+++ b/src/com/softsynth/shared/time/ScheduledQueue.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.softsynth.shared.time;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+/**
+ * @author Phil Burk, (C) 2009 Mobileer Inc
+ */
+public class ScheduledQueue<T> {
+ private SortedMap<TimeStamp, List<T>> timeNodes;
+
+ public ScheduledQueue() {
+ timeNodes = new TreeMap<TimeStamp, List<T>>();
+ }
+
+ public boolean isEmpty() {
+ return timeNodes.isEmpty();
+ }
+
+ public synchronized void add(TimeStamp time, T obj) {
+ List<T> timeList = timeNodes.get(time);
+ if (timeList == null) {
+ timeList = new LinkedList<T>();
+ timeNodes.put(time, timeList);
+ }
+ timeList.add(obj);
+ }
+
+ public synchronized List<T> removeNextList(TimeStamp time) {
+ List<T> timeList = null;
+ if (!timeNodes.isEmpty()) {
+ TimeStamp lowestTime = timeNodes.firstKey();
+ // Is the lowest time before or equal to the specified time.
+ if (lowestTime.compareTo(time) <= 0) {
+ timeList = timeNodes.remove(lowestTime);
+ }
+ }
+ return timeList;
+ }
+
+ public synchronized Object removeNext(TimeStamp time) {
+ Object next = null;
+ if (!timeNodes.isEmpty()) {
+ TimeStamp lowestTime = timeNodes.firstKey();
+ // Is the lowest time before or equal to the specified time.
+ if (lowestTime.compareTo(time) <= 0) {
+ List<T> timeList = timeNodes.get(lowestTime);
+ if (timeList != null) {
+ next = timeList.remove(0);
+ if (timeList.isEmpty()) {
+ timeNodes.remove(lowestTime);
+ }
+ }
+ }
+ }
+ return next;
+ }
+
+ public TimeStamp getNextTime() {
+ return timeNodes.firstKey();
+ }
+
+}
diff --git a/src/com/softsynth/shared/time/TimeStamp.java b/src/com/softsynth/shared/time/TimeStamp.java
new file mode 100644
index 0000000..fd054d2
--- /dev/null
+++ b/src/com/softsynth/shared/time/TimeStamp.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.softsynth.shared.time;
+
+/**
+ * @author Phil Burk, (C) 2009 Mobileer Inc
+ */
+public class TimeStamp implements Comparable<TimeStamp> {
+ private final double time;
+
+ public TimeStamp(double time) {
+ this.time = time;
+ }
+
+ public double getTime() {
+ return time;
+ }
+
+ /**
+ * @return -1 if (this < t2), 0 if equal, or +1
+ */
+ @Override
+ public int compareTo(TimeStamp t2) {
+ if (time < t2.time)
+ return -1;
+ else if (time == t2.time)
+ return 0;
+ else
+ return 1;
+ }
+
+ /**
+ * Create a new TimeStamp at a relative offset in seconds.
+ *
+ * @param delta
+ * @return earlier or later TimeStamp
+ */
+ public TimeStamp makeRelative(double delta) {
+ return new TimeStamp(time + delta);
+ }
+
+}
diff --git a/src/com/softsynth/util/AlertBox.java b/src/com/softsynth/util/AlertBox.java
new file mode 100644
index 0000000..11891aa
--- /dev/null
+++ b/src/com/softsynth/util/AlertBox.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2000 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.softsynth.util;
+
+import java.awt.BorderLayout;
+import java.awt.Button;
+import java.awt.Component;
+import java.awt.Dialog;
+import java.awt.Frame;
+import java.awt.Panel;
+import java.awt.TextArea;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+/**
+ * AlertBox class to display error messages.
+ *
+ * @author (C) 2000 Phil Burk, SoftSynth.com
+ */
+
+public class AlertBox extends Dialog {
+ TextArea textArea;
+ Button okButton;
+ Button abortButton;
+
+ public AlertBox(Frame frame, String title) {
+ super(frame, title, true);
+ setLayout(new BorderLayout());
+ setSize(620, 200);
+ textArea = new TextArea("", 6, 80, TextArea.SCROLLBARS_BOTH);
+ add("Center", textArea);
+
+ Panel panel = new Panel();
+ add("South", panel);
+
+ panel.add(okButton = new Button("Acknowledge"));
+ okButton.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ hide();
+ }
+ });
+
+ panel.add(abortButton = new Button("Abort"));
+ abortButton.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ System.exit(0);
+ }
+ });
+
+ validate();
+ }
+
+ /**
+ * Search up the component hierarchy to find the first Frame containing the component.
+ */
+ public static Frame getFrame(Component c) {
+ do {
+ if (c instanceof Frame)
+ return (Frame) c;
+ } while ((c = c.getParent()) != null);
+ return null;
+ }
+
+ public void showError(String msg) {
+ textArea.setText(msg);
+ show();
+ }
+}
diff --git a/src/com/softsynth/util/FileSearch.java b/src/com/softsynth/util/FileSearch.java
new file mode 100644
index 0000000..f691728
--- /dev/null
+++ b/src/com/softsynth/util/FileSearch.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright 1999 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.softsynth.util;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.util.Enumeration;
+import java.util.Vector;
+
+class WildcardFilenameFilter implements FilenameFilter {
+ String pattern;
+
+ public WildcardFilenameFilter(String pattern) {
+ this.pattern = pattern;
+ }
+
+ /* abc*frog*xyz =?= abctreefroglegxyz */
+ static boolean wildMatch(String pattern, String candidate) {
+ int starPos = pattern.indexOf('*');
+ // System.out.println("------pattern = " + pattern + ", candidate = " + candidate +
+ // ", starPos = " + starPos );
+ if (starPos < 0)
+ return (pattern.equalsIgnoreCase(candidate));
+ else if (starPos > 0) {
+ // System.out.println("------ beginning, pattern = " + pattern + ", candidate = " +
+ // candidate );
+ if (pattern.regionMatches(true, 0, candidate, 0, starPos)) {
+ /* Recursively check remainder of string. */
+ return wildMatch(pattern.substring(starPos), candidate.substring(starPos));
+ }
+ return false;
+ } else {
+ /* Compare remainder of string. */
+ // System.out.println("------ remainder, pattern = " + pattern + ", candidate = " +
+ // candidate );
+ if (pattern.regionMatches(true, 1, candidate, 0, candidate.length()))
+ return true;
+
+ for (int i = 0; i < candidate.length(); i++) {
+ if (wildMatch(pattern.substring(1), candidate.substring(i)))
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean accept(File dir, String name) {
+ return wildMatch(pattern, name);
+ }
+}
+
+/**
+ * Wildcard search for files in directory.
+ *
+ * @author Phil Burk (C) 1999 SoftSynth.com
+ */
+
+public class FileSearch {
+ /**
+ * Expand vector of filenames that may contain wildcards into a new vector of filenames without
+ * wildcards.
+ */
+ public static Vector expandFilenames(Vector filenames) {
+ Vector expandedFilenames = new Vector();
+ Enumeration e = filenames.elements();
+ while (e.hasMoreElements()) {
+ expandFilename((String) e.nextElement(), expandedFilenames);
+ }
+ return expandedFilenames;
+ }
+
+ /**
+ * Expand filename that may contain wildcards and add to a vector of filenames without
+ * wildcards.
+ */
+ public static void expandFilename(String filename, Vector expandedFilenames) {
+ int lastSepPos = filename.lastIndexOf(File.separatorChar);
+
+ // System.out.println("expandFilename: separator = " + File.separatorChar + " , index = " +
+ // lastSepPos );
+
+ int starPos = filename.indexOf('*');
+ if (starPos < 0) {
+ expandedFilenames.addElement(filename);
+ } else if (starPos < lastSepPos) {
+ TextOutput.error("Wildcard * not allowed in directory names! " + filename);
+ } else {
+ String parent;
+ String wildFile;
+ if (lastSepPos < 0) {
+ parent = ".";
+ wildFile = filename;
+ } else {
+ parent = filename.substring(0, lastSepPos);
+ wildFile = filename.substring(lastSepPos + 1);
+ }
+
+ File dir = new File(parent);
+ // System.out.println("expandFilename: parent = " + parent + " , dir = " + dir );
+ if (!dir.exists()) {
+ TextOutput.error("Invalid directory = " + parent);
+ }
+
+ String[] files = dir.list(new WildcardFilenameFilter(wildFile));
+ if (files == null)
+ return;
+ for (int i = 0; i < files.length; i++) {
+ expandedFilenames.addElement(parent + File.separatorChar + files[i]);
+ }
+ }
+ }
+
+ /**
+ * Extract file name from full pathname. For example, if pathname is "/usr/data/report.txt" then
+ * the output will be "report.txt".
+ */
+ public static String removeParent(String pathName) {
+ int lastSepPos = pathName.lastIndexOf(File.separatorChar);
+ if (lastSepPos < 0) {
+ return pathName;
+ } else {
+ return pathName.substring(lastSepPos + 1);
+ }
+ }
+
+ /**
+ * Remove any dot extension from file name. For example, if pathname is "/usr/data/report.txt"
+ * then the output will be "/usr/data/report".
+ */
+ public static String removeExtension(String pathName) {
+ int lastDotPos = pathName.lastIndexOf('.');
+ if (lastDotPos > 0) {
+ return pathName.substring(0, lastDotPos);
+ } else {
+ return pathName;
+ }
+ }
+
+ public static void main(String args[]) {
+ System.out.println("FileSearch - by Phil Burk");
+ boolean result;
+ result = WildcardFilenameFilter.wildMatch("abc*frog*xyz", "abctreefroglegxyz");
+ System.out.println("result = " + result);
+ result = WildcardFilenameFilter.wildMatch("abc*frog*xyz", "abctreefrxglegxyz");
+ System.out.println("result = " + result);
+
+ test();
+ }
+
+ static void test() {
+ Vector expandedFilenames = new Vector();
+ expandFilename("../data/ov*.mid", expandedFilenames);
+ Enumeration e = expandedFilenames.elements();
+ while (e.hasMoreElements()) {
+ String name = (String) e.nextElement();
+ System.out.println("file = " + name);
+ }
+ }
+
+ /********************************************************
+ * Create directory if it doesn't exist.
+ */
+ public static void createDirectoryIfNeeded(String directoryName) throws SecurityException {
+ createDirectoryIfNeeded(new File(directoryName));
+ }
+
+ /********************************************************
+ * Create directory with the given name if it doesn't exist.
+ */
+ public static void createDirectoryIfNeeded(File dir) throws SecurityException {
+ if (!dir.isDirectory()) {
+ if (!dir.mkdirs()) {
+ TextOutput.error("Could not make output directory " + dir.getAbsolutePath());
+ }
+ }
+ }
+
+}
diff --git a/src/com/softsynth/util/IndentingWriter.java b/src/com/softsynth/util/IndentingWriter.java
new file mode 100644
index 0000000..bb9ed99
--- /dev/null
+++ b/src/com/softsynth/util/IndentingWriter.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2000 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.softsynth.util;
+
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.io.Writer;
+
+/**
+ * Write to a file with indentation at the beginning of a line. One advantage of using a PrintWriter
+ * is that it automatically handles line terminators properly on different hosts.
+ *
+ * @author Phil Burk, (C) 2000 SoftSynth.com All Rights Reserved
+ */
+
+public class IndentingWriter extends PrintWriter {
+ int spacesPerIndentation = 4;
+ int indentation = 0;
+ int position = 0;
+
+ public IndentingWriter(OutputStream stream) {
+ super(stream, true);
+ }
+
+ public IndentingWriter(Writer outputStreamWriter) {
+ super(outputStreamWriter, true);
+ }
+
+ public void setIndentation(int level) {
+ indentation = level;
+ }
+
+ public int getIndentation() {
+ return indentation;
+ }
+
+ /**
+ * Increase level of indentation by one.
+ */
+ public void indent() {
+ indentation++;
+ }
+
+ /**
+ * Decrease level of indentation by one. Don't let level go below zero.
+ */
+ public void undent() {
+ indentation--;
+ if (indentation < 0)
+ indentation = 0;
+ }
+
+ /**
+ * Print string. If at left margin, add spaces for current level of indentation.
+ */
+ @Override
+ public void print(String s) {
+ if (position == 0) {
+ int numSpaces = indentation * spacesPerIndentation;
+ for (int i = 0; i < numSpaces; i++)
+ print(' ');
+ position += numSpaces;
+ }
+ super.print(s);
+ // System.out.print(s);
+ position += s.length();
+ }
+
+ @Override
+ public void println() {
+ super.println();
+ position = 0;
+ }
+
+ @Override
+ public void println(String s) {
+ print(s);
+ println();
+ }
+}
diff --git a/src/com/softsynth/util/InsetPanel.java b/src/com/softsynth/util/InsetPanel.java
new file mode 100644
index 0000000..d1b2794
--- /dev/null
+++ b/src/com/softsynth/util/InsetPanel.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 1997 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.softsynth.util;
+
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Insets;
+import java.awt.Panel;
+
+/**
+ * Panel with insets that can be used to border a centered component.
+ *
+ * @author (C) 1997 Phil Burk, SoftSynth.com
+ */
+public class InsetPanel extends Panel {
+ int border; /* Pixels in border. */
+ static final int default_border = 6;
+
+ public InsetPanel(Component borderMe) {
+ this(borderMe, Color.blue, default_border);
+ }
+
+ public InsetPanel(Component borderMe, Color borderColor) {
+ this(borderMe, borderColor, default_border);
+ }
+
+ public InsetPanel(Component borderMe, int border) {
+ this(borderMe, Color.blue, border);
+ }
+
+ /**
+ * @param borderMe component to be centerred in this panel. Typically another Panel.
+ * @param borderColor color to paint the border.
+ * @param border width in pixels of border.
+ */
+ public InsetPanel(Component borderMe, Color borderColor, int border) {
+ this.border = border;
+ setLayout(new BorderLayout());
+ /* Force a background for the component so the border shows up. */
+ if (borderMe != null) {
+ add("Center", borderMe);
+ if (borderMe.getBackground() == null)
+ borderMe.setBackground(Color.white);
+ }
+ if (borderColor != null)
+ setBackground(borderColor);
+ }
+
+ @Override
+ public Insets insets() {
+ return new Insets(border, border, border, border);
+ }
+
+ /** @param border width in pixels of border. */
+ public void setBorder(int borderWidth) {
+ border = borderWidth;
+ repaint();
+ }
+
+ public int getBorder() {
+ return border;
+ }
+}
diff --git a/src/com/softsynth/util/Logger.java b/src/com/softsynth/util/Logger.java
new file mode 100644
index 0000000..03b2ec6
--- /dev/null
+++ b/src/com/softsynth/util/Logger.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright 1999 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.softsynth.util;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+/**
+ * Logger logs output to a file if enabled.
+ *
+ * @author Phil Burk (C) 1999 SoftSynth.com
+ */
+public class Logger {
+ FileOutputStream fileStream = null;
+ BufferedOutputStream stream = null;
+ int column = 0;
+ boolean enabled;
+ String lineTerminator;
+
+ public Logger() {
+ lineTerminator = System.getProperty("line.separator", "\n");
+ // System.out.println("lineTerminator.length = " + lineTerminator.length() );
+ // for( int i=0; i<lineTerminator.length(); i++ )
+ // {
+ // System.out.println("lineTerminator[" + i + "] = " + ((int)lineTerminator.charAt(i)) );
+ // }
+ }
+
+ public void setEnable(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public boolean getEnable() {
+ return enabled;
+ }
+
+ public void log(String msg) {
+ if (!enabled)
+ return;
+
+ if (stream == null)
+ System.out.print(msg);
+ else {
+ try {
+ stream.write(msg.getBytes());
+ } catch (IOException e) {
+ System.err.println("Log File: " + e);
+ }
+ }
+ column += msg.length();
+ }
+
+ public void logln(String msg) {
+ log(msg);
+ logln();
+ }
+
+ public void logln() {
+ log(lineTerminator);
+ column = 0;
+ }
+
+ /**
+ * Output spaces if needed to position output at desired column. Nothing will be output if
+ * already past that column.
+ */
+ public void advanceToColumn(int toColumn) {
+ if (!enabled)
+ return;
+
+ try {
+ for (; column < toColumn; column++) {
+ stream.write(' ');
+ }
+ } catch (IOException e) {
+ System.err.println("Log File: " + e);
+ }
+ }
+
+ /**
+ * Start a new line if there is already text on current line.
+ */
+ public void newLine() {
+ if (column > 0)
+ logln("");
+ }
+
+ /**
+ * Open a log file.
+ */
+ public void open(String logFileName) throws IOException, SecurityException {
+ open(new File(logFileName));
+ }
+
+ /**
+ * Open a log file by name.
+ */
+ public void open(File logFile) throws IOException, SecurityException {
+ // Close any existing log file.
+ close();
+ // Open a new log file.
+ fileStream = new FileOutputStream(logFile);
+ // Buffer it so it isn't awfully slow.
+ stream = new BufferedOutputStream(fileStream);
+ column = 0;
+ }
+
+ public void close() throws IOException {
+ if (stream != null) {
+ stream.flush();
+ stream.close();
+ }
+ if (fileStream != null)
+ fileStream.close();
+ stream = null;
+ }
+}
diff --git a/src/com/softsynth/util/NumericOutput.java b/src/com/softsynth/util/NumericOutput.java
new file mode 100644
index 0000000..92acbdf
--- /dev/null
+++ b/src/com/softsynth/util/NumericOutput.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright 1999 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.softsynth.util;
+
+/**
+ * Formatted numeric output. Convert integers and floats to strings based on field widths and
+ * desired decimal places.
+ *
+ * @author Phil Burk (C) 1999 SoftSynth.com
+ */
+
+public class NumericOutput {
+ static char digitToChar(int digit) {
+ if (digit > 9) {
+ return (char) ('A' + digit - 10);
+ } else {
+ return (char) ('0' + digit);
+ }
+ }
+
+ public static String integerToString(int n, int width, boolean leadingZeros) {
+ return integerToString(n, width, leadingZeros, 10);
+ }
+
+ public static String integerToString(int n, int width) {
+ return integerToString(n, width, false, 10);
+ }
+
+ public static String integerToString(int n, int width, boolean leadingZeros, int radix) {
+ if (width > 32)
+ width = 32;
+ StringBuffer buf = new StringBuffer();
+ long ln = n;
+ boolean ifNeg = false;
+ // only do sign if decimal
+ if (radix != 10) {
+ // System.out.println("MASK before : ln = " + ln );
+ ln = ln & 0x00000000FFFFFFFFL;
+ // System.out.println("MASK after : ln = " + ln );
+ } else if (ln < 0) {
+ ifNeg = true;
+ ln = -ln;
+ }
+ if (ln == 0) {
+ buf.append('0');
+ } else {
+ // System.out.println(" ln = " + ln );
+ while (ln > 0) {
+ int rem = (int) (ln % radix);
+ buf.append(digitToChar(rem));
+ ln = ln / radix;
+ }
+ }
+ if (leadingZeros) {
+ int pl = width;
+ if (ifNeg)
+ pl -= 1;
+ for (int i = buf.length(); i < pl; i++)
+ buf.append('0');
+ }
+ if (ifNeg)
+ buf.append('-');
+ // leading spaces
+ for (int i = buf.length(); i < width; i++)
+ buf.append(' ');
+ // reverse buffer to put characters in correct order
+ buf.reverse();
+
+ return buf.toString();
+ }
+
+ /**
+ * Convert double to string.
+ *
+ * @param width = minimum width of formatted string
+ * @param places = number of digits displayed after decimal point
+ */
+ public static String doubleToString(double value, int width, int places) {
+ return doubleToString(value, width, places, false);
+ }
+
+ /**
+ * Convert double to string.
+ *
+ * @param width = minimum width of formatted string
+ * @param places = number of digits displayed after decimal point
+ */
+ public static String doubleToString(double value, int width, int places, boolean leadingZeros) {
+ if (width > 32)
+ width = 32;
+ if (places > 16)
+ places = 16;
+
+ boolean ifNeg = false;
+ if (value < 0.0) {
+ ifNeg = true;
+ value = -value;
+ }
+ // round at relevant decimal place
+ value += 0.5 * Math.pow(10.0, 0 - places);
+ int ival = (int) Math.floor(value);
+ // get portion after decimal point as an integer
+ int fval = (int) ((value - Math.floor(value)) * Math.pow(10.0, places));
+ String result = "";
+
+ result += integerToString(ival, 0, false, 10);
+ result += ".";
+ result += integerToString(fval, places, true, 10);
+
+ if (leadingZeros) {
+ // prepend leading zeros and {-}
+ int zw = width;
+ if (ifNeg)
+ zw -= 1;
+ while (result.length() < zw)
+ result = "0" + result;
+ if (ifNeg)
+ result = "-" + result;
+ } else {
+ // prepend {-} and leading spaces
+ if (ifNeg)
+ result = "-" + result;
+ while (result.length() < width)
+ result = " " + result;
+ }
+ return result;
+ }
+
+ static void testInteger(int n) {
+ System.out.println("Test " + n + ", 0x" + Integer.toHexString(n) + ", %"
+ + Integer.toBinaryString(n));
+ System.out.println(" +,8,t,10 = " + integerToString(n, 8, true, 10));
+ System.out.println(" +,8,f,10 = " + integerToString(n, 8, false, 10));
+ System.out.println(" -,8,t,10 = " + integerToString(-n, 8, true, 10));
+ System.out.println(" -,8,f,10 = " + integerToString(-n, 8, false, 10));
+ System.out.println(" +,8,t,16 = " + integerToString(n, 8, true, 16));
+ System.out.println(" +,8,f,16 = " + integerToString(n, 8, false, 16));
+ System.out.println(" -,8,t,16 = " + integerToString(-n, 8, true, 16));
+ System.out.println(" -,8,f,16 = " + integerToString(-n, 8, false, 16));
+ System.out.println(" +,8,t, 2 = " + integerToString(n, 8, true, 2));
+ System.out.println(" +,8,f, 2 = " + integerToString(n, 8, false, 2));
+ }
+
+ static void testDouble(double value) {
+ System.out.println("Test " + value);
+ System.out.println(" +,5,1 = " + doubleToString(value, 5, 1));
+ System.out.println(" -,5,1 = " + doubleToString(-value, 5, 1));
+
+ System.out.println(" +,14,3 = " + doubleToString(value, 14, 3));
+ System.out.println(" -,14,3 = " + doubleToString(-value, 14, 3));
+
+ System.out.println(" +,6,2,true = " + doubleToString(value, 6, 2, true));
+ System.out.println(" -,6,2,true = " + doubleToString(-value, 6, 2, true));
+ }
+
+ public static void main(String argv[]) {
+ System.out.println("Test NumericOutput");
+ testInteger(0);
+ testInteger(1);
+ testInteger(16);
+ testInteger(23456);
+ testInteger(0x23456);
+ testInteger(0x89ABC);
+ testDouble(0.0);
+ testDouble(0.0678);
+ testDouble(0.1234567);
+ testDouble(1.234567);
+ testDouble(12.34567);
+ testDouble(123.4567);
+ testDouble(1234.5678);
+
+ }
+}
diff --git a/src/com/softsynth/util/RandomOutputStream.java b/src/com/softsynth/util/RandomOutputStream.java
new file mode 100644
index 0000000..d4c182c
--- /dev/null
+++ b/src/com/softsynth/util/RandomOutputStream.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.softsynth.util;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.RandomAccessFile;
+
+/**
+ * An OutputStream wrapper for a RandomAccessFile Needed by routines that output to an OutputStream.
+ * <p>
+ * Note that if you are overwriting a RandomAccessFile then you should clear it before you start.
+ * This will prevent having the remainder of a longer file stuck at the end of a short file. In Java
+ * 1.2 you can call setLength(0).
+ */
+public class RandomOutputStream extends OutputStream {
+ RandomAccessFile randomFile;
+
+ public RandomOutputStream(RandomAccessFile randomFile) {
+ this.randomFile = randomFile;
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ randomFile.write(b);
+ }
+
+ @Override
+ public void write(byte[] b) throws IOException {
+ randomFile.write(b);
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException {
+ randomFile.write(b, off, len);
+ }
+
+ public void seek(long position) throws IOException {
+ randomFile.seek(position);
+ }
+
+ public long getFilePointer() throws IOException {
+ return randomFile.getFilePointer();
+ }
+
+}
diff --git a/src/com/softsynth/util/Semaphore.java b/src/com/softsynth/util/Semaphore.java
new file mode 100644
index 0000000..879d4b6
--- /dev/null
+++ b/src/com/softsynth/util/Semaphore.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.softsynth.util;
+
+/**
+ * Atomic sepahore.
+ */
+public class Semaphore {
+ boolean inUse = false;
+
+ public synchronized boolean request() {
+ boolean result = false;
+ if (!inUse) {
+ result = inUse = true;
+ }
+ return result;
+ }
+
+ public synchronized void free() {
+ inUse = false;
+ }
+}
diff --git a/src/com/softsynth/util/TextOutput.java b/src/com/softsynth/util/TextOutput.java
new file mode 100644
index 0000000..7f34965
--- /dev/null
+++ b/src/com/softsynth/util/TextOutput.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright 1999 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.softsynth.util;
+
+import java.awt.BorderLayout;
+import java.awt.Button;
+import java.awt.Font;
+import java.awt.Frame;
+import java.awt.TextArea;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+
+/**
+ * TextOutput sends text to System.out and/or a TextArea.
+ *
+ * @author Phil Burk (C) 1999 SoftSynth.com
+ */
+public class TextOutput extends Frame {
+ private static final long serialVersionUID = 1L;
+ /** Maximum number of characters allowed in TextArea. */
+ public int maxChars = (8 * 1024);
+ int numChars = 0;
+ static TextOutput staticTextOutput;
+ static Logger logger;
+ boolean isTextAreaEnabled = true;
+ boolean isSystemOutEnabled = true;
+ TextArea textArea;
+ Button buttonClear;
+ String lineTerminator = System.getProperty("line.separator", "\n");
+
+ public TextOutput() {
+ super("Text Output");
+ setSize(620, 400);
+ setLayout(new BorderLayout());
+ textArea = new TextArea("", 30, 120, TextArea.SCROLLBARS_BOTH);
+ textArea.setEditable(false);
+ textArea.setFont(Font.getFont("Monospaced-18")); // FIXME - why doesn't this work???
+ add("Center", textArea);
+ add("South", buttonClear = new Button("Clear"));
+ buttonClear.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ textArea.setText("");
+ numChars = 0;
+ }
+ });
+
+ /* If user tries to close window, hide dialog. */
+ addWindowListener(new WindowAdapter() {
+ @Override
+ public void windowClosing(WindowEvent e) {
+ hide();
+ }
+ });
+
+ }
+
+ public void setTextAreaEnable(boolean enabled) {
+ isTextAreaEnabled = enabled;
+ }
+
+ public void setSystemOutEnable(boolean enabled) {
+ isSystemOutEnabled = enabled;
+ }
+
+ /**
+ * Append text to the display area. If the TextArea gets full, then remove 1/4 of the text.
+ */
+ private void append(String text) {
+ int len = text.length();
+ if ((numChars + len) > maxChars) {
+ int numKill = maxChars / 4;
+ /* Delete 1/4 the buffer. */
+ textArea.replaceRange("", 0, numKill);
+ numChars -= numKill;
+ }
+ textArea.append(text);
+ numChars += len;
+ }
+
+ public void log(String msg) {
+ if (isSystemOutEnabled)
+ System.out.print(msg);
+ if (isTextAreaEnabled)
+ append(msg);
+ }
+
+ public void logln(String msg) {
+ log(msg);
+ logln();
+ }
+
+ /* Just output a new line. */
+ public void logln() {
+ if (isSystemOutEnabled)
+ System.out.println();
+ if (isTextAreaEnabled)
+ append(lineTerminator);
+ }
+
+ /**
+ * Set a Logger to be used for directing a copy of the output to a log file.
+ */
+ public static void setLogger(Logger lgr) {
+ logger = lgr;
+ }
+
+ public static Logger getLogger() {
+ return logger;
+ }
+
+ public static void print(String msg) {
+ if (staticTextOutput != null)
+ staticTextOutput.log(msg);
+ else
+ System.out.print(msg);
+ if (logger != null)
+ logger.log(msg);
+ }
+
+ public static void println(String msg) {
+ if (staticTextOutput != null)
+ staticTextOutput.logln(msg);
+ else
+ System.out.println(msg);
+ if (logger != null)
+ logger.logln(msg);
+ }
+
+ public static void println() {
+ if (staticTextOutput != null)
+ staticTextOutput.logln();
+ else
+ System.out.println();
+ if (logger != null)
+ logger.logln();
+ }
+
+ public static void error(String msg) {
+ println("ERROR: " + msg);
+ throw new RuntimeException(msg);
+ }
+
+ public static void setStaticLocation(int x, int y) {
+ getStaticInstance().setLocation(x, y);
+ }
+
+ // TODO - why can't this be called from Wire?
+ public static TextOutput getStaticInstance() {
+ if (staticTextOutput == null) {
+ staticTextOutput = new TextOutput();
+ staticTextOutput.setLocation(30, 0);
+ }
+ return staticTextOutput;
+ }
+
+ public static void open() {
+ getStaticInstance().setVisible(true);
+ }
+
+ public static void close() {
+ if (staticTextOutput != null)
+ staticTextOutput.setVisible(false);
+ }
+
+ public static void bringToFront() {
+ if (staticTextOutput != null)
+ staticTextOutput.toFront();
+ }
+}
diff --git a/src/com/softsynth/util/XMLListener.java b/src/com/softsynth/util/XMLListener.java
new file mode 100644
index 0000000..94f9ec7
--- /dev/null
+++ b/src/com/softsynth/util/XMLListener.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 1997 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.softsynth.util;
+
+/**
+ * Listener for parsing an XML stream.
+ *
+ * @author (C) 1997 Phil Burk
+ * @see XMLReader
+ * @see XMLPrinter
+ */
+
+public interface XMLListener {
+ /** Handles the start of an element. The flag ifEmpty if there is no content or endTag. */
+ void beginElement(String tag, java.util.Hashtable attributes, boolean ifEmpty);
+
+ /** Handles the content of an element. */
+ void foundContent(String content);
+
+ /** Handles the end of an element. */
+ void endElement(String tag);
+}
diff --git a/src/com/softsynth/util/XMLPrinter.java b/src/com/softsynth/util/XMLPrinter.java
new file mode 100644
index 0000000..2670b39
--- /dev/null
+++ b/src/com/softsynth/util/XMLPrinter.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 1997 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.softsynth.util;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Enumeration;
+import java.util.Hashtable;
+
+/**
+ * Pretty print an XML file.
+ * Indent each nested element.
+ *
+ * @author (C) 1997 Phil Burk
+ * @see XMLReader
+ * @see XMLListener
+ */
+
+/*********************************************************************************
+ */
+public class XMLPrinter extends IndentingWriter implements XMLListener {
+
+ public XMLPrinter() {
+ this(System.out);
+ }
+
+ public XMLPrinter(OutputStream stream) {
+ super(stream);
+ }
+
+ /**
+ * Print a file passed as a command line argument.
+ */
+ public static void main(String args[]) {
+ String fileName;
+
+ fileName = (args.length > 0) ? args[0] : "xmlpatch.txt";
+ try {
+ File file = new File(fileName);
+ System.out.println("File: " + file.getAbsolutePath());
+ InputStream stream = (new FileInputStream(file));
+ com.softsynth.util.XMLReader xmlr = new XMLReader(stream);
+ xmlr.setXMLListener(new XMLPrinter());
+ xmlr.parse();
+ xmlr.close();
+ stream.close();
+ } catch (IOException e) {
+ System.out.println("Error = " + e);
+ } catch (SecurityException e) {
+ System.out.println("Error = " + e);
+ }
+ }
+
+ @Override
+ public void beginElement(String tag, Hashtable attributes, boolean ifEmpty) {
+ print("<" + tag);
+ indent();
+ Enumeration e = attributes.keys();
+ if (e.hasMoreElements())
+ println();
+ while (e.hasMoreElements()) {
+ String key = (String) e.nextElement();
+ String value = (String) attributes.get(key);
+ println(key + "=\"" + value + "\"");
+ }
+ if (ifEmpty) {
+ undent();
+ println("/>");
+ } else {
+ println(">");
+ }
+ }
+
+ @Override
+ public void foundContent(String content) {
+ if (content != null)
+ println(content);
+ }
+
+ @Override
+ public void endElement(String tag) {
+ undent();
+ println("</" + tag + ">");
+ }
+}
diff --git a/src/com/softsynth/util/XMLReader.java b/src/com/softsynth/util/XMLReader.java
new file mode 100644
index 0000000..181097c
--- /dev/null
+++ b/src/com/softsynth/util/XMLReader.java
@@ -0,0 +1,328 @@
+/*
+ * Copyright 1997 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.softsynth.util;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PushbackInputStream;
+import java.io.StreamCorruptedException;
+import java.util.Hashtable;
+
+/**
+ * Parse an XML stream using a simple State Machine XMLReader does not buffer the input stream so
+ * you may want to do that yourself using a BufferedInputStream.
+ *
+ * @author (C) 1997 Phil Burk
+ * @see XMLListener
+ * @see XMLPrinter
+ */
+
+public class XMLReader extends PushbackInputStream {
+ XMLListener listener;
+ final static int IDLE = 0;
+ final static int INTAG = 0;
+ static int depth = 0;
+
+ final static int STATE_TOP = 0;
+ final static int STATE_TAG_NAME = 1;
+ final static int STATE_TAG_FIND_ANGLE = 2;
+ final static int STATE_TAG_ATTR_NAME = 3;
+ final static int STATE_TAG_FIND_EQUAL = 4;
+ final static int STATE_TAG_FIND_QUOTE = 5;
+ final static int STATE_TAG_ATTR_VALUE = 6;
+ final static int STATE_CONTENT = 7;
+ final static int STATE_CHECK_END = 8;
+ final static int STATE_TAG_SKIP = 9;
+
+ public void setXMLListener(XMLListener listener) {
+ this.listener = listener;
+ }
+
+ public XMLReader(InputStream stream) {
+ super(stream);
+ }
+
+ /**
+ * Read a unicode character from a UTF8 stream.
+ *
+ * @throws IOException
+ */
+ private int readChar() throws IOException {
+ int c = read();
+ if (c < 0)
+ return c; // EOF
+ else if (c < 128)
+ return c; // regular ASCII char
+ // We are probably starting a multi-byte character.
+ {
+ byte[] bar = null;
+ if (c < 0xE0)
+ bar = new byte[2];
+ else if (c < 0xF0)
+ bar = new byte[3];
+ else if (c < 0xF8)
+ bar = new byte[4];
+ else if (c < 0xFC)
+ bar = new byte[5];
+ else if (c < 0xFE)
+ bar = new byte[6];
+ bar[0] = (byte) c;
+ // Gather the rest of the bytes used to encode this character.
+ for (int i = 1; i < bar.length; i++) {
+ c = read();
+ if ((c & 0xc0) != 0x80)
+ throw new IOException("invalid UTF8 continuation " + Integer.toHexString(c));
+ bar[i] = (byte) c;
+ }
+ return new String(bar, "UTF8").charAt(0);
+ }
+ }
+
+ /*****************************************************************
+ */
+ public void parse() throws IOException {
+ int i;
+ char c;
+
+ while (true) {
+ i = readChar();
+ if (i < 0)
+ break; // got end of file
+ c = (char) i;
+
+ if (c == '<') {
+ parseElement();
+ } else if (!Character.isWhitespace(c)) {
+ throw new StreamCorruptedException(
+ "Unexpected character. This doesn't look like an XML file!");
+ }
+ }
+ }
+
+ String charToString(char c) {
+ String s;
+ switch (c) {
+ case '\r':
+ s = "\\r";
+ break;
+ case '\n':
+ s = "\\n";
+ break;
+ case '\t':
+ s = "\\t";
+ break;
+ default:
+ if (Character.isWhitespace(c))
+ s = " ";
+ else
+ s = "" + c;
+ break;
+ }
+ return s;
+ }
+
+ boolean isStringWhite(String s) {
+ if (s == null)
+ return true;
+ int len = s.length();
+ for (int i = 0; i < len; i++) {
+ if (!Character.isWhitespace(s.charAt(i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /*****************************************************************
+ */
+ void parseElement() throws IOException {
+ int state = STATE_TAG_NAME;
+ int i;
+ char c;
+ String tagName = "";
+ String name = null, value = null;
+ boolean ifEmpty = false;
+ boolean endTag = false;
+ boolean done = false;
+ boolean skipWhiteSpace = true;
+ char endQuote = '"'; // may also be single quote
+ String content = null;
+ Hashtable attributes = new Hashtable();
+
+ // System.out.println("\nparseElement() ---------- " + depth++);
+ while (!done) {
+ do {
+ i = readChar();
+ if (i < 0)
+ throw new EOFException("EOF inside element!");
+ c = (char) i;
+ } while (skipWhiteSpace && Character.isWhitespace(c));
+ skipWhiteSpace = false;
+
+ // System.out.print("(" + charToString(c) + "," + state + ")" );
+
+ switch (state) {
+
+ case STATE_TAG_NAME:
+ if (Character.isWhitespace(c)) {
+ skipWhiteSpace = true;
+ state = STATE_TAG_FIND_ANGLE;
+ } else if (c == '/') // this tag has no matching end tag
+ {
+ ifEmpty = true;
+ state = STATE_TAG_FIND_ANGLE;
+ } else if (c == '>') // end of tag
+ {
+ if (endTag) {
+ listener.endElement(tagName);
+ done = true;
+ } else {
+ listener.beginElement(tagName, attributes, ifEmpty);
+ state = STATE_CONTENT;
+ }
+ } else if (c == '?') {
+ state = STATE_TAG_SKIP; // got version stuff so skip to end
+ } else if (c == '!') // FIXME - parse for "--"
+ {
+ state = STATE_TAG_SKIP; // got comment
+ } else {
+ tagName += c;
+ }
+ break;
+
+ case STATE_TAG_SKIP:
+ if (c == '>') {
+ done = true;
+ }
+ break;
+
+ case STATE_TAG_FIND_ANGLE:
+ if (c == '/') // this tag has no matching end tag
+ {
+ ifEmpty = true;
+ } else if (c == '>') {
+ if (endTag) {
+ listener.endElement(tagName);
+ done = true;
+ } else {
+ listener.beginElement(tagName, attributes, ifEmpty);
+ state = STATE_CONTENT;
+ done = ifEmpty;
+ }
+ } else {
+ state = STATE_TAG_ATTR_NAME;
+ name = "" + c;
+ }
+ break;
+
+ case STATE_TAG_ATTR_NAME:
+ if (Character.isWhitespace(c)) {
+ skipWhiteSpace = true;
+ state = STATE_TAG_FIND_EQUAL;
+ } else if (c == '=') {
+ skipWhiteSpace = true;
+ state = STATE_TAG_FIND_QUOTE;
+ } else {
+ name += c;
+ }
+ break;
+
+ case STATE_TAG_FIND_EQUAL:
+ if (c == '=') {
+ skipWhiteSpace = true;
+ state = STATE_TAG_FIND_QUOTE;
+ } else {
+ throw new StreamCorruptedException("Found " + charToString(c)
+ + ", expected =.");
+ }
+ break;
+
+ case STATE_TAG_FIND_QUOTE:
+ if (c == '"') {
+ state = STATE_TAG_ATTR_VALUE;
+ value = "";
+ endQuote = '"';
+ } else if (c == '\'') {
+ state = STATE_TAG_ATTR_VALUE;
+ value = "";
+ endQuote = '\'';
+ } else {
+ throw new StreamCorruptedException("Found " + charToString(c)
+ + ", expected '\"'.");
+ }
+ break;
+
+ case STATE_TAG_ATTR_VALUE:
+ if (c == endQuote) {
+ attributes.put(name, value);
+ // System.out.println("\ngot " + name + " = " + value );
+ skipWhiteSpace = true;
+ state = STATE_TAG_FIND_ANGLE;
+ } else {
+ value += c;
+ }
+ break;
+
+ case STATE_CONTENT:
+ if (c == '<') {
+ state = STATE_CHECK_END;
+ if (!isStringWhite(content)) {
+ String unescaped = com.softsynth.util.XMLTools.unescapeText(content);
+ listener.foundContent(unescaped);
+ }
+ content = null;
+ } else {
+ if (content == null)
+ content = "";
+ content += c;
+ }
+ break;
+
+ case STATE_CHECK_END:
+ if (c == '/') {
+ endTag = true;
+ state = STATE_TAG_NAME;
+ tagName = "";
+ } else {
+ unread(c);
+ parseElement();
+ state = STATE_CONTENT;
+ }
+ break;
+ }
+ }
+ // System.out.println("\nparseElement: returns, " + --depth );
+ }
+
+ /**
+ * Get a single attribute from the Hashtable. Use the default if not found.
+ */
+ public static int getAttribute(Hashtable attributes, String key, int defaultValue) {
+ String s = (String) attributes.get(key);
+ return (s == null) ? defaultValue : Integer.parseInt(s);
+ }
+
+ /**
+ * Get a single attribute from the Hashtable. Use the default if not found.
+ */
+ public static double getAttribute(Hashtable attributes, String key, double defaultValue) {
+ String s = (String) attributes.get(key);
+ return (s == null) ? defaultValue : Double.valueOf(s).doubleValue();
+ }
+
+}
diff --git a/src/com/softsynth/util/XMLTools.java b/src/com/softsynth/util/XMLTools.java
new file mode 100644
index 0000000..b997249
--- /dev/null
+++ b/src/com/softsynth/util/XMLTools.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2002 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.softsynth.util;
+
+/**
+ * Tools for reading and writing XML files.
+ *
+ * @author Phil Burk, (C) 2002 Mobileer Inc, PROPRIETARY and CONFIDENTIAL
+ * @see XMLListener
+ * @see XMLPrinter
+ */
+
+public class XMLTools {
+ static public String replaceCharacters(String text, int ch, String newText) {
+ // just return same string if character does not occur
+ int index = text.indexOf(ch);
+ if (index < 0)
+ return text;
+
+ StringBuffer buffer = new StringBuffer();
+ index = 0;
+ while (index < text.length()) {
+ char cs = text.charAt(index);
+ if (cs == ch) {
+ buffer.append(newText);
+ } else {
+ buffer.append(cs);
+ }
+ index++;
+ }
+ return buffer.toString();
+ }
+
+/** Convert a human readable string into an XML valid string with proper escape sequences.
+ * Character like '<' must be converted to &lt;
+ *
+ * <pre>
+ * & => &amp;
+ * < => &lt;
+ * > => &gt;
+ * " => &quot;
+ * ' => &apos;
+ * </pre>
+ */
+ public static String escapeText(String text) {
+ text = replaceCharacters(text, '&', "&amp;");
+ text = replaceCharacters(text, '<', "&lt;");
+ text = replaceCharacters(text, '>', "&gt;");
+ text = replaceCharacters(text, '"', "&quot;");
+ text = replaceCharacters(text, '\'', "&apos;");
+ return text;
+ }
+
+ public static String unescapeText(String text) {
+ int newchar;
+ // just return same string if ampersand does not occur
+ int index = text.indexOf('&');
+ if (index < 0)
+ return text;
+
+ StringBuffer buffer = new StringBuffer();
+ index = 0;
+ while (index < text.length()) {
+ char cs = text.charAt(index);
+ if (cs == '&') {
+ // find ending semicolon
+ int indexSemiColon = text.indexOf(';', index);
+ if (indexSemiColon >= 0) {
+ // figure out replacement string
+ String repStr = null;
+ String escape = text.substring(index + 1, indexSemiColon);
+ if (escape.equals("amp"))
+ repStr = "&";
+ else if (escape.equals("lt"))
+ repStr = "<";
+ else if (escape.equals("gt"))
+ repStr = ">";
+ else if (escape.equals("quot"))
+ repStr = "\"";
+ else if (escape.equals("apos"))
+ repStr = "'";
+ if (repStr != null)
+ buffer.append(repStr);
+ } else
+ break; // FIXME - throw exception?
+ index = indexSemiColon;
+ } else {
+ buffer.append(cs);
+ }
+ index++;
+ }
+ return buffer.toString();
+ }
+
+ public static void testText(String text1) {
+ String text2, text3;
+ text2 = escapeText(text1);
+ text3 = unescapeText(text2);
+ System.out.println("Convert \"" + text1 + "\"");
+ System.out.println("to \"" + text2 + "\"");
+ System.out.println("and back to \"" + text3 + "\"");
+ }
+
+ public static void main(String argv[]) {
+ testText("Is 2 < 3 or is 2 > 3 ?");
+ testText("Joe's & Fred's <<<wow>>> use quotes \"hey bob\"");
+ }
+}
diff --git a/src/com/softsynth/util/XMLWriter.java b/src/com/softsynth/util/XMLWriter.java
new file mode 100644
index 0000000..5276085
--- /dev/null
+++ b/src/com/softsynth/util/XMLWriter.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2000 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.softsynth.util;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.Writer;
+import java.util.Stack;
+
+/**********************************************************************
+ * Write XML formatted file.
+ *
+ * @author (C) 2000 Phil Burk, SoftSynth.com
+ */
+
+public class XMLWriter extends IndentingWriter {
+ Stack tagStack = new Stack();
+ boolean hasContent = false;
+
+ public XMLWriter(OutputStream stream) {
+ super(stream);
+ }
+
+ public XMLWriter(Writer outputStreamWriter) {
+ super(outputStreamWriter);
+ }
+
+ public void writeAttribute(String name, String value) {
+ print(" " + name + "=\"" + XMLTools.escapeText(value) + "\"");
+ }
+
+ public void writeAttribute(String name, int value) {
+ writeAttribute(name, Integer.toString(value));
+ }
+
+ public void writeAttribute(String name, long value) {
+ writeAttribute(name, Long.toString(value));
+ }
+
+ public void writeAttribute(String name, double value) {
+ writeAttribute(name, Double.toString(value));
+ }
+
+ public void writeAttribute(String name, boolean value) {
+ writeAttribute(name, (value ? "1" : "0"));
+ }
+
+ public void writeHeader() {
+ println("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>");
+ }
+
+ public void startTag(String name) {
+ beginTag(name);
+ }
+
+ public void beginTag(String name) {
+ if (!hasContent && (tagStack.size() > 0)) {
+ beginContent();
+ println();
+ }
+ print("<" + name);
+ tagStack.push(name);
+ hasContent = false;
+ indent();
+ }
+
+ public void endTag() {
+ undent();
+ String name = (String) tagStack.pop();
+ if (hasContent) {
+ println("</" + name + ">");
+ } else {
+ println(" />");
+ }
+ // If there are tags on the stack, then they obviously had content
+ // because we are ending a nested tag.
+ hasContent = !tagStack.isEmpty();
+ }
+
+ public void beginContent() {
+ print(">");
+ hasContent = true;
+ }
+
+ public void endContent() {
+ }
+
+ public void writeComment(String text) throws IOException {
+ if (!hasContent && (tagStack.size() > 0)) {
+ beginContent();
+ println();
+ }
+ println("<!-- " + XMLTools.escapeText(text) + "-->");
+ }
+
+ public void writeContent(String string) {
+ beginContent();
+ print(XMLTools.escapeText(string));
+ endContent();
+ }
+
+ public void writeTag(String tag, String content) {
+ beginTag(tag);
+ writeContent(content);
+ endTag();
+ }
+
+}
diff --git a/tests/com/jsyn/SynthTestSuite.java b/tests/com/jsyn/SynthTestSuite.java
new file mode 100644
index 0000000..25a89c8
--- /dev/null
+++ b/tests/com/jsyn/SynthTestSuite.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn;
+
+import junit.framework.Test;
+import junit.framework.TestSuite;
+
+import com.jsyn.data.TestShortSample;
+import com.jsyn.engine.TestDevices;
+import com.jsyn.engine.TestEngine;
+import com.jsyn.engine.TestFifo;
+import com.jsyn.engine.TestWaveFileReadWrite;
+import com.jsyn.ports.TestQueuedDataPort;
+import com.jsyn.ports.TestSet;
+import com.jsyn.unitgen.TestEnable;
+import com.jsyn.unitgen.TestEnvelopeAttackDecay;
+import com.jsyn.unitgen.TestEnvelopeDAHDSR;
+import com.jsyn.unitgen.TestFunction;
+import com.jsyn.unitgen.TestRamps;
+import com.jsyn.util.TestVoiceAllocator;
+
+/**
+ * Test new pure Java JSyn API.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class SynthTestSuite {
+
+ public static Test suite() {
+
+ TestSuite suite = new TestSuite();
+
+ suite.addTestSuite(TestEngine.class);
+ suite.addTestSuite(TestSet.class);
+ // suite.addTestSuite( TestMath.class );
+ suite.addTestSuite(TestRamps.class);
+ suite.addTestSuite(TestEnvelopeAttackDecay.class);
+ suite.addTestSuite(TestEnvelopeDAHDSR.class);
+ suite.addTestSuite(TestShortSample.class);
+ suite.addTestSuite(TestDevices.class);
+ suite.addTestSuite(TestQueuedDataPort.class);
+ suite.addTestSuite(TestFifo.class);
+ suite.addTestSuite(TestEnable.class);
+ suite.addTestSuite(TestFunction.class);
+ // suite.addTestSuite( TestSampleLoader.class );
+ suite.addTestSuite(TestWaveFileReadWrite.class);
+ suite.addTestSuite(TestVoiceAllocator.class);
+ // suite.addTestSuite(TestWrapAroundBug.class);
+
+ return suite;
+ }
+
+ /**
+ * Runs the test suite using the textual runner.
+ */
+ public static void main(String[] args) {
+ junit.textui.TestRunner.run(suite());
+ System.exit(0);
+ }
+}
diff --git a/tests/com/jsyn/benchmarks/BenchJSyn.java b/tests/com/jsyn/benchmarks/BenchJSyn.java
new file mode 100644
index 0000000..51e09a4
--- /dev/null
+++ b/tests/com/jsyn/benchmarks/BenchJSyn.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright 2013 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ *
+ */
+
+package com.jsyn.benchmarks;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import com.jsyn.unitgen.PassThrough;
+import com.jsyn.unitgen.SawtoothOscillator;
+import com.jsyn.unitgen.SawtoothOscillatorBL;
+import com.jsyn.unitgen.SawtoothOscillatorDPW;
+import com.jsyn.unitgen.UnitOscillator;
+import com.softsynth.math.FourierMath;
+
+/**
+ * @author Phil Burk (C) 2013 Mobileer Inc
+ */
+public class BenchJSyn {
+ private Synthesizer synth;
+ private long startTime;
+ private long endTime;
+ private PassThrough pass;
+
+ public void run() {
+ try {
+ // Run multiple times to see if HotSpot compiler or cache makes a difference.
+ for (int i = 0; i < 4; i++) {
+ benchmark();
+ }
+ } catch (InstantiationException e) {
+ e.printStackTrace();
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void benchmark() throws InstantiationException, IllegalAccessException,
+ InterruptedException {
+ double realTime = 10.0;
+ int count = 40;
+
+ benchFFTDouble();
+ benchFFTFloat();
+ benchmarkOscillator(SawtoothOscillator.class, count, realTime);
+ benchmarkOscillator(SawtoothOscillatorDPW.class, count, realTime);
+ benchmarkOscillator(SawtoothOscillatorBL.class, count, realTime);
+
+ }
+
+ public void benchFFTDouble() {
+ int size = 2048;
+ int bin = 5;
+ int count = 20000;
+ double[] ar = new double[size];
+ double[] ai = new double[size];
+ double[] magnitudes = new double[size];
+
+ double amplitude = 1.0;
+ addSineWave(size, bin, ar, amplitude);
+ System.out.println("Bench double FFT");
+ startTiming();
+ for (int i = 0; i < count; i++) {
+ FourierMath.transform(1, size, ar, ai);
+ }
+
+ endTiming(FourierMath.class, count, size / (2.0 * 44100));
+ FourierMath.calculateMagnitudes(ar, ai, magnitudes);
+
+ assert (magnitudes[bin - 1] < 0.001);
+ assert (magnitudes[bin] > 0.5);
+ assert (magnitudes[bin + 1] < 0.001);
+
+ }
+
+ public void benchFFTFloat() {
+ int size = 2048;
+ int bin = 5;
+ int count = 20000;
+ float[] ar = new float[size];
+ float[] ai = new float[size];
+ float[] magnitudes = new float[size];
+
+ float amplitude = 1.0f;
+ addSineWave(size, bin, ar, amplitude);
+
+ System.out.println("Bench float FFT");
+ startTiming();
+ for (int i = 0; i < count; i++) {
+ FourierMath.transform(1, size, ar, ai);
+ }
+
+ endTiming(FourierMath.class, count, size / (2.0 * 44100));
+ FourierMath.calculateMagnitudes(ar, ai, magnitudes);
+
+ assert (magnitudes[bin - 1] < 0.001);
+ assert (magnitudes[bin] > 0.5);
+ assert (magnitudes[bin + 1] < 0.001);
+
+ }
+
+ private void addSineWave(int size, int bin, double[] ar, double amplitude) {
+ double phase = 0.0;
+ double phaseIncrement = 2.0 * Math.PI * bin / size;
+ for (int i = 0; i < size; i++) {
+ ar[i] += Math.sin(phase) * amplitude;
+ // System.out.println( i + " = " + ar[i] );
+ phase += phaseIncrement;
+ }
+ }
+
+ private void addSineWave(int size, int bin, float[] ar, float amplitude) {
+ float phase = 0.0f;
+ float phaseIncrement = (float) (2.0 * Math.PI * bin / size);
+ for (int i = 0; i < size; i++) {
+ ar[i] += (float) Math.sin(phase) * amplitude;
+ // System.out.println( i + " = " + ar[i] );
+ phase += phaseIncrement;
+ }
+ }
+
+ private void stopSynth() {
+ synth.stop();
+ }
+
+ private void startSynth() {
+ synth = JSyn.createSynthesizer(); // Mac
+ // synth = JSyn.createSynthesizer( new JSynAndroidAudioDevice() ); // Android
+ synth.setRealTime(false);
+ pass = new PassThrough();
+ synth.add(pass);
+ synth.start();
+ pass.start();
+ }
+
+ private void benchmarkOscillator(Class<?> clazz, int count, double realTime)
+ throws InstantiationException, IllegalAccessException, InterruptedException {
+ startSynth();
+ for (int i = 0; i < count; i++) {
+ UnitOscillator osc = (UnitOscillator) clazz.newInstance();
+ osc.output.connect(pass.input);
+ synth.add(osc);
+ }
+ startTiming();
+ synth.sleepFor(realTime);
+ endTiming(clazz, count, realTime);
+ stopSynth();
+ }
+
+ private void endTiming(Class<?> clazz, int count, double realTime) {
+ endTime = System.nanoTime();
+ double elapsedTime = (endTime - startTime) * 1E-9;
+ double percent = 100.0 * elapsedTime / (realTime * count);
+ System.out.printf("%32s took %5.3f/%d seconds to process %5.4f of audio = %6.3f%c.\n",
+ clazz.getSimpleName(), elapsedTime, count, realTime, percent, '%');
+ }
+
+ private void startTiming() {
+ startTime = System.nanoTime();
+ }
+
+ /**
+ * @param args
+ */
+ public static void main(String[] args) {
+ new BenchJSyn().run();
+ }
+
+}
diff --git a/tests/com/jsyn/data/TestShortSample.java b/tests/com/jsyn/data/TestShortSample.java
new file mode 100644
index 0000000..3837c19
--- /dev/null
+++ b/tests/com/jsyn/data/TestShortSample.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.data;
+
+import junit.framework.TestCase;
+
+/**
+ * @author Phil Burk, (C) 2009 Mobileer Inc
+ */
+public class TestShortSample extends TestCase {
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ public void testBytes() {
+ byte[] bar = {
+ 18, -3
+ };
+ short s = (short) ((bar[0] << 8) | (bar[1] & 0xFF));
+ assertEquals("A", 0x12FD, s);
+ }
+
+ public void testReadWrite() {
+ short[] data = {
+ 123, 456, -789, 111, 20000, -32768, 32767, 0, 9876
+ };
+ ShortSample sample = new ShortSample(data.length, 1);
+ assertEquals("Sample numFrames", data.length, sample.getNumFrames());
+
+ // Write and read entire sample.
+ sample.write(data);
+ short[] buffer = new short[data.length];
+ sample.read(buffer);
+
+ for (int i = 0; i < data.length; i++) {
+ assertEquals("read = write", data[i], buffer[i]);
+ }
+
+ // Write and read part of an array.
+ short[] partial = {
+ 333, 444, 555, 666, 777
+ };
+
+ sample.write(2, partial, 1, 3);
+ sample.read(1, buffer, 1, 5);
+
+ for (int i = 0; i < data.length; i++) {
+ if ((i >= 2) && (i <= 4)) {
+ assertEquals("partial", partial[i - 1], buffer[i]);
+ } else {
+ assertEquals("read = write", data[i], buffer[i]);
+ }
+ }
+
+ }
+
+}
diff --git a/tests/com/jsyn/engine/TestAudioOutput.java b/tests/com/jsyn/engine/TestAudioOutput.java
new file mode 100644
index 0000000..a95d426
--- /dev/null
+++ b/tests/com/jsyn/engine/TestAudioOutput.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.engine;
+
+import java.io.IOException;
+
+import junit.framework.TestCase;
+
+import com.jsyn.devices.AudioDeviceManager;
+import com.jsyn.devices.AudioDeviceOutputStream;
+import com.jsyn.devices.javasound.JavaSoundAudioDevice;
+
+/**
+ * @author Phil Burk, (C) 2009 Mobileer Inc
+ */
+public class TestAudioOutput extends TestCase {
+
+ SynthesisEngine synthesisEngine;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ public void testMonoSine() throws IOException {
+ System.out.println("Test mono output.");
+ final int FRAMES_PER_BUFFER = 128;
+ final int SAMPLES_PER_FRAME = 1;
+ double[] buffer = new double[FRAMES_PER_BUFFER * SAMPLES_PER_FRAME];
+ AudioDeviceManager audioDevice = new JavaSoundAudioDevice();
+ AudioDeviceOutputStream audioOutput = audioDevice.createOutputStream(
+ audioDevice.getDefaultOutputDeviceID(), 44100, SAMPLES_PER_FRAME);
+ for (int i = 0; i < FRAMES_PER_BUFFER; i++) {
+ double angle = (i * Math.PI * 2.0) / FRAMES_PER_BUFFER;
+ buffer[i] = Math.sin(angle);
+ }
+ audioOutput.start();
+ for (int i = 0; i < 1000; i++) {
+ audioOutput.write(buffer);
+ }
+ audioOutput.stop();
+
+ }
+
+ public void testStereoSine() throws IOException {
+ System.out.println("Test stereo output.");
+ final int FRAMES_PER_BUFFER = 128;
+ final int SAMPLES_PER_FRAME = 2;
+ double[] buffer = new double[FRAMES_PER_BUFFER * SAMPLES_PER_FRAME];
+ AudioDeviceManager audioDevice = new JavaSoundAudioDevice();
+ AudioDeviceOutputStream audioOutput = audioDevice.createOutputStream(
+ audioDevice.getDefaultOutputDeviceID(), 44100, SAMPLES_PER_FRAME);
+ int bi = 0;
+ for (int i = 0; i < FRAMES_PER_BUFFER; i++) {
+ double angle = (i * Math.PI * 2.0) / FRAMES_PER_BUFFER;
+ buffer[bi++] = Math.sin(angle);
+ buffer[bi++] = Math.sin(angle);
+ }
+ audioOutput.start();
+ for (int i = 0; i < 1000; i++) {
+ audioOutput.write(buffer);
+ }
+ audioOutput.stop();
+
+ }
+
+}
diff --git a/tests/com/jsyn/engine/TestDevices.java b/tests/com/jsyn/engine/TestDevices.java
new file mode 100644
index 0000000..56f2c1f
--- /dev/null
+++ b/tests/com/jsyn/engine/TestDevices.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.engine;
+
+import junit.framework.TestCase;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import com.jsyn.devices.AudioDeviceFactory;
+import com.jsyn.devices.AudioDeviceManager;
+import com.jsyn.unitgen.LineIn;
+import com.jsyn.unitgen.LineOut;
+
+public class TestDevices extends TestCase {
+ // Test audio input and output simultaneously.
+ public void testPassThrough() {
+ Synthesizer synth;
+ LineIn lineIn;
+ LineOut lineOut;
+ // Create a context for the synthesizer.
+ synth = JSyn.createSynthesizer(AudioDeviceFactory.createAudioDeviceManager(true));
+ // Add an audio input.
+ synth.add(lineIn = new LineIn());
+ // Add an audio output.
+ synth.add(lineOut = new LineOut());
+ // Connect the input to the output.
+ lineIn.output.connect(0, lineOut.input, 0);
+ lineIn.output.connect(1, lineOut.input, 1);
+
+ // Both stereo.
+ int numInputChannels = 2;
+ int numOutputChannels = 2;
+ synth.start(44100, AudioDeviceManager.USE_DEFAULT_DEVICE, numInputChannels,
+ AudioDeviceManager.USE_DEFAULT_DEVICE, numOutputChannels);
+
+ // We only need to start the LineOut. It will pull data from the LineIn.
+ lineOut.start();
+ System.out.println("Audio passthrough started.");
+ // Sleep a while.
+ double sleepTime = 2.0;
+ try {
+ double time = synth.getCurrentTime();
+ // Sleep for a few seconds.
+ synth.sleepUntil(time + sleepTime);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ double synthTime = synth.getCurrentTime();
+ assertEquals("Time has advanced. " + synthTime, sleepTime, synthTime, 0.2);
+ // Stop everything.
+ synth.stop();
+ System.out.println("All done.");
+
+ }
+}
diff --git a/tests/com/jsyn/engine/TestEngine.java b/tests/com/jsyn/engine/TestEngine.java
new file mode 100644
index 0000000..b633bc1
--- /dev/null
+++ b/tests/com/jsyn/engine/TestEngine.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.engine;
+
+import junit.framework.TestCase;
+
+import com.jsyn.unitgen.LineOut;
+import com.jsyn.unitgen.PitchDetector;
+import com.jsyn.unitgen.SineOscillator;
+import com.jsyn.unitgen.ZeroCrossingCounter;
+
+public class TestEngine extends TestCase {
+
+ public void testInitialization() {
+ final int DEFAULT_FRAME_RATE = 44100;
+ SynthesisEngine synthesisEngine = new SynthesisEngine();
+ assertEquals("frameCount zero before starting", 0, synthesisEngine.getFrameCount());
+ assertEquals("default frameRate", DEFAULT_FRAME_RATE, synthesisEngine.getFrameRate());
+ assertEquals("default pullData", true, synthesisEngine.isPullDataEnabled());
+ }
+
+ public void checkPullData(boolean pullData) {
+ SynthesisEngine synthesisEngine = new SynthesisEngine();
+ assertEquals("default realTime", true, synthesisEngine.isRealTime());
+ synthesisEngine.setRealTime(false);
+
+ assertEquals("default pullData", true, synthesisEngine.isPullDataEnabled());
+ synthesisEngine.setPullDataEnabled(pullData);
+
+ SineOscillator sineOscillator = new SineOscillator();
+ synthesisEngine.add(sineOscillator);
+
+ LineOut lineOut = new LineOut();
+ synthesisEngine.add(lineOut);
+ sineOscillator.output.connect(0, lineOut.input, 0);
+
+ assertEquals("initial sine value", 0.0, sineOscillator.output.getValue());
+
+ synthesisEngine.start();
+ if (!pullData) {
+ sineOscillator.start();
+ }
+ // We always have to start the LineOut.
+ lineOut.start();
+ synthesisEngine.generateNextBuffer();
+ synthesisEngine.generateNextBuffer();
+
+ double value = sineOscillator.output.getValue();
+ assertTrue("sine value after generation = " + value, (value > 0.0));
+ }
+
+ public void testPullDataFalse() {
+ checkPullData(false);
+ }
+
+ public void testPullDataTrue() {
+ checkPullData(true);
+ }
+
+ public void testMixedAdding() {
+ boolean gotCaught = false;
+ SynthesisEngine synthesisEngine1 = new SynthesisEngine();
+ synthesisEngine1.setRealTime(false);
+ synthesisEngine1.setPullDataEnabled(true);
+ SynthesisEngine synthesisEngine2 = new SynthesisEngine();
+ synthesisEngine2.setRealTime(false);
+ synthesisEngine2.setPullDataEnabled(true);
+
+ // Create a sineOscillator but do not add it to the synth!
+ SineOscillator sineOscillator = new SineOscillator();
+ LineOut lineOut = new LineOut();
+
+ synthesisEngine1.add(lineOut);
+ synthesisEngine2.add(sineOscillator);
+ try {
+ sineOscillator.output.connect(0, lineOut.input, 0);
+ } catch (RuntimeException e) {
+ gotCaught = true;
+ assertTrue("informative MPE message", e.getMessage().contains("different synths"));
+ }
+
+ assertTrue("caught NPE caused by forgetting synth.add", gotCaught);
+ }
+
+ public void testNotAdding() {
+ SynthesisEngine synthesisEngine = new SynthesisEngine();
+ synthesisEngine.setRealTime(false);
+ synthesisEngine.setPullDataEnabled(true);
+
+ // Create a sineOscillator but do not add it to the synth!
+ SineOscillator sineOscillator = new SineOscillator();
+
+ LineOut lineOut = new LineOut();
+ sineOscillator.output.connect(0, lineOut.input, 0);
+ synthesisEngine.add(lineOut);
+
+ assertEquals("initial sine value", 0.0, sineOscillator.output.getValue());
+
+ synthesisEngine.start();
+ // We always have to start the LineOut.
+ lineOut.start();
+ boolean gotCaught = false;
+ try {
+ synthesisEngine.generateNextBuffer();
+ synthesisEngine.generateNextBuffer();
+ } catch (NullPointerException e) {
+ gotCaught = true;
+ assertTrue("informative MPE message", e.getMessage().contains("forgot to add"));
+ }
+
+ assertTrue("caught NPE caused by forgetting synth.add", gotCaught);
+ }
+
+ public void testMultipleStarts() throws InterruptedException {
+ SynthesisEngine synth = new SynthesisEngine();
+
+ // Create a sineOscillator but do not add it to the synth!
+ SineOscillator osc = new SineOscillator();
+ ZeroCrossingCounter counter = new ZeroCrossingCounter();
+ PitchDetector pitchDetector = new PitchDetector();
+ LineOut lineOut = new LineOut();
+ synth.add(osc);
+ synth.add(counter);
+ synth.add(lineOut);
+ synth.add(pitchDetector);
+ osc.output.connect(counter.input);
+ osc.output.connect(pitchDetector.input);
+ counter.output.connect(0, lineOut.input, 0);
+
+ assertEquals("initial count", 0, counter.getCount());
+
+ int[] rates = {
+ 32000, 48000, 44100, 22050
+ };
+ for (int rate : rates) {
+ synth.start(rate);
+ lineOut.start();
+ pitchDetector.start();
+
+ double time = synth.getCurrentTime();
+ double interval = 1.0;
+ time += interval;
+
+ long previousFrameCount = counter.getCount();
+ synth.sleepUntil(time);
+
+ double frequencyMeasured = pitchDetector.frequency.get();
+ double confidenceMeasured = pitchDetector.confidence.get();
+ double oscFreq = osc.frequency.get();
+ String msg = "freq at " + rate + " Hz";
+ System.out.println(msg);
+ assertEquals(msg, oscFreq, frequencyMeasured, oscFreq * 0.1);
+ assertEquals("pitch confidence", 0.9, confidenceMeasured, 0.1);
+
+ double expectedCount = interval * oscFreq;
+ double framesMeasured = counter.getCount() - previousFrameCount;
+ msg = "count at " + rate + " Hz";
+ System.out.println(msg);
+ assertEquals(msg, expectedCount, framesMeasured, expectedCount * 0.1);
+
+ synth.stop();
+ }
+
+ }
+
+}
diff --git a/tests/com/jsyn/engine/TestFifo.java b/tests/com/jsyn/engine/TestFifo.java
new file mode 100644
index 0000000..e504a0b
--- /dev/null
+++ b/tests/com/jsyn/engine/TestFifo.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.engine;
+
+import junit.framework.TestCase;
+
+import com.jsyn.io.AudioFifo;
+
+public class TestFifo extends TestCase {
+
+ public void testBasic() {
+ Thread watchdog = startWatchdog(600);
+
+ AudioFifo fifo = new AudioFifo();
+ fifo.setReadWaitEnabled(false);
+ fifo.allocate(8);
+ assertEquals("start empty", 0, fifo.available());
+
+ assertEquals("read back Nan when emopty", Double.NaN, fifo.read());
+
+ fifo.write(1.0);
+ assertEquals("added one value", 1, fifo.available());
+ assertEquals("read back same value", 1.0, fifo.read());
+ assertEquals("back to empty", 0, fifo.available());
+
+ for (int i = 0; i < fifo.size(); i++) {
+ assertEquals("adding data", i, fifo.available());
+ fifo.write(100.0 + i);
+ }
+ for (int i = 0; i < fifo.size(); i++) {
+ assertEquals("removing data", fifo.size() - i, fifo.available());
+ assertEquals("reading back data", 100.0 + i, fifo.read());
+ }
+ watchdog.interrupt();
+ }
+
+ /**
+ * Wrap around several times to test masking.
+ */
+ public void testWrapping() {
+
+ final int chunk = 5;
+ AudioFifo fifo = new AudioFifo();
+ fifo.allocate(8);
+ double value = 1000.0;
+ for (int i = 0; i < (fifo.size() * chunk); i++) {
+ value = checkFifoChunk(fifo, value, chunk);
+ }
+
+ }
+
+ private double checkFifoChunk(AudioFifo fifo, double value, int chunk) {
+ for (int i = 0; i < chunk; i++) {
+ assertEquals("adding data", i, fifo.available());
+ fifo.write(value + i);
+ }
+ for (int i = 0; i < chunk; i++) {
+ assertEquals("removing data", chunk - i, fifo.available());
+ assertEquals("reading back data", value + i, fifo.read());
+ }
+ return value + chunk;
+ }
+
+ public void testBadSize() {
+ boolean caught = false;
+ try {
+ AudioFifo fifo = new AudioFifo();
+ fifo.allocate(20); // not power of 2
+ assertTrue("should not get here", false);
+ } catch (IllegalArgumentException e) {
+ caught = true;
+ }
+ assertTrue("should have caught size exception", caught);
+ }
+
+ public void testSingleReadWait() {
+ final int chunk = 5;
+ final AudioFifo fifo = new AudioFifo();
+ fifo.allocate(8);
+
+ fifo.setWriteWaitEnabled(false);
+ fifo.setReadWaitEnabled(true);
+ final double value = 50.0;
+
+ // Schedule a delayed write in another thread.
+ new Thread() {
+ @Override
+ public void run() {
+ try {
+ sleep(200);
+ for (int i = 0; i < chunk; i++) {
+ fifo.write(value + i);
+ }
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ }.start();
+
+ Thread watchdog = startWatchdog(500);
+ for (int i = 0; i < chunk; i++) {
+ assertEquals("reading back data", value + i, fifo.read());
+ }
+ watchdog.interrupt();
+ }
+
+ private Thread startWatchdog(final int msec) {
+ Thread watchdog = new Thread() {
+ @Override
+ public void run() {
+ try {
+ sleep(msec);
+ assertTrue("test must still be waiting", false);
+ } catch (InterruptedException e) {
+ }
+ }
+ };
+ watchdog.start();
+ return watchdog;
+ }
+
+ public void testSingleWriteWait() {
+ final int chunk = 13;
+ final AudioFifo fifo = new AudioFifo();
+ fifo.allocate(8);
+
+ fifo.setWriteWaitEnabled(true);
+ fifo.setReadWaitEnabled(true);
+ final double value = 50.0;
+
+ // Schedule a delayed read in another thread.
+ Thread readThread = new Thread() {
+ @Override
+ public void run() {
+ try {
+ sleep(200);
+ for (int i = 0; i < chunk; i++) {
+ // System.out.println( "testSingleWriteWait: try to read" );
+ double got = fifo.read();
+ assertEquals("adding data", value + i, got);
+ // System.out.println( "testSingleWriteWait: read " + got );
+ }
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ };
+ readThread.start();
+
+ Thread watchdog = startWatchdog(500);
+ // Try to write more than will fit so we will hang.
+ for (int i = 0; i < chunk; i++) {
+ fifo.write(value + i);
+ }
+ watchdog.interrupt();
+
+ try {
+ readThread.join(200);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ assertEquals("readThread should be done.", false, readThread.isAlive());
+ }
+
+ public void testBlockReadWait() {
+ final int chunk = 50;
+ final AudioFifo fifo = new AudioFifo();
+ fifo.allocate(8);
+
+ fifo.setWriteWaitEnabled(false);
+ fifo.setReadWaitEnabled(true);
+ final double value = 300.0;
+ double[] readBuffer = new double[chunk];
+
+ // Schedule delayed writes in another thread.
+ new Thread() {
+ @Override
+ public void run() {
+ int numWritten = 0;
+ double[] writeBuffer = new double[4];
+ try {
+ while (numWritten < chunk) {
+ sleep(30);
+ for (int i = 0; i < writeBuffer.length; i++) {
+ writeBuffer[i] = value + numWritten;
+ numWritten += 1;
+ }
+
+ fifo.write(writeBuffer);
+ }
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ }.start();
+
+ Thread watchdog = startWatchdog(600);
+ fifo.read(readBuffer);
+ for (int i = 0; i < chunk; i++) {
+ assertEquals("reading back data", value + i, readBuffer[i]);
+ }
+ watchdog.interrupt();
+
+ }
+}
diff --git a/tests/com/jsyn/engine/TestWaveFileReadWrite.java b/tests/com/jsyn/engine/TestWaveFileReadWrite.java
new file mode 100644
index 0000000..ee406de
--- /dev/null
+++ b/tests/com/jsyn/engine/TestWaveFileReadWrite.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.engine;
+
+import java.io.File;
+import java.io.IOException;
+
+import javax.sound.sampled.UnsupportedAudioFileException;
+
+import junit.framework.TestCase;
+
+import com.jsyn.data.FloatSample;
+import com.jsyn.util.SampleLoader;
+import com.jsyn.util.WaveFileWriter;
+
+public class TestWaveFileReadWrite extends TestCase {
+
+ public void checkWriteReadWave(int numChannels, float[] data) throws IOException,
+ UnsupportedAudioFileException {
+ File temp = File.createTempFile("test_wave", ".wav");
+ temp.deleteOnExit();
+ System.out.println("Creating wave file " + temp);
+
+ WaveFileWriter writer = new WaveFileWriter(temp);
+ writer.setFrameRate(44100);
+ writer.setSamplesPerFrame(numChannels);
+ writer.setBitsPerSample(16);
+
+ for (int i = 0; i < data.length; i++) {
+ writer.write(data[i]);
+ }
+ writer.close();
+
+ // TODO Make sure blow up if writing after close.
+ // writer.write( 0.7 );
+
+ FloatSample sample = SampleLoader.loadFloatSample(temp);
+ assertEquals("stereo", numChannels, sample.getChannelsPerFrame());
+ assertEquals("frame rate", 44100.0, sample.getFrameRate());
+
+ for (int i = 0; i < data.length; i++) {
+ float v = data[i];
+ if (v > 1.0)
+ v = 1.0f;
+ else if (v < -1.0)
+ v = -1.0f;
+ assertEquals("sample data", v, sample.readDouble(i), 0.0001);
+ }
+
+ }
+
+ public void testRamp() throws IOException, UnsupportedAudioFileException {
+ float[] data = new float[200];
+ for (int i = 0; i < data.length; i++) {
+ data[i] = i / 1000.0f;
+ }
+
+ checkWriteReadWave(2, data);
+ }
+
+ public void testClippedSine() throws IOException, UnsupportedAudioFileException {
+ float[] data = new float[200];
+ for (int i = 0; i < data.length; i++) {
+ double phase = i * Math.PI * 2.0 / 100;
+ data[i] = (float) (1.3 * Math.sin(phase));
+ }
+
+ checkWriteReadWave(2, data);
+ }
+
+ public void testArguments() throws IOException {
+ File temp = File.createTempFile("test_wave", ".wav");
+ temp.deleteOnExit();
+ System.out.println("Creating wave file " + temp);
+
+ WaveFileWriter writer = new WaveFileWriter(temp);
+ writer.setBitsPerSample(16);
+ assertEquals("bitsPerSample", 16, writer.getBitsPerSample());
+ writer.setBitsPerSample(24);
+ assertEquals("bitsPerSample", 24, writer.getBitsPerSample());
+ boolean caughtIt = false;
+ try {
+ writer.setBitsPerSample(17);
+ assertTrue("tried setting illegal value", false);
+ } catch (IllegalArgumentException e) {
+ // e.printStackTrace();
+ caughtIt = true;
+ }
+ assertTrue("17 generated exception", caughtIt);
+ writer.close();
+ }
+
+}
diff --git a/tests/com/jsyn/examples/AudioPassThrough.java b/tests/com/jsyn/examples/AudioPassThrough.java
new file mode 100644
index 0000000..fb46992
--- /dev/null
+++ b/tests/com/jsyn/examples/AudioPassThrough.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.examples;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import com.jsyn.devices.AudioDeviceManager;
+import com.jsyn.unitgen.LineIn;
+import com.jsyn.unitgen.LineOut;
+
+/**
+ * Pass audio input to audio output.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class AudioPassThrough {
+ Synthesizer synth;
+ LineIn lineIn;
+ LineOut lineOut;
+
+ private void test() {
+ // Create a context for the synthesizer.
+ synth = JSyn.createSynthesizer();
+ // Add an audio input.
+ synth.add(lineIn = new LineIn());
+ // Add an audio output.
+ synth.add(lineOut = new LineOut());
+ // Connect the input to the output.
+ lineIn.output.connect(0, lineOut.input, 0);
+ lineIn.output.connect(1, lineOut.input, 1);
+
+ // Both stereo.
+ int numInputChannels = 2;
+ int numOutputChannels = 2;
+ synth.start(44100, AudioDeviceManager.USE_DEFAULT_DEVICE, numInputChannels,
+ AudioDeviceManager.USE_DEFAULT_DEVICE, numOutputChannels);
+
+ // We only need to start the LineOut. It will pull data from the LineIn.
+ lineOut.start();
+ System.out.println("Audio passthrough started.");
+ // Sleep a while.
+ try {
+ double time = synth.getCurrentTime();
+ // Sleep for a few seconds.
+ synth.sleepUntil(time + 8.0);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ // Stop everything.
+ synth.stop();
+ System.out.println("All done.");
+ }
+
+ public static void main(String[] args) {
+ new AudioPassThrough().test();
+ }
+}
diff --git a/tests/com/jsyn/examples/ChebyshevSong.java b/tests/com/jsyn/examples/ChebyshevSong.java
new file mode 100644
index 0000000..fffd4bb
--- /dev/null
+++ b/tests/com/jsyn/examples/ChebyshevSong.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.examples;
+
+import java.awt.BorderLayout;
+
+import javax.swing.JApplet;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import com.jsyn.instruments.WaveShapingVoice;
+import com.jsyn.scope.AudioScope;
+import com.jsyn.swing.JAppletFrame;
+import com.jsyn.unitgen.Add;
+import com.jsyn.unitgen.LineOut;
+import com.jsyn.util.PseudoRandom;
+import com.jsyn.util.VoiceAllocator;
+import com.softsynth.math.AudioMath;
+import com.softsynth.shared.time.TimeStamp;
+
+/***************************************************************
+ * Play notes using a WaveShapingVoice. Allocate the notes using a VoiceAllocator.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class ChebyshevSong extends JApplet implements Runnable {
+ private static final long serialVersionUID = -7459137388629333223L;
+ private Synthesizer synth;
+ private Add mixer;
+ private LineOut lineOut;
+ private AudioScope scope;
+ private volatile boolean go = false;
+ private PseudoRandom pseudo = new PseudoRandom();
+ private final static int MAX_VOICES = 8;
+ private final static int MAX_NOTES = 5;
+ private VoiceAllocator allocator;
+ private final static int scale[] = {
+ 0, 2, 4, 7, 9
+ }; // pentatonic scale
+
+ /* Can be run as either an application or as an applet. */
+ public static void main(String args[]) {
+ ChebyshevSong applet = new ChebyshevSong();
+ JAppletFrame frame = new JAppletFrame("ChebyshevSong", applet);
+ frame.setSize(640, 300);
+ frame.setVisible(true);
+ frame.test();
+ }
+
+ /*
+ * Setup synthesis.
+ */
+ @Override
+ public void start() {
+ setLayout(new BorderLayout());
+
+ synth = JSyn.createSynthesizer();
+
+ // Use a submix so we can show it on the scope.
+ synth.add(mixer = new Add());
+ synth.add(lineOut = new LineOut());
+
+ mixer.output.connect(0, lineOut.input, 0);
+ mixer.output.connect(0, lineOut.input, 1);
+
+ WaveShapingVoice[] voices = new WaveShapingVoice[MAX_VOICES];
+ for (int i = 0; i < MAX_VOICES; i++) {
+ WaveShapingVoice voice = new WaveShapingVoice();
+ synth.add(voice);
+ voice.usePreset(0);
+ voice.getOutput().connect(mixer.inputA);
+ voices[i] = voice;
+ }
+ allocator = new VoiceAllocator(voices);
+
+ // Start synthesizer using default stereo output at 44100 Hz.
+ synth.start();
+ lineOut.start();
+
+ // Use a scope to show the mixed output.
+ scope = new AudioScope(synth);
+ scope.addProbe(mixer.output);
+ scope.setTriggerMode(AudioScope.TriggerMode.NORMAL);
+ scope.getView().setControlsVisible(false);
+ add(BorderLayout.CENTER, scope.getView());
+ scope.start();
+
+ /* Synchronize Java display. */
+ getParent().validate();
+ getToolkit().sync();
+
+ // start thread that plays notes
+ Thread thread = new Thread(this);
+ go = true;
+ thread.start();
+
+ }
+
+ @Override
+ public void stop() {
+ // tell song thread to finish
+ go = false;
+ removeAll();
+ synth.stop();
+ }
+
+ double indexToFrequency(int index) {
+ int octave = index / scale.length;
+ int temp = index % scale.length;
+ int pitch = scale[temp] + (12 * octave);
+ return AudioMath.pitchToFrequency(pitch + 16);
+ }
+
+ private void noteOff(double time, int noteNumber) {
+ allocator.noteOff(noteNumber, new TimeStamp(time));
+ }
+
+ private void noteOn(double time, int noteNumber) {
+ double frequency = indexToFrequency(noteNumber);
+ double amplitude = 0.1;
+ TimeStamp timeStamp = new TimeStamp(time);
+ allocator.noteOn(noteNumber, frequency, amplitude, timeStamp);
+ allocator.setPort(noteNumber, "Range", 0.7, synth.createTimeStamp());
+ }
+
+ @Override
+ public void run() {
+ // always choose a new song based on time&date
+ int savedSeed = (int) System.currentTimeMillis();
+ // calculate tempo
+ double duration = 0.2;
+ // set time ahead of any system latency
+ double advanceTime = 0.5;
+ // time for next note to start
+ double nextTime = synth.getCurrentTime() + advanceTime;
+ // note is ON for half the duration
+ double onTime = duration / 2;
+ int beatIndex = 0;
+ try {
+ do {
+ // on every measure, maybe repeat previous pattern
+ if ((beatIndex & 7) == 0) {
+ if ((Math.random() < (1.0 / 2.0)))
+ pseudo.setSeed(savedSeed);
+ else if ((Math.random() < (1.0 / 2.0)))
+ savedSeed = pseudo.getSeed();
+ }
+
+ // Play a bunch of random notes in the scale.
+ int numNotes = pseudo.choose(MAX_NOTES);
+ for (int i = 0; i < numNotes; i++) {
+ int noteNumber = pseudo.choose(30);
+ noteOn(nextTime, noteNumber);
+ noteOff(nextTime + onTime, noteNumber);
+ }
+
+ nextTime += duration;
+ beatIndex += 1;
+
+ // wake up before we need to play note to cover system latency
+ synth.sleepUntil(nextTime - advanceTime);
+ } while (go);
+ } catch (InterruptedException e) {
+ System.err.println("Song exiting. " + e);
+ }
+ }
+}
diff --git a/tests/com/jsyn/examples/CircuitTester.java b/tests/com/jsyn/examples/CircuitTester.java
new file mode 100644
index 0000000..22db760
--- /dev/null
+++ b/tests/com/jsyn/examples/CircuitTester.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2012 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.examples;
+
+import java.awt.BorderLayout;
+
+import javax.swing.JApplet;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import com.jsyn.scope.AudioScope;
+import com.jsyn.swing.JAppletFrame;
+import com.jsyn.swing.SoundTweaker;
+import com.jsyn.unitgen.LineOut;
+import com.jsyn.unitgen.UnitSource;
+
+/**
+ * Listen to a circuit while tweaking it knobs. Show output in a scope.
+ *
+ * @author Phil Burk (C) 2012 Mobileer Inc
+ */
+public class CircuitTester extends JApplet {
+ private static final long serialVersionUID = -2704222221111608377L;
+ private Synthesizer synth;
+ private LineOut lineOut;
+ private SoundTweaker tweaker;
+ private UnitSource unitSource;
+ private AudioScope scope;
+
+ @Override
+ public void init() {
+ setLayout(new BorderLayout());
+
+ synth = JSyn.createSynthesizer();
+ synth.add(lineOut = new LineOut());
+
+ unitSource = createUnitSource();
+ synth.add(unitSource.getUnitGenerator());
+
+ // Connect the source to both left and right speakers.
+ unitSource.getOutput().connect(0, lineOut.input, 0);
+ unitSource.getOutput().connect(0, lineOut.input, 1);
+
+ tweaker = new SoundTweaker(synth, unitSource.getUnitGenerator().getClass().getName(),
+ unitSource);
+ add(tweaker, BorderLayout.CENTER);
+
+ // Use a scope to see the output.
+ scope = new AudioScope(synth);
+ scope.addProbe(unitSource.getOutput());
+ scope.setTriggerMode(AudioScope.TriggerMode.NORMAL);
+ scope.getView().setControlsVisible(false);
+ add(BorderLayout.SOUTH, scope.getView());
+
+ validate();
+ }
+
+ /**
+ * Override this to test your own circuits.
+ *
+ * @return
+ */
+ public UnitSource createUnitSource() {
+ // return new SampleHoldNoteBlaster();
+ // return new com.syntona.exported.FMVoice();
+ // return new SubtractiveSynthVoice();
+ return new WindCircuit();
+ }
+
+ @Override
+ public void start() {
+ // Start synthesizer using default stereo output at 44100 Hz.
+ synth.start();
+ // Start the LineOut. It will pull data from the other units.
+ lineOut.start();
+
+ scope.start();
+ }
+
+ @Override
+ public void stop() {
+ scope.stop();
+ synth.stop();
+ }
+
+ /* Can be run as either an application or as an applet. */
+ public static void main(String args[]) {
+ CircuitTester applet = new CircuitTester();
+ JAppletFrame frame = new JAppletFrame("JSyn Circuit Tester", applet);
+ frame.setSize(600, 600);
+ frame.setVisible(true);
+ frame.test();
+ }
+
+}
diff --git a/tests/com/jsyn/examples/CustomCubeUnit.java b/tests/com/jsyn/examples/CustomCubeUnit.java
new file mode 100644
index 0000000..892c30c
--- /dev/null
+++ b/tests/com/jsyn/examples/CustomCubeUnit.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.examples;
+
+import com.jsyn.unitgen.UnitFilter;
+
+/**
+ * Custom unit generator that can be used with other JSyn units. Cube the input value and write it
+ * to output port.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class CustomCubeUnit extends UnitFilter {
+
+ @Override
+ /** This is where the synthesis occurs.
+ * It is called in a high priority background thread so do not do
+ * anything crazy here like reading a file or doing network I/O.
+ * Just do fast arithmetic.
+ * <br>
+ * The start and limit allow us to do either block or single sample processing.
+ */
+ public void generate(int start, int limit) {
+ // Get signal arrays from ports.
+ double[] inputs = input.getValues();
+ double[] outputs = output.getValues();
+
+ for (int i = start; i < limit; i++) {
+ double x = inputs[i];
+ // Do the math.
+ outputs[i] = x * x * x;
+ }
+ }
+}
diff --git a/tests/com/jsyn/examples/DualOscilloscope.java b/tests/com/jsyn/examples/DualOscilloscope.java
new file mode 100644
index 0000000..d7798f4
--- /dev/null
+++ b/tests/com/jsyn/examples/DualOscilloscope.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2012 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.examples;
+
+import java.awt.BorderLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.util.ArrayList;
+
+import javax.swing.JApplet;
+import javax.swing.JComboBox;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import com.jsyn.devices.AudioDeviceFactory;
+import com.jsyn.devices.AudioDeviceManager;
+import com.jsyn.scope.AudioScope;
+import com.jsyn.swing.JAppletFrame;
+import com.jsyn.unitgen.ChannelIn;
+import com.jsyn.unitgen.PassThrough;
+
+/**
+ * Two channel oscilloscope that demonstrates the use of audio input.
+ *
+ * @author Phil Burk (C) 2012 Mobileer Inc
+ */
+public class DualOscilloscope extends JApplet {
+ private static final long serialVersionUID = -2704222221111608377L;
+ private Synthesizer synth;
+ private ChannelIn channel1;
+ private ChannelIn channel2;
+ private PassThrough pass1;
+ private PassThrough pass2;
+ private AudioScope scope;
+ private AudioDeviceManager audioManager;
+ private int defaultInputId;
+ private ArrayList<String> deviceNames = new ArrayList<String>();
+ private ArrayList<Integer> deviceMaxInputs = new ArrayList<Integer>();
+ private ArrayList<Integer> deviceIds = new ArrayList<Integer>();
+ private int defaultSelection;
+ private JComboBox deviceComboBox;
+
+ @Override
+ public void init() {
+ audioManager = AudioDeviceFactory.createAudioDeviceManager(true);
+ synth = JSyn.createSynthesizer(audioManager);
+
+ int numDevices = audioManager.getDeviceCount();
+ defaultInputId = audioManager.getDefaultInputDeviceID();
+ for (int i = 0; i < numDevices; i++) {
+ int maxInputs = audioManager.getMaxInputChannels(i);
+ if (maxInputs > 0) {
+ String deviceName = audioManager.getDeviceName(i);
+ String itemName = maxInputs + ", " + deviceName + " (#" + i + ")";
+ if (i == defaultInputId) {
+ defaultSelection = deviceNames.size();
+ itemName += " (Default)";
+ }
+ deviceNames.add(itemName);
+ deviceMaxInputs.add(maxInputs);
+ deviceIds.add(i);
+ }
+ }
+
+ synth.add(channel1 = new ChannelIn());
+ channel1.setChannelIndex(0);
+ synth.add(channel2 = new ChannelIn());
+ channel2.setChannelIndex(1);
+
+ // Use PassThrough so we can easily disconnect input channels from the scope.
+ synth.add(pass1 = new PassThrough());
+ synth.add(pass2 = new PassThrough());
+
+ setupGUI();
+ }
+
+ private void setupGUI() {
+ setLayout(new BorderLayout());
+
+ deviceComboBox = new JComboBox(deviceNames.toArray(new String[0]));
+ deviceComboBox.setSelectedIndex(defaultSelection);
+ add(deviceComboBox, BorderLayout.NORTH);
+ deviceComboBox.addActionListener(new ActionListener() {
+
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ stopAudio();
+ int itemIndex = deviceComboBox.getSelectedIndex();
+ startAudio(itemIndex);
+ }
+ });
+
+ scope = new AudioScope(synth);
+
+ scope.addProbe(pass1.output);
+ scope.addProbe(pass2.output);
+
+ scope.setTriggerMode(AudioScope.TriggerMode.AUTO);
+ scope.getView().setControlsVisible(true);
+ add(scope.getView(), BorderLayout.CENTER);
+ validate();
+ }
+
+ protected void startAudio(int itemIndex) {
+ // Both stereo.
+ int numInputChannels = deviceMaxInputs.get(itemIndex);
+ if (numInputChannels > 2)
+ numInputChannels = 2;
+ int inputDeviceIndex = deviceIds.get(itemIndex);
+ synth.start(44100, inputDeviceIndex, numInputChannels,
+ AudioDeviceManager.USE_DEFAULT_DEVICE, 0);
+
+ channel1.output.connect(pass1.input);
+ // Only connect second channel if more than one input channel.
+ if (numInputChannels > 1) {
+ channel2.output.connect(pass2.input);
+ }
+
+ // We only need to start the LineOut. It will pull data from the
+ // channels.
+ scope.start();
+ }
+
+ @Override
+ public void start() {
+ startAudio(defaultSelection);
+ }
+
+ public void stopAudio() {
+ pass1.input.disconnectAll();
+ pass2.input.disconnectAll();
+ scope.stop();
+ synth.stop();
+ }
+
+ @Override
+ public void stop() {
+ stopAudio();
+ }
+
+ /* Can be run as either an application or as an applet. */
+ public static void main(String args[]) {
+ DualOscilloscope applet = new DualOscilloscope();
+ JAppletFrame frame = new JAppletFrame("Dual Oscilloscope", applet);
+ frame.setSize(640, 400);
+ frame.setVisible(true);
+ frame.test();
+ }
+
+}
diff --git a/tests/com/jsyn/examples/EditEnvelope1.java b/tests/com/jsyn/examples/EditEnvelope1.java
new file mode 100644
index 0000000..763037b
--- /dev/null
+++ b/tests/com/jsyn/examples/EditEnvelope1.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright 1997 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * Test Envelope using Java Audio Synthesizer
+ * Trigger attack or release portion.
+ *
+ * @author (C) 1997 Phil Burk
+ */
+
+package com.jsyn.examples;
+
+import java.awt.BorderLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+import javax.swing.JApplet;
+import javax.swing.JButton;
+import javax.swing.JPanel;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import com.jsyn.data.SegmentedEnvelope;
+import com.jsyn.swing.EnvelopeEditorPanel;
+import com.jsyn.swing.EnvelopePoints;
+import com.jsyn.swing.JAppletFrame;
+import com.jsyn.unitgen.LineOut;
+import com.jsyn.unitgen.SawtoothOscillatorBL;
+import com.jsyn.unitgen.UnitOscillator;
+import com.jsyn.unitgen.VariableRateDataReader;
+import com.jsyn.unitgen.VariableRateMonoReader;
+
+public class EditEnvelope1 extends JApplet {
+ private Synthesizer synth;
+ private UnitOscillator osc;
+ private LineOut lineOut;
+ private SegmentedEnvelope envelope;
+ private VariableRateDataReader envelopePlayer;
+
+ final int MAX_FRAMES = 16;
+ JButton hitme;
+ JButton attackButton;
+ JButton releaseButton;
+ private EnvelopeEditorPanel envEditor;
+ private EnvelopePoints points;
+
+ /* Can be run as either an application or as an applet. */
+ public static void main(String args[]) {
+ EditEnvelope1 applet = new EditEnvelope1();
+ JAppletFrame frame = new JAppletFrame("Test SynthEnvelope", applet);
+ frame.setSize(440, 200);
+ frame.setVisible(true);
+ frame.test();
+ }
+
+ /*
+ * Setup synthesis.
+ */
+ @Override
+ public void start() {
+ setLayout(new BorderLayout());
+
+ // Create a context for the synthesizer.
+ synth = JSyn.createSynthesizer();
+ // Add a tone generator.
+ synth.add(osc = new SawtoothOscillatorBL());
+ // Add an envelope player.
+ synth.add(envelopePlayer = new VariableRateMonoReader());
+
+ envelope = new SegmentedEnvelope(MAX_FRAMES);
+
+ // Add an output mixer.
+ synth.add(lineOut = new LineOut());
+ envelopePlayer.output.connect(osc.amplitude);
+ // Connect the oscillator to the output.
+ osc.output.connect(0, lineOut.input, 0);
+ osc.output.connect(0, lineOut.input, 1);
+
+ // Start synthesizer using default stereo output at 44100 Hz.
+ synth.start();
+
+ JPanel bottomPanel = new JPanel();
+ bottomPanel.add(hitme = new JButton("On"));
+ hitme.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ points.updateEnvelopeIfDirty(envelope);
+ envelopePlayer.dataQueue.queueOn(envelope);
+ }
+ });
+
+ bottomPanel.add(attackButton = new JButton("Off"));
+ attackButton.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ points.updateEnvelopeIfDirty(envelope);
+ envelopePlayer.dataQueue.queueOff(envelope);
+ }
+ });
+
+ bottomPanel.add(releaseButton = new JButton("Queue"));
+ releaseButton.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ points.updateEnvelopeIfDirty(envelope);
+ envelopePlayer.dataQueue.queue(envelope);
+ }
+ });
+
+ add(bottomPanel, BorderLayout.SOUTH);
+ lineOut.start();
+
+ // Create vector of points for editor.
+ points = new EnvelopePoints();
+ points.setName(osc.amplitude.getName());
+
+ // Setup initial envelope shape.
+ points.add(0.5, 1.0);
+ points.add(0.5, 0.2);
+ points.add(0.5, 0.8);
+ points.add(0.5, 0.0);
+ points.updateEnvelope(envelope);
+
+ // Add an envelope editor to the center of the panel.
+ add("Center", envEditor = new EnvelopeEditorPanel(points, MAX_FRAMES));
+
+ /* Synchronize Java display. */
+ getParent().validate();
+ getToolkit().sync();
+ }
+
+ @Override
+ public void stop() {
+ synth.stop();
+ }
+}
diff --git a/tests/com/jsyn/examples/FFTPassthrough.java b/tests/com/jsyn/examples/FFTPassthrough.java
new file mode 100644
index 0000000..1a1a7c5
--- /dev/null
+++ b/tests/com/jsyn/examples/FFTPassthrough.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.examples;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import com.jsyn.unitgen.LineOut;
+import com.jsyn.unitgen.PassThrough;
+import com.jsyn.unitgen.SineOscillator;
+import com.jsyn.unitgen.SpectralFFT;
+import com.jsyn.unitgen.SpectralIFFT;
+import com.jsyn.unitgen.UnitOscillator;
+
+/**
+ * Play a sine sweep through an FFT/IFFT pair.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class FFTPassthrough {
+ private Synthesizer synth;
+ private PassThrough center;
+ private UnitOscillator osc;
+ private UnitOscillator lfo;
+ private SpectralFFT fft;
+ private SpectralIFFT ifft1;
+ private LineOut lineOut;
+ private SpectralIFFT ifft2;
+
+ private void test() {
+ // Create a context for the synthesizer.
+ synth = JSyn.createSynthesizer();
+
+ // Start synthesizer using default stereo output at 44100 Hz.
+ synth.start();
+
+ // Add a tone generator.
+ synth.add(center = new PassThrough());
+ // synth.add( osc = new SawtoothOscillatorBL() );
+ synth.add(osc = new SineOscillator());
+ synth.add(lfo = new SineOscillator());
+ synth.add(fft = new SpectralFFT());
+ synth.add(ifft1 = new SpectralIFFT());
+ synth.add(ifft2 = new SpectralIFFT());
+ // Add a stereo audio output unit.
+ synth.add(lineOut = new LineOut());
+
+ // Connect the oscillator to both channels of the output.
+ center.output.connect(osc.frequency);
+ lfo.output.connect(osc.frequency);
+ osc.output.connect(fft.input);
+ fft.output.connect(ifft1.input);
+ fft.output.connect(ifft2.input);
+ ifft1.output.connect(0, lineOut.input, 0);
+ ifft2.output.connect(0, lineOut.input, 1);
+
+ // Set the frequency and amplitude for the modulated sine wave.
+ center.input.set(600.0);
+ lfo.frequency.set(0.2);
+ lfo.amplitude.set(400.0);
+ osc.amplitude.set(0.6);
+
+ // We only need to start the LineOut. It will pull data through the
+ // chain.
+ lineOut.start();
+
+ System.out.println("You should now be hearing a clean oscillator on the left channel,");
+ System.out.println("and the FFT->IFFT processed signal on the right channel.");
+
+ // Sleep while the sound is generated in the background.
+ try {
+ double time = synth.getCurrentTime();
+ // Sleep for a few seconds.
+ synth.sleepUntil(time + 20.0);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+
+ System.out.println("Stop playing. -------------------");
+ // Stop everything.
+ synth.stop();
+ }
+
+ public static void main(String[] args) {
+ new FFTPassthrough().test();
+ }
+}
diff --git a/tests/com/jsyn/examples/GoogleWaveOscillator.java b/tests/com/jsyn/examples/GoogleWaveOscillator.java
new file mode 100644
index 0000000..2f45cc4
--- /dev/null
+++ b/tests/com/jsyn/examples/GoogleWaveOscillator.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.examples;
+
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.unitgen.UnitOscillator;
+
+/**
+ * Custom unit generator to create the waveform shown on the Google home page on 2/22/12.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class GoogleWaveOscillator extends UnitOscillator {
+ public UnitInputPort variance;
+ private double phaseIncrement = 0.1;
+ private double previousY;
+ private double randomAmplitude = 0.0;
+
+ public GoogleWaveOscillator() {
+ addPort(variance = new UnitInputPort("Variance", 0.0));
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ // Get signal arrays from ports.
+ double[] freqs = frequency.getValues();
+ double[] outputs = output.getValues();
+ double currentPhase = phase.getValue();
+ double y;
+
+ for (int i = start; i < limit; i++) {
+ if (currentPhase > 0.0) {
+ double p = currentPhase;
+ y = Math.sqrt(4.0 * (p * (1.0 - p)));
+ } else {
+ double p = -currentPhase;
+ y = -Math.sqrt(4.0 * (p * (1.0 - p)));
+ }
+
+ if ((previousY * y) <= 0.0) {
+ // Calculate randomly offset phaseIncrement.
+ double v = variance.getValues()[0];
+ double range = ((Math.random() - 0.5) * 4.0 * v);
+ double scale = Math.pow(2.0, range);
+ phaseIncrement = convertFrequencyToPhaseIncrement(freqs[i]) * scale;
+
+ // Calculate random amplitude.
+ scale = 1.0 + ((Math.random() - 0.5) * 1.5 * v);
+ randomAmplitude = amplitude.getValues()[0] * scale;
+ }
+
+ outputs[i] = y * randomAmplitude;
+ previousY = y;
+
+ currentPhase = incrementWrapPhase(currentPhase, phaseIncrement);
+ }
+ phase.setValue(currentPhase);
+ }
+}
diff --git a/tests/com/jsyn/examples/HearDAHDSR.java b/tests/com/jsyn/examples/HearDAHDSR.java
new file mode 100644
index 0000000..23e6fb5
--- /dev/null
+++ b/tests/com/jsyn/examples/HearDAHDSR.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.examples;
+
+import java.awt.GridLayout;
+
+import javax.swing.BorderFactory;
+import javax.swing.JApplet;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.swing.DoubleBoundedRangeModel;
+import com.jsyn.swing.JAppletFrame;
+import com.jsyn.swing.PortModelFactory;
+import com.jsyn.swing.RotaryTextController;
+import com.jsyn.unitgen.EnvelopeDAHDSR;
+import com.jsyn.unitgen.LineOut;
+import com.jsyn.unitgen.SineOscillator;
+import com.jsyn.unitgen.SquareOscillator;
+import com.jsyn.unitgen.UnitOscillator;
+
+/**
+ * Play a tone using a JSyn oscillator. Modulate the amplitude using a DAHDSR envelope.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class HearDAHDSR extends JApplet {
+ private static final long serialVersionUID = -2704222221111608377L;
+ private Synthesizer synth;
+ private UnitOscillator osc;
+ // Use a square wave to trigger the envelope.
+ private UnitOscillator gatingOsc;
+ private EnvelopeDAHDSR dahdsr;
+ private LineOut lineOut;
+
+ @Override
+ public void init() {
+ synth = JSyn.createSynthesizer();
+
+ // Add a tone generator.
+ synth.add(osc = new SineOscillator());
+ // Add a trigger.
+ synth.add(gatingOsc = new SquareOscillator());
+ // Use an envelope to control the amplitude.
+ synth.add(dahdsr = new EnvelopeDAHDSR());
+ // Add an output mixer.
+ synth.add(lineOut = new LineOut());
+
+ gatingOsc.output.connect(dahdsr.input);
+ dahdsr.output.connect(osc.amplitude);
+ dahdsr.attack.setup(0.001, 0.01, 2.0);
+ osc.output.connect(0, lineOut.input, 0);
+ osc.output.connect(0, lineOut.input, 1);
+
+ gatingOsc.frequency.setup(0.001, 0.5, 10.0);
+ gatingOsc.frequency.setName("Rate");
+
+ osc.frequency.setup(50.0, 440.0, 2000.0);
+ osc.frequency.setName("Freq");
+
+ // Arrange the knob in a row.
+ setLayout(new GridLayout(1, 0));
+
+ setupPortKnob(osc.frequency);
+ setupPortKnob(gatingOsc.frequency);
+ setupPortKnob(dahdsr.attack);
+ setupPortKnob(dahdsr.hold);
+ setupPortKnob(dahdsr.decay);
+ setupPortKnob(dahdsr.sustain);
+ setupPortKnob(dahdsr.release);
+
+ validate();
+ }
+
+ private void setupPortKnob(UnitInputPort port) {
+
+ DoubleBoundedRangeModel model = PortModelFactory.createExponentialModel(port);
+ System.out.println("Make knob for " + port.getName() + ", model.getDV = "
+ + model.getDoubleValue() + ", model.getV = " + model.getValue() + ", port.getV = "
+ + port.get());
+ RotaryTextController knob = new RotaryTextController(model, 10);
+ knob.setBorder(BorderFactory.createTitledBorder(port.getName()));
+ knob.setTitle(port.getName());
+ add(knob);
+ }
+
+ @Override
+ public void start() {
+ // Start synthesizer using default stereo output at 44100 Hz.
+ synth.start();
+ // We only need to start the LineOut. It will pull data from the
+ // oscillator.
+ lineOut.start();
+ }
+
+ @Override
+ public void stop() {
+ synth.stop();
+ }
+
+ /* Can be run as either an application or as an applet. */
+ public static void main(String args[]) {
+ HearDAHDSR applet = new HearDAHDSR();
+ JAppletFrame frame = new JAppletFrame("Hear DAHDSR Envelope", applet);
+ frame.setSize(640, 200);
+ frame.setVisible(true);
+ frame.test();
+ }
+
+}
diff --git a/tests/com/jsyn/examples/HearMoogFilter.java b/tests/com/jsyn/examples/HearMoogFilter.java
new file mode 100644
index 0000000..dfe3bec
--- /dev/null
+++ b/tests/com/jsyn/examples/HearMoogFilter.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.examples;
+
+import java.awt.BorderLayout;
+import java.awt.GridLayout;
+import java.awt.event.ItemEvent;
+import java.awt.event.ItemListener;
+
+import javax.swing.BorderFactory;
+import javax.swing.BoxLayout;
+import javax.swing.ButtonGroup;
+import javax.swing.JApplet;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JRadioButton;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.scope.AudioScope;
+import com.jsyn.scope.AudioScopeProbe;
+import com.jsyn.swing.DoubleBoundedRangeModel;
+import com.jsyn.swing.JAppletFrame;
+import com.jsyn.swing.PortModelFactory;
+import com.jsyn.swing.RotaryTextController;
+import com.jsyn.unitgen.FilterFourPoles;
+import com.jsyn.unitgen.FilterLowPass;
+import com.jsyn.unitgen.LineOut;
+import com.jsyn.unitgen.LinearRamp;
+import com.jsyn.unitgen.PassThrough;
+import com.jsyn.unitgen.SawtoothOscillatorBL;
+import com.jsyn.unitgen.UnitOscillator;
+
+/**
+ * Play a sawtooth through a 4-pole filter.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class HearMoogFilter extends JApplet {
+ private Synthesizer synth;
+ private UnitOscillator oscillator;
+ private FilterFourPoles filterMoog;
+ private FilterLowPass filterBiquad;
+ private LinearRamp rampCutoff;
+ private PassThrough tieQ;
+ private PassThrough mixer;
+ private LineOut lineOut;
+
+ private AudioScope scope;
+ private AudioScopeProbe moogProbe;
+
+ @Override
+ public void init() {
+ synth = JSyn.createSynthesizer();
+ synth.add(oscillator = new SawtoothOscillatorBL());
+ synth.add(rampCutoff = new LinearRamp());
+ synth.add(tieQ = new PassThrough());
+ synth.add(filterMoog = new FilterFourPoles());
+ synth.add(filterBiquad = new FilterLowPass());
+ synth.add(mixer = new PassThrough());
+ synth.add(lineOut = new LineOut());
+
+ oscillator.output.connect(filterMoog.input);
+ oscillator.output.connect(filterBiquad.input);
+ rampCutoff.output.connect(filterMoog.frequency);
+ rampCutoff.output.connect(filterBiquad.frequency);
+ rampCutoff.time.set(0.050);
+ tieQ.output.connect(filterMoog.Q);
+ tieQ.output.connect(filterBiquad.Q);
+ filterMoog.output.connect(mixer.input);
+ mixer.output.connect(0, lineOut.input, 0);
+ mixer.output.connect(0, lineOut.input, 1);
+
+ filterBiquad.amplitude.set(0.1);
+ oscillator.frequency.setup(50.0, 130.0, 3000.0);
+ oscillator.amplitude.setup(0.0, 0.336, 1.0);
+ rampCutoff.input.setup(50.0, 400.0, 4000.0);
+ tieQ.input.setup(0.1, 0.7, 10.0);
+ setupGUI();
+ }
+
+ private void setupGUI() {
+ setLayout(new BorderLayout());
+
+ add(new JLabel("Sawtooth through a \"Moog\" style filter."), BorderLayout.NORTH);
+
+ JPanel rackPanel = new JPanel();
+ rackPanel.setLayout(new BoxLayout(rackPanel, BoxLayout.Y_AXIS));
+
+ JPanel buttonPanel = new JPanel();
+ ButtonGroup cbg = new ButtonGroup();
+ JRadioButton radioButton = new JRadioButton("Moog", true);
+ cbg.add(radioButton);
+ radioButton.addItemListener(new ItemListener() {
+ @Override
+ public void itemStateChanged(ItemEvent e) {
+ mixer.input.disconnectAll();
+ filterMoog.output.connect(mixer.input);
+ }
+ });
+ buttonPanel.add(radioButton);
+ radioButton = new JRadioButton("Biquad", false);
+ cbg.add(radioButton);
+ radioButton.addItemListener(new ItemListener() {
+ @Override
+ public void itemStateChanged(ItemEvent e) {
+ mixer.input.disconnectAll();
+ filterBiquad.output.connect(mixer.input);
+ }
+ });
+ buttonPanel.add(radioButton);
+
+ /*
+ * buttonPanel.add( new JLabel("Show:") ); cbg = new ButtonGroup(); radioButton = new
+ * JRadioButton( "Waveform", true ); cbg.add( radioButton ); radioButton.addItemListener(
+ * new ItemListener() { public void itemStateChanged( ItemEvent e ) { scope.setViewMode(
+ * AudioScope.ViewMode.WAVEFORM ); } } ); buttonPanel.add( radioButton ); radioButton = new
+ * JRadioButton( "Spectrum", true ); cbg.add( radioButton ); radioButton.addItemListener(
+ * new ItemListener() { public void itemStateChanged( ItemEvent e ) { scope.setViewMode(
+ * AudioScope.ViewMode.SPECTRUM ); } } ); buttonPanel.add( radioButton );
+ */
+
+ rackPanel.add(buttonPanel);
+
+ // Arrange the knobs in a row.
+ JPanel knobPanel = new JPanel();
+ knobPanel.setLayout(new GridLayout(1, 0));
+
+ knobPanel.add(setupPortKnob(oscillator.frequency, "OscFreq"));
+ knobPanel.add(setupPortKnob(oscillator.amplitude, "OscAmp"));
+
+ knobPanel.add(setupPortKnob(rampCutoff.input, "Cutoff"));
+ knobPanel.add(setupPortKnob(tieQ.input, "Q"));
+ rackPanel.add(knobPanel);
+ add(rackPanel, BorderLayout.SOUTH);
+
+ scope = new AudioScope(synth);
+ scope.addProbe(oscillator.output);
+ moogProbe = scope.addProbe(filterMoog.output);
+ scope.addProbe(filterBiquad.output);
+ scope.setTriggerMode(AudioScope.TriggerMode.NORMAL);
+ scope.getView().setControlsVisible(false);
+ add(scope.getView(), BorderLayout.CENTER);
+ scope.start();
+ validate();
+ }
+
+ private RotaryTextController setupPortKnob(UnitInputPort port, String label) {
+ DoubleBoundedRangeModel model = PortModelFactory.createExponentialModel(port);
+ RotaryTextController knob = new RotaryTextController(model, 10);
+ knob.setBorder(BorderFactory.createTitledBorder(label));
+ knob.setTitle(label);
+ return knob;
+ }
+
+ @Override
+ public void start() {
+ // Start synthesizer using default stereo output at 44100 Hz.
+ synth.start();
+ scope.start();
+ // We only need to start the LineOut. It will pull data from the
+ // oscillator.
+ lineOut.start();
+ }
+
+ @Override
+ public void stop() {
+ scope.stop();
+ synth.stop();
+ }
+
+ /* Can be run as either an application or as an applet. */
+ public static void main(String args[]) {
+ HearMoogFilter applet = new HearMoogFilter();
+ JAppletFrame frame = new JAppletFrame("Hear Moog Style Filter", applet);
+ frame.setSize(800, 600);
+ frame.setVisible(true);
+ frame.test();
+ }
+
+}
diff --git a/tests/com/jsyn/examples/HearSinePM.java b/tests/com/jsyn/examples/HearSinePM.java
new file mode 100644
index 0000000..2949605
--- /dev/null
+++ b/tests/com/jsyn/examples/HearSinePM.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.examples;
+
+import java.awt.BorderLayout;
+import java.awt.GridLayout;
+
+import javax.swing.BorderFactory;
+import javax.swing.JApplet;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.scope.AudioScope;
+import com.jsyn.swing.DoubleBoundedRangeModel;
+import com.jsyn.swing.JAppletFrame;
+import com.jsyn.swing.PortModelFactory;
+import com.jsyn.swing.RotaryTextController;
+import com.jsyn.unitgen.LineOut;
+import com.jsyn.unitgen.SineOscillator;
+import com.jsyn.unitgen.SineOscillatorPhaseModulated;
+
+/**
+ * Play a tone using a phase modulated sinewave oscillator. Phase modulation (PM) is very similar to
+ * frequency modulation (FM) but is easier to control.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class HearSinePM extends JApplet {
+ private static final long serialVersionUID = -2704222221111608377L;
+ private Synthesizer synth;
+ SineOscillatorPhaseModulated carrier;
+ SineOscillator modulator;
+ LineOut lineOut;
+ AudioScope scope;
+
+ @Override
+ public void init() {
+ synth = JSyn.createSynthesizer();
+ // Add a tone generator.
+ synth.add(modulator = new SineOscillator());
+ // Add a trigger.
+ synth.add(carrier = new SineOscillatorPhaseModulated());
+ // Add an output mixer.
+ synth.add(lineOut = new LineOut());
+
+ modulator.output.connect(carrier.modulation);
+ carrier.output.connect(0, lineOut.input, 0);
+ carrier.output.connect(0, lineOut.input, 1);
+ modulator.amplitude.setup(0.0, 1.0, 10.0);
+ carrier.amplitude.setup(0.0, 1.0, 1.0);
+ setupGUI();
+ }
+
+ private void setupGUI() {
+ setLayout(new BorderLayout());
+
+ add(new JLabel("Show Phase Modulation in an AudioScope"), BorderLayout.NORTH);
+
+ // Arrange the knob in a row.
+ JPanel knobPanel = new JPanel();
+ knobPanel.setLayout(new GridLayout(1, 0));
+
+ knobPanel.add(setupPortKnob(modulator.frequency, "MFreq"));
+ knobPanel.add(setupPortKnob(modulator.amplitude, "MAmp"));
+ knobPanel.add(setupPortKnob(carrier.frequency, "CFreq"));
+ knobPanel.add(setupPortKnob(carrier.amplitude, "CAmp"));
+ add(knobPanel, BorderLayout.SOUTH);
+
+ scope = new AudioScope(synth);
+ scope.addProbe(carrier.output);
+ scope.addProbe(modulator.output);
+ scope.setTriggerMode(AudioScope.TriggerMode.NORMAL);
+ scope.getView().setControlsVisible(true);
+ add(scope.getView(), BorderLayout.CENTER);
+ scope.start();
+ validate();
+ }
+
+ private RotaryTextController setupPortKnob(UnitInputPort port, String label) {
+ DoubleBoundedRangeModel model = PortModelFactory.createExponentialModel(port);
+ RotaryTextController knob = new RotaryTextController(model, 10);
+ knob.setBorder(BorderFactory.createTitledBorder(label));
+ knob.setTitle(label);
+ return knob;
+ }
+
+ @Override
+ public void start() {
+ // Start synthesizer using default stereo output at 44100 Hz.
+ synth.start();
+ scope.start();
+ // We only need to start the LineOut. It will pull data from the
+ // oscillator.
+ lineOut.start();
+ }
+
+ @Override
+ public void stop() {
+ scope.stop();
+ synth.stop();
+ }
+
+ /* Can be run as either an application or as an applet. */
+ public static void main(String args[]) {
+ HearSinePM applet = new HearSinePM();
+ JAppletFrame frame = new JAppletFrame("Hear Phase Modulation", applet);
+ frame.setSize(640, 400);
+ frame.setVisible(true);
+ frame.test();
+ }
+
+}
diff --git a/tests/com/jsyn/examples/HearSpectralFilter.java b/tests/com/jsyn/examples/HearSpectralFilter.java
new file mode 100644
index 0000000..93559b4
--- /dev/null
+++ b/tests/com/jsyn/examples/HearSpectralFilter.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.examples;
+
+import java.io.File;
+import java.io.IOException;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import com.jsyn.data.Spectrum;
+import com.jsyn.unitgen.LineOut;
+import com.jsyn.unitgen.PassThrough;
+import com.jsyn.unitgen.SawtoothOscillatorBL;
+import com.jsyn.unitgen.SineOscillator;
+import com.jsyn.unitgen.SpectralFilter;
+import com.jsyn.unitgen.SpectralProcessor;
+import com.jsyn.unitgen.UnitOscillator;
+import com.jsyn.unitgen.WhiteNoise;
+import com.jsyn.util.WaveRecorder;
+
+/**
+ * Play a sine sweep through an FFT/IFFT pair.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class HearSpectralFilter {
+ private Synthesizer synth;
+ private PassThrough center;
+ private UnitOscillator osc;
+ private UnitOscillator lfo;
+ private PassThrough mixer;
+ private SpectralFilter filter;
+ private LineOut lineOut;
+ private WaveRecorder recorder;
+ private final static boolean useRecorder = true;
+ private final static boolean useProcessor = true;
+ private final static int NUM_FFTS = 4;
+ private final static int SIZE_LOG_2 = 10;
+ private final static int SIZE = 1 << SIZE_LOG_2;
+ private SpectralProcessor[] processors;
+ private WhiteNoise noise;
+ private static int SAMPLE_RATE = 44100;
+
+ private static class CustomSpectralProcessor extends SpectralProcessor {
+ public CustomSpectralProcessor() {
+ super(SIZE);
+ }
+
+ @Override
+ public void processSpectrum(Spectrum inputSpectrum, Spectrum outputSpectrum) {
+ // pitchUpOctave( inputSpectrum, outputSpectrum );
+ lowPassFilter(inputSpectrum, outputSpectrum, 1500.0);
+ }
+
+ public void lowPassFilter(Spectrum inputSpectrum, Spectrum outputSpectrum, double frequency) {
+ inputSpectrum.copyTo(outputSpectrum);
+ double[] outReal = outputSpectrum.getReal();
+ double[] outImag = outputSpectrum.getImaginary();
+ // brickwall filter
+ int size = outReal.length;
+ int cutoff = (int) (frequency * size / SAMPLE_RATE);
+ int nyquist = size / 2;
+ for (int i = cutoff; i < nyquist; i++) {
+ // Bins above nyquist are mirror of ones below.
+ outReal[i] = outReal[size - i] = 0.0;
+ outImag[i] = outImag[size - i] = 0.0;
+ }
+ }
+
+ // TODO Figure out why this sounds bad.
+ public void pitchUpOctave(Spectrum inputSpectrum, Spectrum outputSpectrum) {
+ outputSpectrum.clear();
+ double[] inReal = inputSpectrum.getReal();
+ double[] inImag = inputSpectrum.getImaginary();
+ double[] outReal = outputSpectrum.getReal();
+ double[] outImag = outputSpectrum.getImaginary();
+ int size = inReal.length;
+ int nyquist = size / 2;
+ // Octave doubling by shifting the spectrum.
+ for (int i = nyquist - 2; i > 1; i--) {
+ int h = i / 2;
+ outReal[i] = inReal[h];
+ outImag[i] = inImag[h];
+ outReal[size - i] = inReal[size - h];
+ outImag[size - i] = inImag[size - h];
+ }
+ }
+ }
+
+ private void test() throws IOException {
+ // Create a context for the synthesizer.
+ synth = JSyn.createSynthesizer();
+ synth.setRealTime(true);
+
+ if (useRecorder) {
+ File waveFile = new File("temp_recording.wav");
+ // Default is stereo, 16 bits.
+ recorder = new WaveRecorder(synth, waveFile);
+ System.out.println("Writing to WAV file " + waveFile.getAbsolutePath());
+ }
+
+ if (useProcessor) {
+ processors = new SpectralProcessor[NUM_FFTS];
+ for (int i = 0; i < NUM_FFTS; i++) {
+ processors[i] = new CustomSpectralProcessor();
+ }
+ }
+
+ // Add a tone generator.
+ synth.add(center = new PassThrough());
+ synth.add(lfo = new SineOscillator());
+ synth.add(noise = new WhiteNoise());
+ synth.add(mixer = new PassThrough());
+
+ synth.add(osc = new SawtoothOscillatorBL());
+ // synth.add( osc = new SineOscillator() );
+
+ synth.add(filter = new SpectralFilter(NUM_FFTS, SIZE_LOG_2));
+ // Add a stereo audio output unit.
+ synth.add(lineOut = new LineOut());
+
+ center.output.connect(osc.frequency);
+ lfo.output.connect(osc.frequency);
+ osc.output.connect(mixer.input);
+ noise.output.connect(mixer.input);
+ mixer.output.connect(filter.input);
+ if (useProcessor) {
+ // Pass spectra through a custom processor.
+ for (int i = 0; i < NUM_FFTS; i++) {
+ filter.getSpectralOutput(i).connect(processors[i].input);
+ processors[i].output.connect(filter.getSpectralInput(i));
+ }
+ } else {
+ for (int i = 0; i < NUM_FFTS; i++) {
+ // Connect FFTs directly to IFFTs for passthrough.
+ filter.getSpectralOutput(i).connect(filter.getSpectralInput(i));
+ }
+
+ }
+ mixer.output.connect(0, lineOut.input, 0);
+ filter.output.connect(0, lineOut.input, 1);
+
+ // Set the frequency and amplitude for the modulated sine wave.
+ center.input.set(600.0);
+ lfo.frequency.set(0.2);
+ lfo.amplitude.set(400.0);
+ osc.amplitude.set(0.2);
+ noise.amplitude.set(0.2);
+
+ synth.start(SAMPLE_RATE);
+
+ if (useRecorder) {
+ mixer.output.connect(0, recorder.getInput(), 0);
+ filter.output.connect(0, recorder.getInput(), 1);
+ // When we start the recorder it will pull data from the oscillator
+ // and sweeper.
+ recorder.start();
+ }
+
+ lineOut.start();
+
+ System.out.println("You should now be hearing a clean oscillator on the left channel,");
+ System.out.println("and the FFT->IFFT processed signal on the right channel.");
+
+ // Sleep while the sound is generated in the background.
+ try {
+ double time = synth.getCurrentTime();
+ // Sleep for a few seconds.
+ synth.sleepUntil(time + 10.0);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+
+ if (recorder != null) {
+ recorder.stop();
+ recorder.close();
+ }
+
+ System.out.println("Stop playing. -------------------");
+ // Stop everything.
+ synth.stop();
+ }
+
+ public static void main(String[] args) {
+ try {
+ new HearSpectralFilter().test();
+ } catch (IOException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/tests/com/jsyn/examples/ListAudioDevices.java b/tests/com/jsyn/examples/ListAudioDevices.java
new file mode 100644
index 0000000..6c5372d
--- /dev/null
+++ b/tests/com/jsyn/examples/ListAudioDevices.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.examples;
+
+import com.jsyn.devices.AudioDeviceFactory;
+import com.jsyn.devices.AudioDeviceManager;
+
+public class ListAudioDevices {
+
+ /**
+ * @param args
+ */
+ public static void main(String[] args) {
+ AudioDeviceManager audioManager = AudioDeviceFactory.createAudioDeviceManager();
+
+ int numDevices = audioManager.getDeviceCount();
+ for (int i = 0; i < numDevices; i++) {
+ String deviceName = audioManager.getDeviceName(i);
+ int maxInputs = audioManager.getMaxInputChannels(i);
+ int maxOutputs = audioManager.getMaxInputChannels(i);
+ boolean isDefaultInput = (i == audioManager.getDefaultInputDeviceID());
+ boolean isDefaultOutput = (i == audioManager.getDefaultInputDeviceID());
+ System.out.println("#" + i + " : " + deviceName);
+ System.out.println(" max inputs : " + maxInputs
+ + (isDefaultInput ? " (default)" : ""));
+ System.out.println(" max outputs: " + maxOutputs
+ + (isDefaultOutput ? " (default)" : ""));
+ }
+
+ }
+
+}
diff --git a/tests/com/jsyn/examples/LongEcho.java b/tests/com/jsyn/examples/LongEcho.java
new file mode 100644
index 0000000..41f21f2
--- /dev/null
+++ b/tests/com/jsyn/examples/LongEcho.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.examples;
+
+import java.io.File;
+import java.io.IOException;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import com.jsyn.data.FloatSample;
+import com.jsyn.devices.AudioDeviceManager;
+import com.jsyn.unitgen.ChannelIn;
+import com.jsyn.unitgen.ChannelOut;
+import com.jsyn.unitgen.FixedRateMonoReader;
+import com.jsyn.unitgen.FixedRateMonoWriter;
+import com.jsyn.unitgen.Maximum;
+import com.jsyn.unitgen.Minimum;
+import com.jsyn.util.WaveFileWriter;
+
+/**
+ * Echo the input using a circular buffer in a sample.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class LongEcho {
+ final static int DELAY_SECONDS = 4;
+ Synthesizer synth;
+ ChannelIn channelIn;
+ ChannelOut channelOut;
+ FloatSample sample;
+ FixedRateMonoReader reader;
+ FixedRateMonoWriter writer;
+ Minimum minner;
+ Maximum maxxer;
+
+ private void test() throws IOException {
+ // Create a context for the synthesizer.
+ synth = JSyn.createSynthesizer();
+ // Add a tone generator.
+ synth.add(channelIn = new ChannelIn());
+ // Add an output mixer.
+ synth.add(channelOut = new ChannelOut());
+
+ synth.add(minner = new Minimum());
+ synth.add(maxxer = new Maximum());
+ synth.add(reader = new FixedRateMonoReader());
+ synth.add(writer = new FixedRateMonoWriter());
+
+ sample = new FloatSample(44100 * DELAY_SECONDS, 1);
+
+ maxxer.inputB.set(-0.98); // clip
+ minner.inputB.set(0.98);
+
+ // Connect the input to the output.
+ channelIn.output.connect(minner.inputA);
+ minner.output.connect(maxxer.inputA);
+ maxxer.output.connect(writer.input);
+
+ reader.output.connect(channelOut.input);
+
+ // Both stereo.
+ int numInputChannels = 2;
+ int numOutputChannels = 2;
+ synth.start(44100, AudioDeviceManager.USE_DEFAULT_DEVICE, numInputChannels,
+ AudioDeviceManager.USE_DEFAULT_DEVICE, numOutputChannels);
+
+ writer.start();
+ channelOut.start();
+
+ // For a long echo, read cursor should be just in front of the write cursor.
+ reader.dataQueue.queue(sample, 1000, sample.getNumFrames() - 1000);
+ // Loop both forever.
+ reader.dataQueue.queueLoop(sample, 0, sample.getNumFrames());
+ writer.dataQueue.queueLoop(sample, 0, sample.getNumFrames());
+ System.out.println("Start talking. You should hear an echo after " + DELAY_SECONDS
+ + " seconds.");
+ // Sleep a while.
+ try {
+ double time = synth.getCurrentTime();
+ // Sleep for a while
+ synth.sleepUntil(time + 30.0);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+
+ saveEcho(new File("saved_echo.wav"));
+ // Stop everything.
+ synth.stop();
+ }
+
+ private void saveEcho(File file) throws IOException {
+ WaveFileWriter writer = new WaveFileWriter(file);
+ writer.setFrameRate(44100);
+ writer.setSamplesPerFrame(1);
+ writer.setBitsPerSample(16);
+ float[] buffer = new float[sample.getNumFrames()];
+ sample.read(buffer);
+ for (float v : buffer) {
+ writer.write(v);
+ }
+ writer.close();
+ }
+
+ public static void main(String[] args) {
+ try {
+ new LongEcho().test();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/tests/com/jsyn/examples/MonoPassThrough.java b/tests/com/jsyn/examples/MonoPassThrough.java
new file mode 100644
index 0000000..0e81abf
--- /dev/null
+++ b/tests/com/jsyn/examples/MonoPassThrough.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.examples;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import com.jsyn.devices.AudioDeviceManager;
+import com.jsyn.unitgen.ChannelIn;
+import com.jsyn.unitgen.ChannelOut;
+
+/**
+ * Pass audio input to audio output.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class MonoPassThrough {
+ Synthesizer synth;
+ ChannelIn channelIn;
+ ChannelOut channelOut;
+
+ private void test() {
+ // Create a context for the synthesizer.
+ synth = JSyn.createSynthesizer();
+ synth.add(channelIn = new ChannelIn());
+ synth.add(channelOut = new ChannelOut());
+ // Connect the input to the output.
+ channelIn.output.connect(channelOut.input);
+
+ // Both stereo.
+ int numInputChannels = 2;
+ int numOutputChannels = 2;
+ synth.start(48000, AudioDeviceManager.USE_DEFAULT_DEVICE, numInputChannels,
+ AudioDeviceManager.USE_DEFAULT_DEVICE, numOutputChannels);
+
+ // We only need to start the ChannelOut. It will pull data from the ChannelIn.
+ channelOut.start();
+ // Sleep a while.
+ try {
+ double time = synth.getCurrentTime();
+ // Sleep for a few seconds.
+ synth.sleepUntil(time + 4.0);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ // Stop everything.
+ synth.stop();
+ }
+
+ public static void main(String[] args) {
+ new MonoPassThrough().test();
+ }
+}
diff --git a/tests/com/jsyn/examples/NotesToTone.java b/tests/com/jsyn/examples/NotesToTone.java
new file mode 100644
index 0000000..9186087
--- /dev/null
+++ b/tests/com/jsyn/examples/NotesToTone.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * If you play notes fast enough they become a tone.
+ *
+ * Play a sine wave modulated by an envelope.
+ * Speed up the envelope until it is playing at audio rate.
+ * Slow down the oscillator until it becomes an LFO amp modulator.
+ * Use a LatchZeroCrossing to stop at the end of a sine wave cycle when we are finished.
+ *
+ * @author Phil Burk, (C) 2010 Mobileer Inc
+ */
+
+package com.jsyn.examples;
+
+import java.io.File;
+import java.io.IOException;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import com.jsyn.data.SegmentedEnvelope;
+import com.jsyn.unitgen.ExponentialRamp;
+import com.jsyn.unitgen.LatchZeroCrossing;
+import com.jsyn.unitgen.LineOut;
+import com.jsyn.unitgen.SineOscillator;
+import com.jsyn.unitgen.UnitOscillator;
+import com.jsyn.unitgen.VariableRateDataReader;
+import com.jsyn.unitgen.VariableRateMonoReader;
+import com.jsyn.util.WaveRecorder;
+
+/**
+ * When notes speed up they can become a new tone. <br>
+ * Multiply an oscillator and an envelope. Speed up the envelope until it becomes a tone. Slow down
+ * the oscillator until it acts like an envelope. Write the resulting audio to a WAV file.
+ *
+ * @author Phil Burk (C) 2011 Mobileer Inc
+ */
+
+public class NotesToTone {
+ private final static double SONG_AMPLITUDE = 0.7;
+ private final static double INTRO_DURATION = 2.0;
+ private final static double OUTRO_DURATION = 2.0;
+ private final static double RAMP_DURATION = 20.0;
+ private final static double LOW_FREQUENCY = 1.0;
+ private final static double HIGH_FREQUENCY = 800.0;
+
+ private final static boolean useRecorder = true;
+ private WaveRecorder recorder;
+
+ private Synthesizer synth;
+ private ExponentialRamp envSweeper;
+ private ExponentialRamp oscSweeper;
+ private VariableRateDataReader envelopePlayer;
+ private UnitOscillator osc;
+ private LatchZeroCrossing latch;
+ private LineOut lineOut;
+ private SegmentedEnvelope envelope;
+
+ private void play() throws IOException {
+ synth = JSyn.createSynthesizer();
+ synth.setRealTime(true);
+
+ if (useRecorder) {
+ File waveFile = new File("notes_to_tone.wav");
+ // Default is stereo, 16 bits.
+ recorder = new WaveRecorder(synth, waveFile, 1);
+ System.out.println("Writing to WAV file " + waveFile.getAbsolutePath());
+ }
+
+ createUnits();
+
+ connectUnits();
+
+ setupEnvelope();
+
+ osc.amplitude.set(SONG_AMPLITUDE);
+
+ // Ramp the rate of the envelope up until it becomes an audible tone.
+ envSweeper.current.set(LOW_FREQUENCY);
+ envSweeper.input.set(LOW_FREQUENCY);
+ envSweeper.time.set(RAMP_DURATION);
+
+ // Ramp the rate of the oscillator down until it becomes an LFO.
+ oscSweeper.current.set(HIGH_FREQUENCY);
+ oscSweeper.input.set(HIGH_FREQUENCY);
+ oscSweeper.time.set(RAMP_DURATION);
+
+ // Start synthesizer using default stereo output at 44100 Hz.
+ synth.start();
+
+ // When we start the recorder it will pull data from the oscillator and
+ // sweeper.
+ if (recorder != null) {
+ recorder.start();
+ }
+
+ // We also need to start the LineOut if we want to hear it now.
+ lineOut.start();
+
+ // Get synthesizer time in seconds.
+ double timeNow = synth.getCurrentTime();
+
+ // Schedule start of ramps.
+ double songDuration = INTRO_DURATION + RAMP_DURATION + OUTRO_DURATION;
+ envSweeper.input.set(HIGH_FREQUENCY, timeNow + INTRO_DURATION);
+ oscSweeper.input.set(LOW_FREQUENCY, timeNow + INTRO_DURATION);
+
+ // Arm zero crossing latch
+ latch.gate.set(0.0, timeNow + songDuration);
+
+ // Sleep while the sound is being generated in the background thread.
+ try {
+ synth.sleepUntil(timeNow + songDuration + 2.0);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+
+ if (recorder != null) {
+ recorder.stop();
+ recorder.close();
+ }
+ // Stop everything.
+ synth.stop();
+ }
+
+ private void createUnits() {
+ // Add a tone generators.
+ synth.add(osc = new SineOscillator());
+ // Add a controller that will sweep the envelope rate up.
+ synth.add(envSweeper = new ExponentialRamp());
+ // Add a controller that will sweep the oscillator down.
+ synth.add(oscSweeper = new ExponentialRamp());
+
+ synth.add(latch = new LatchZeroCrossing());
+ // Add an output unit.
+ synth.add(lineOut = new LineOut());
+
+ // Add an envelope player.
+ synth.add(envelopePlayer = new VariableRateMonoReader());
+ }
+
+ private void connectUnits() {
+ oscSweeper.output.connect(osc.frequency);
+ osc.output.connect(latch.input);
+ // Latch when sine LFO crosses zero.
+ latch.output.connect(envelopePlayer.amplitude);
+
+ envSweeper.output.connect(envelopePlayer.rate);
+
+ // Connect the envelope player to the audio output.
+ envelopePlayer.output.connect(0, lineOut.input, 0);
+ // crossFade.output.connect( 0, lineOut.input, 1 );
+
+ if (recorder != null) {
+ envelopePlayer.output.connect(0, recorder.getInput(), 0);
+ // crossFade.output.connect( 0, recorder.getInput(), 1 );
+ }
+ }
+
+ private void setupEnvelope() {
+ // Setup envelope. The envelope has a total duration of 1.0 seconds.
+ // Values are (duration,target) pairs.
+ double[] pairs = new double[5 * 2 * 2];
+ int i = 0;
+ // duration, target for delay
+ pairs[i++] = 0.15;
+ pairs[i++] = 0.0;
+ // duration, target for attack
+ pairs[i++] = 0.05;
+ pairs[i++] = 1.0;
+ // duration, target for release
+ pairs[i++] = 0.1;
+ pairs[i++] = 0.6;
+ // duration, target for sustain
+ pairs[i++] = 0.1;
+ pairs[i++] = 0.6;
+ // duration, target for release
+ pairs[i++] = 0.1;
+ pairs[i++] = 0.0;
+ // Create mirror image of this envelope.
+ int halfLength = i;
+ while (i < pairs.length) {
+ pairs[i] = pairs[i - halfLength];
+ i++;
+ pairs[i] = pairs[i - halfLength] * -1.0;
+ i++;
+ }
+ envelope = new SegmentedEnvelope(pairs);
+
+ envelopePlayer.dataQueue.queueLoop(envelope, 0, envelope.getNumFrames());
+ }
+
+ public static void main(String[] args) {
+ try {
+ new NotesToTone().play();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+}
diff --git a/tests/com/jsyn/examples/PlayChords.java b/tests/com/jsyn/examples/PlayChords.java
new file mode 100644
index 0000000..0b1ae2e
--- /dev/null
+++ b/tests/com/jsyn/examples/PlayChords.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.examples;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import com.jsyn.instruments.SubtractiveSynthVoice;
+import com.jsyn.unitgen.LineOut;
+import com.jsyn.unitgen.UnitVoice;
+import com.jsyn.util.VoiceAllocator;
+import com.softsynth.shared.time.TimeStamp;
+
+/**
+ * Play chords and melody using the VoiceAllocator.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ */
+public class PlayChords {
+ private static final int MAX_VOICES = 8;
+ private Synthesizer synth;
+ private VoiceAllocator allocator;
+ private LineOut lineOut;
+ /** Number of seconds to generate music in advance of presentation-time. */
+ private double advance = 0.2;
+ private double secondsPerBeat = 0.6;
+ // on time over note duration
+ private double dutyCycle = 0.8;
+ private double measure = secondsPerBeat * 4.0;
+ private UnitVoice[] voices;
+
+ private void test() {
+ synth = JSyn.createSynthesizer();
+
+ // Add an output.
+ synth.add(lineOut = new LineOut());
+
+ voices = new UnitVoice[MAX_VOICES];
+ for (int i = 0; i < MAX_VOICES; i++) {
+ SubtractiveSynthVoice voice = new SubtractiveSynthVoice();
+ synth.add(voice);
+ voice.getOutput().connect(0, lineOut.input, 0);
+ voice.getOutput().connect(0, lineOut.input, 1);
+ voices[i] = voice;
+ }
+ allocator = new VoiceAllocator(voices);
+
+ // Start synthesizer using default stereo output at 44100 Hz.
+ synth.start();
+ // We only need to start the LineOut. It will pull data from the
+ // voices.
+ lineOut.start();
+
+ // Get synthesizer time in seconds.
+ double timeNow = synth.getCurrentTime();
+
+ // Advance to a near future time so we have a clean start.
+ double time = timeNow + 1.0;
+
+ try {
+ int tonic = 60 - 12;
+ for (int i = 0; i < 4; i++) {
+ playMajorMeasure1(time, tonic);
+ time += measure;
+ catchUp(time);
+ playMajorMeasure1(time, tonic + 4);
+ time += measure;
+ catchUp(time);
+ playMajorMeasure1(time, tonic + 7);
+ time += measure;
+ catchUp(time);
+ playMinorMeasure1(time, tonic + 2); // minor chord
+ time += measure;
+ catchUp(time);
+ }
+ time += secondsPerBeat;
+ catchUp(time);
+
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+
+ // Stop everything.
+ synth.stop();
+ }
+
+ private void playMinorMeasure1(double time, int base) throws InterruptedException {
+ int p1 = base;
+ int p2 = base + 3;
+ int p3 = base + 7;
+ playChord1(time, p1, p2, p3);
+ playNoodle1(time, p1 + 24, p2 + 24, p3 + 24);
+ }
+
+ private void playMajorMeasure1(double time, int base) throws InterruptedException {
+ int p1 = base;
+ int p2 = base + 4;
+ int p3 = base + 7;
+ playChord1(time, p1, p2, p3);
+ playNoodle1(time, p1 + 24, p2 + 24, p3 + 24);
+ }
+
+ private void playNoodle1(double time, int p1, int p2, int p3) {
+ double secondsPerNote = secondsPerBeat * 0.5;
+ for (int i = 0; i < 8; i++) {
+ int p = pickFromThree(p1, p2, p3);
+ noteOn(time, p);
+ noteOff(time + dutyCycle * secondsPerNote, p);
+ time += secondsPerNote;
+ }
+ }
+
+ private int pickFromThree(int p1, int p2, int p3) {
+ int r = (int) (Math.random() * 3.0);
+ if (r < 1)
+ return p1;
+ else if (r < 2)
+ return p2;
+ else
+ return p3;
+ }
+
+ private void playChord1(double time, int p1, int p2, int p3) throws InterruptedException {
+ double dur = dutyCycle * secondsPerBeat;
+ playTriad(time, dur, p1, p2, p3);
+ time += secondsPerBeat;
+ playTriad(time, dur, p1, p2, p3);
+ time += secondsPerBeat;
+ playTriad(time, dur * 0.25, p1, p2, p3);
+ time += secondsPerBeat * 0.25;
+ playTriad(time, dur * 0.25, p1, p2, p3);
+ time += secondsPerBeat * 0.75;
+ playTriad(time, dur, p1, p2, p3);
+ time += secondsPerBeat;
+ }
+
+ private void playTriad(double time, double dur, int p1, int p2, int p3)
+ throws InterruptedException {
+ noteOn(time, p1);
+ noteOn(time, p2);
+ noteOn(time, p3);
+ double offTime = time + dur;
+ noteOff(offTime, p1);
+ noteOff(offTime, p2);
+ noteOff(offTime, p3);
+ }
+
+ private void catchUp(double time) throws InterruptedException {
+ synth.sleepUntil(time - advance);
+ }
+
+ private void noteOff(double time, int noteNumber) {
+ allocator.noteOff(noteNumber, new TimeStamp(time));
+ }
+
+ private void noteOn(double time, int noteNumber) {
+ double frequency = convertPitchToFrequency(noteNumber);
+ double amplitude = 0.2;
+ TimeStamp timeStamp = new TimeStamp(time);
+ allocator.noteOn(noteNumber, frequency, amplitude, timeStamp);
+ }
+
+ /**
+ * Calculate frequency in Hertz based on MIDI pitch. Middle C is 60.0. You can use fractional
+ * pitches so 60.5 would give you a pitch half way between C and C#.
+ */
+ double convertPitchToFrequency(double pitch) {
+ final double concertA = 440.0;
+ return concertA * Math.pow(2.0, ((pitch - 69) * (1.0 / 12.0)));
+ }
+
+ public static void main(String[] args) {
+ new PlayChords().test();
+ }
+}
diff --git a/tests/com/jsyn/examples/PlayCustomUnit.java b/tests/com/jsyn/examples/PlayCustomUnit.java
new file mode 100644
index 0000000..609c351
--- /dev/null
+++ b/tests/com/jsyn/examples/PlayCustomUnit.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.examples;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import com.jsyn.unitgen.LineOut;
+import com.jsyn.unitgen.SineOscillator;
+import com.jsyn.unitgen.UnitOscillator;
+
+/**
+ * Play a tone using a JSyn oscillator and process it using a custom unit generator.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class PlayCustomUnit {
+ private Synthesizer synth;
+ private UnitOscillator osc;
+ private CustomCubeUnit cuber;
+ private LineOut lineOut;
+
+ private void test() {
+ synth = JSyn.createSynthesizer();
+ // Add a tone generator.
+ synth.add(osc = new SineOscillator());
+ // Add a tone generator.
+ synth.add(cuber = new CustomCubeUnit());
+ // Add an output to the DAC.
+ synth.add(lineOut = new LineOut());
+ // Connect the oscillator to the cuber.
+ osc.output.connect(0, cuber.input, 0);
+ // Connect the cuber to the right output.
+ cuber.output.connect(0, lineOut.input, 1);
+ // Send the original to the left output for comparison.
+ osc.output.connect(0, lineOut.input, 0);
+
+ osc.frequency.set(240.0);
+
+ // Start synthesizer using default stereo output at 44100 Hz.
+ synth.start();
+ // We only need to start the LineOut.
+ // It will pull data from the cuber and the oscillator.
+ lineOut.start();
+ // Sleep while the sound is generated in the background.
+ try {
+ double time = synth.getCurrentTime();
+ // Sleep for a few seconds.
+ synth.sleepUntil(time + 10.0);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ // Stop everything.
+ synth.stop();
+ }
+
+ public static void main(String[] args) {
+ new PlayCustomUnit().test();
+ }
+}
diff --git a/tests/com/jsyn/examples/PlayFunction.java b/tests/com/jsyn/examples/PlayFunction.java
new file mode 100644
index 0000000..700152b
--- /dev/null
+++ b/tests/com/jsyn/examples/PlayFunction.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.examples;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import com.jsyn.data.Function;
+import com.jsyn.unitgen.FunctionOscillator;
+import com.jsyn.unitgen.LineOut;
+
+/**
+ * Play a tone using a FunctionOscillator.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class PlayFunction {
+ Synthesizer synth;
+ FunctionOscillator osc;
+ LineOut lineOut;
+
+ private void test() {
+ // Create a context for the synthesizer.
+ synth = JSyn.createSynthesizer();
+
+ // Start synthesizer using default stereo output at 44100 Hz.
+ synth.start();
+
+ // Add a FunctionOscillator
+ synth.add(osc = new FunctionOscillator());
+
+ // Define a function that gives the shape of the waveform.
+ Function func = new Function() {
+ @Override
+ public double evaluate(double input) {
+ // Input ranges from -1.0 to 1.0
+ double s = Math.sin(input * Math.PI * 2.0);
+ double cubed = s * s * s;
+ return cubed;
+ }
+ };
+ osc.function.set(func);
+
+ // Add a stereo audio output unit.
+ synth.add(lineOut = new LineOut());
+
+ // Connect the oscillator to both channels of the output.
+ osc.output.connect(0, lineOut.input, 0);
+ osc.output.connect(0, lineOut.input, 1);
+
+ // Set the frequency and amplitude for the sine wave.
+ osc.frequency.set(345.0);
+ osc.amplitude.set(0.6);
+
+ // We only need to start the LineOut. It will pull data from the
+ // oscillator.
+ lineOut.start();
+
+ System.out.println("You should now be hearing a sine wave. ---------");
+
+ // Sleep while the sound is generated in the background.
+ try {
+ double time = synth.getCurrentTime();
+ // Sleep for a few seconds.
+ synth.sleepUntil(time + 4.0);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+
+ System.out.println("Stop playing. -------------------");
+ // Stop everything.
+ synth.stop();
+ }
+
+ public static void main(String[] args) {
+ new PlayFunction().test();
+ }
+}
diff --git a/tests/com/jsyn/examples/PlayGrains.java b/tests/com/jsyn/examples/PlayGrains.java
new file mode 100644
index 0000000..6b2b11e
--- /dev/null
+++ b/tests/com/jsyn/examples/PlayGrains.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright 2011 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.examples;
+
+import java.awt.BorderLayout;
+import java.awt.GridLayout;
+import java.io.File;
+import java.io.IOException;
+
+import javax.swing.BorderFactory;
+import javax.swing.JApplet;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import com.jsyn.data.FloatSample;
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.scope.AudioScope;
+import com.jsyn.swing.DoubleBoundedRangeModel;
+import com.jsyn.swing.JAppletFrame;
+import com.jsyn.swing.PortModelFactory;
+import com.jsyn.swing.RotaryTextController;
+import com.jsyn.unitgen.ContinuousRamp;
+import com.jsyn.unitgen.GrainFarm;
+import com.jsyn.unitgen.LineOut;
+import com.jsyn.unitgen.SampleGrainFarm;
+import com.jsyn.util.SampleLoader;
+import com.jsyn.util.WaveRecorder;
+
+/**
+ * Play with Granular Synthesis tools.
+ *
+ * @author Phil Burk (C) 2011 Mobileer Inc
+ */
+public class PlayGrains extends JApplet {
+ private static final long serialVersionUID = -8315903842197137926L;
+ private Synthesizer synth;
+ private LineOut lineOut;
+ private AudioScope scope;
+ private GrainFarm grainFarm;
+ private ContinuousRamp ramp;
+ private static final int NUM_GRAINS = 32;
+ private FloatSample sample;
+ private WaveRecorder recorder;
+
+ private static final boolean useSample = true;
+ private final static boolean useRecorder = false;
+
+ // File sampleFile = new File( "samples/instructions.wav" );
+ File sampleFile = new File(
+ // "/Users/phil/Work/jsyn/guitar100/Guitar100_Ocean_1#02.aif" );
+ "/Users/phil/Music/samples/ChewyMonkeysWhistle.aiff");
+
+ /* Can be run as either an application or as an applet. */
+ public static void main(String args[]) {
+ PlayGrains applet = new PlayGrains();
+ JAppletFrame frame = new JAppletFrame("PlayGrains", applet);
+ frame.setSize(840, 500);
+ frame.setVisible(true);
+ frame.test();
+ }
+
+ private void setupGUI() {
+ setLayout(new BorderLayout());
+
+ add(BorderLayout.NORTH,
+ new JLabel("PlayGrains in an AudioScope - JSyn V" + synth.getVersion()));
+
+ scope = new AudioScope(synth);
+
+ // scope.addProbe( osc.output );
+ scope.addProbe(grainFarm.output);
+
+ scope.setTriggerMode(AudioScope.TriggerMode.NORMAL);
+ scope.getView().setControlsVisible(true);
+ add(BorderLayout.CENTER, scope.getView());
+ scope.start();
+
+ // Arrange the knob in a row.
+ JPanel knobPanel = new JPanel();
+ knobPanel.setLayout(new GridLayout(1, 0));
+
+ if (useSample) {
+ SampleGrainFarm sampleGrainFarm = (SampleGrainFarm) grainFarm;
+ knobPanel.add(setupLinearPortKnob(ramp.time, 0.001, 10.0, "Time"));
+ knobPanel.add(setupLinearPortKnob(ramp.input, -1.0, 1.0, "Position"));
+ knobPanel.add(setupLinearPortKnob(sampleGrainFarm.positionRange, 0.0, 0.5, "PosRange"));
+ }
+ knobPanel.add(setupPortKnob(grainFarm.density, 1.0, "Density"));
+ knobPanel.add(setupPortKnob(grainFarm.rate, 4.0, "Rate"));
+ knobPanel.add(setupPortKnob(grainFarm.rateRange, 3.0, "RateRange"));
+ knobPanel.add(setupPortKnob(grainFarm.duration, 0.1, "Duration"));
+ knobPanel.add(setupPortKnob(grainFarm.durationRange, 3.0, "DurRange"));
+ knobPanel.add(setupPortKnob(grainFarm.amplitude, 6.0, "Amplitude"));
+ knobPanel.add(setupPortKnob(grainFarm.amplitudeRange, 1.0, "AmpRange"));
+ add(knobPanel, BorderLayout.SOUTH);
+
+ validate();
+ }
+
+ private RotaryTextController setupLinearPortKnob(UnitInputPort port, double min, double max,
+ String label) {
+ port.setMinimum(min);
+ port.setMaximum(max);
+
+ DoubleBoundedRangeModel model = PortModelFactory.createLinearModel(port);
+ RotaryTextController knob = new RotaryTextController(model, 10);
+ knob.setBorder(BorderFactory.createTitledBorder(label));
+ knob.setTitle(label);
+ return knob;
+ }
+
+ private RotaryTextController setupPortKnob(UnitInputPort port, double max, String label) {
+ port.setMinimum(0.0);
+ port.setMaximum(max);
+
+ DoubleBoundedRangeModel model = PortModelFactory.createExponentialModel(port);
+ RotaryTextController knob = new RotaryTextController(model, 10);
+ knob.setBorder(BorderFactory.createTitledBorder(label));
+ knob.setTitle(label);
+ return knob;
+ }
+
+ @Override
+ public void start() {
+ synth = JSyn.createSynthesizer();
+
+ try {
+
+ if (useRecorder) {
+ File waveFile = new File("temp_recording.wav");
+ // Record mono 16 bits.
+ recorder = new WaveRecorder(synth, waveFile, 1);
+ System.out.println("Writing to WAV file " + waveFile.getAbsolutePath());
+ }
+
+ if (useSample) {
+ sample = SampleLoader.loadFloatSample(sampleFile);
+ SampleGrainFarm sampleGrainFarm = new SampleGrainFarm();
+ synth.add(ramp = new ContinuousRamp());
+ sampleGrainFarm.setSample(sample);
+ ramp.output.connect(sampleGrainFarm.position);
+ grainFarm = sampleGrainFarm;
+ } else {
+ GrainFarm sampleGrainFarm = new GrainFarm();
+ grainFarm = sampleGrainFarm;
+ }
+
+ synth.add(grainFarm);
+
+ grainFarm.allocate(NUM_GRAINS);
+
+ // Add an output so we can hear the grains.
+ synth.add(lineOut = new LineOut());
+
+ grainFarm.getOutput().connect(0, lineOut.input, 0);
+ grainFarm.getOutput().connect(0, lineOut.input, 1);
+
+ // Start synthesizer using default stereo output at 44100 Hz.
+ synth.start();
+ // Start lineOut so it can pull data from other units.
+ lineOut.start();
+
+ if (useRecorder) {
+ grainFarm.output.connect(0, recorder.getInput(), 0);
+ // When we start the recorder it will pull data from the
+ // oscillator
+ // and sweeper.
+ recorder.start();
+ }
+
+ setupGUI();
+ // We only need to start the LineOut. It will pull data from the
+ // oscillator.
+ lineOut.start();
+ } catch (IOException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+
+ }
+
+ @Override
+ public void stop() {
+ try {
+ if (recorder != null) {
+ recorder.stop();
+ recorder.close();
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ scope.stop();
+ synth.stop();
+ }
+
+}
diff --git a/tests/com/jsyn/examples/PlayNotes.java b/tests/com/jsyn/examples/PlayNotes.java
new file mode 100644
index 0000000..65dc930
--- /dev/null
+++ b/tests/com/jsyn/examples/PlayNotes.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.examples;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import com.jsyn.unitgen.LineOut;
+import com.jsyn.unitgen.SawtoothOscillator;
+import com.jsyn.unitgen.UnitGenerator;
+import com.jsyn.unitgen.UnitVoice;
+import com.softsynth.shared.time.TimeStamp;
+
+/**
+ * Play notes using timestamped noteOn and noteOff methods of the UnitVoice.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ */
+public class PlayNotes {
+ Synthesizer synth;
+ UnitGenerator ugen;
+ UnitVoice voice;
+ LineOut lineOut;
+
+ private void test() {
+ // Create a context for the synthesizer.
+ synth = JSyn.createSynthesizer();
+ // Set output latency to 123 msec because this is not an interactive app.
+ synth.getAudioDeviceManager().setSuggestedOutputLatency(0.123);
+
+ // Add a tone generator.
+ synth.add(ugen = new SawtoothOscillator());
+ // synth.add( ugen = new SineOscillator() );
+ // synth.add( ugen = new SubtractiveSynthVoice() );
+ voice = (UnitVoice) ugen;
+ // Add an output mixer.
+ synth.add(lineOut = new LineOut());
+
+ // Connect the oscillator to the left and right audio output.
+ voice.getOutput().connect(0, lineOut.input, 0);
+ voice.getOutput().connect(0, lineOut.input, 1);
+
+ // Start synthesizer using default stereo output at 44100 Hz.
+ synth.start();
+
+ // Get synthesizer time in seconds.
+ double timeNow = synth.getCurrentTime();
+
+ // Advance to a near future time so we have a clean start.
+ TimeStamp timeStamp = new TimeStamp(timeNow + 0.5);
+
+ // We only need to start the LineOut. It will pull data from the
+ // oscillator.
+ synth.startUnit(lineOut, timeStamp);
+
+ // Schedule a note on and off.
+ double freq = 200.0; // hertz
+ double duration = 1.4;
+ double onTime = 1.0;
+ voice.noteOn(freq, 0.5, timeStamp);
+ voice.noteOff(timeStamp.makeRelative(onTime));
+
+ // Schedule this to happen a bit later.
+ timeStamp = timeStamp.makeRelative(duration);
+ freq *= 1.5; // up a perfect fifth
+ voice.noteOn(freq, 0.5, timeStamp);
+ voice.noteOff(timeStamp.makeRelative(onTime));
+
+ timeStamp = timeStamp.makeRelative(duration);
+ freq *= 4.0 / 5.0; // down a major third
+ voice.noteOn(freq, 0.5, timeStamp);
+ voice.noteOff(timeStamp.makeRelative(onTime));
+
+ // Sleep while the song is being generated in the background thread.
+ try {
+ System.out.println("Sleep while synthesizing.");
+ synth.sleepUntil(timeStamp.getTime() + 2.0);
+ System.out.println("Woke up...");
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+
+ // Stop everything.
+ synth.stop();
+ }
+
+ public static void main(String[] args) {
+ new PlayNotes().test();
+ }
+}
diff --git a/tests/com/jsyn/examples/PlayPartials.java b/tests/com/jsyn/examples/PlayPartials.java
new file mode 100644
index 0000000..1d7d88e
--- /dev/null
+++ b/tests/com/jsyn/examples/PlayPartials.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.examples;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import com.jsyn.unitgen.LineOut;
+import com.jsyn.unitgen.LinearRamp;
+import com.jsyn.unitgen.Multiply;
+import com.jsyn.unitgen.SineOscillator;
+import com.jsyn.unitgen.UnitOscillator;
+
+/**
+ * Play a enharmonic sine tones using JSyn oscillators.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class PlayPartials {
+ private Synthesizer synth;
+ private UnitOscillator[] osc;
+ private Multiply[] multipliers;
+ private LinearRamp ramp;
+ private LineOut lineOut;
+ private double[] amps = {
+ 0.2, 0.1, 0.3, 0.4
+ };
+ private double[] ratios = {
+ 1.0, Math.sqrt(2.0), Math.E, Math.PI
+ };
+
+ private void test() {
+ // Create a context for the synthesizer.
+ synth = JSyn.createSynthesizer();
+
+ // Start synthesizer using default stereo output at 44100 Hz.
+ synth.start();
+
+ // Add a stereo audio output unit.
+ synth.add(lineOut = new LineOut());
+ synth.add(ramp = new LinearRamp());
+
+ // Add a tone generator.
+ osc = new SineOscillator[amps.length];
+ multipliers = new Multiply[ratios.length];
+
+ for (int i = 0; i < osc.length; i++) {
+ // Create unit generators and store them in arrays.
+ synth.add(osc[i] = new SineOscillator());
+ synth.add(multipliers[i] = new Multiply());
+
+ // Connect each oscillator to both channels of the output.
+ // They will be mixed automatically.
+ osc[i].output.connect(0, lineOut.input, 0);
+ osc[i].output.connect(0, lineOut.input, 1);
+
+ // Use a multiplier to scale the output of the ramp.
+ // output = inputA * inputB
+ ramp.output.connect(multipliers[i].inputA);
+ multipliers[i].output.connect(osc[i].frequency);
+ multipliers[i].inputB.set(ratios[i]);
+
+ osc[i].amplitude.set(amps[i]);
+ }
+
+ // start ramping up
+ ramp.current.set(100.0);
+ ramp.time.set(3.0);
+ ramp.input.set(700.0);
+
+ // We only need to start the LineOut. It will pull data from the
+ // oscillator.
+ lineOut.start();
+
+ System.out.println("You should now be hearing a sine wave. ---------");
+
+ // Sleep while the sound is generated in the background.
+ try {
+ // Sleep for a few seconds.
+ synth.sleepFor(4.0);
+ // ramp down
+ ramp.input.set(100.0);
+ synth.sleepFor(4.0);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+
+ System.out.println("Stop playing. -------------------");
+ // Stop everything.
+ synth.stop();
+ }
+
+ public static void main(String[] args) {
+ System.out.println("Java version = " + System.getProperty("java.version"));
+ new PlayPartials().test();
+ }
+}
diff --git a/tests/com/jsyn/examples/PlaySample.java b/tests/com/jsyn/examples/PlaySample.java
new file mode 100644
index 0000000..ac3d5ff
--- /dev/null
+++ b/tests/com/jsyn/examples/PlaySample.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.examples;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import com.jsyn.data.FloatSample;
+import com.jsyn.unitgen.LineOut;
+import com.jsyn.unitgen.VariableRateDataReader;
+import com.jsyn.unitgen.VariableRateMonoReader;
+import com.jsyn.unitgen.VariableRateStereoReader;
+import com.jsyn.util.SampleLoader;
+
+/**
+ * Play a sample from a WAV file using JSyn.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class PlaySample {
+ private Synthesizer synth;
+ private VariableRateDataReader samplePlayer;
+ private LineOut lineOut;
+
+ private void test() {
+
+ URL sampleFile;
+ try {
+ sampleFile = new URL("http://www.softsynth.com/samples/Clarinet.wav");
+ // sampleFile = new URL("http://www.softsynth.com/samples/NotHereNow22K.wav");
+ } catch (MalformedURLException e2) {
+ e2.printStackTrace();
+ return;
+ }
+
+ synth = JSyn.createSynthesizer();
+
+ FloatSample sample;
+ try {
+ // Add an output mixer.
+ synth.add(lineOut = new LineOut());
+
+ // Load the sample and display its properties.
+ SampleLoader.setJavaSoundPreferred(false);
+ sample = SampleLoader.loadFloatSample(sampleFile);
+ System.out.println("Sample has: channels = " + sample.getChannelsPerFrame());
+ System.out.println(" frames = " + sample.getNumFrames());
+ System.out.println(" rate = " + sample.getFrameRate());
+ System.out.println(" loopStart = " + sample.getSustainBegin());
+ System.out.println(" loopEnd = " + sample.getSustainEnd());
+
+ if (sample.getChannelsPerFrame() == 1) {
+ synth.add(samplePlayer = new VariableRateMonoReader());
+ samplePlayer.output.connect(0, lineOut.input, 0);
+ } else if (sample.getChannelsPerFrame() == 2) {
+ synth.add(samplePlayer = new VariableRateStereoReader());
+ samplePlayer.output.connect(0, lineOut.input, 0);
+ samplePlayer.output.connect(1, lineOut.input, 1);
+ } else {
+ throw new RuntimeException("Can only play mono or stereo samples.");
+ }
+
+ // Start synthesizer using default stereo output at 44100 Hz.
+ synth.start();
+
+ samplePlayer.rate.set(sample.getFrameRate());
+
+ // We only need to start the LineOut. It will pull data from the
+ // sample player.
+ lineOut.start();
+
+ // We can simply queue the entire file.
+ // Or if it has a loop we can play the loop for a while.
+ if (sample.getSustainBegin() < 0) {
+ System.out.println("queue the sample");
+ samplePlayer.dataQueue.queue(sample);
+ } else {
+ System.out.println("queueOn the sample");
+ samplePlayer.dataQueue.queueOn(sample);
+ synth.sleepFor(8.0);
+ System.out.println("queueOff the sample");
+ samplePlayer.dataQueue.queueOff(sample);
+ }
+
+ // Wait until the sample has finished playing.
+ do {
+ synth.sleepFor(1.0);
+ } while (samplePlayer.dataQueue.hasMore());
+
+ synth.sleepFor(0.5);
+
+ } catch (IOException e1) {
+ e1.printStackTrace();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ // Stop everything.
+ synth.stop();
+ }
+
+ public static void main(String[] args) {
+ new PlaySample().test();
+ }
+}
diff --git a/tests/com/jsyn/examples/PlaySampleCrossfade.java b/tests/com/jsyn/examples/PlaySampleCrossfade.java
new file mode 100644
index 0000000..b5ea5ca
--- /dev/null
+++ b/tests/com/jsyn/examples/PlaySampleCrossfade.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.examples;
+
+import java.awt.GridLayout;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import javax.swing.JApplet;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import com.jsyn.data.FloatSample;
+import com.jsyn.devices.AudioDeviceFactory;
+import com.jsyn.ports.QueueDataCommand;
+import com.jsyn.swing.DoubleBoundedRangeModel;
+import com.jsyn.swing.DoubleBoundedRangeSlider;
+import com.jsyn.swing.JAppletFrame;
+import com.jsyn.swing.PortControllerFactory;
+import com.jsyn.unitgen.LineOut;
+import com.jsyn.unitgen.VariableRateDataReader;
+import com.jsyn.unitgen.VariableRateMonoReader;
+import com.jsyn.unitgen.VariableRateStereoReader;
+import com.jsyn.util.SampleLoader;
+
+/**
+ * Play a sample from a WAV file using JSyn. Use a crossfade to play a loop at an arbitrary
+ * position.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class PlaySampleCrossfade extends JApplet {
+ private static final double LOOP_START_FRACTION = 0.2;
+ private Synthesizer synth;
+ private VariableRateDataReader samplePlayer;
+ private LineOut lineOut;
+ private FloatSample sample;
+ private DoubleBoundedRangeModel rangeModelSize;
+ private DoubleBoundedRangeModel rangeModelCrossfade;
+ private int loopStartFrame;
+
+ @Override
+ public void init() {
+
+ URL sampleFile;
+ try {
+ sampleFile = new URL("http://www.softsynth.com/samples/Clarinet.wav");
+ } catch (MalformedURLException e2) {
+ e2.printStackTrace();
+ return;
+ }
+
+ synth = JSyn.createSynthesizer(AudioDeviceFactory.createAudioDeviceManager(true));
+
+ try {
+ // Add an output mixer.
+ synth.add(lineOut = new LineOut());
+
+ // Load the sample and display its properties.
+ SampleLoader.setJavaSoundPreferred(false);
+ sample = SampleLoader.loadFloatSample(sampleFile);
+ System.out.println("Sample has: channels = " + sample.getChannelsPerFrame());
+ System.out.println(" frames = " + sample.getNumFrames());
+ System.out.println(" rate = " + sample.getFrameRate());
+ System.out.println(" loopStart = " + sample.getSustainBegin());
+ System.out.println(" loopEnd = " + sample.getSustainEnd());
+
+ if (sample.getChannelsPerFrame() == 1) {
+ synth.add(samplePlayer = new VariableRateMonoReader());
+ samplePlayer.output.connect(0, lineOut.input, 0);
+ } else if (sample.getChannelsPerFrame() == 2) {
+ synth.add(samplePlayer = new VariableRateStereoReader());
+ samplePlayer.output.connect(0, lineOut.input, 0);
+ samplePlayer.output.connect(1, lineOut.input, 1);
+ } else {
+ throw new RuntimeException("Can only play mono or stereo samples.");
+ }
+
+ samplePlayer.rate.set(sample.getFrameRate());
+
+ } catch (IOException e1) {
+ e1.printStackTrace();
+ }
+
+ // Start at arbitrary position near beginning of sample.
+ loopStartFrame = (int) (sample.getNumFrames() * LOOP_START_FRACTION);
+
+ // Arrange the faders in a stack.
+ setLayout(new GridLayout(0, 1));
+
+ samplePlayer.rate.setup(4000.0, sample.getFrameRate(), sample.getFrameRate() * 2.0);
+ add(PortControllerFactory.createExponentialPortSlider(samplePlayer.rate));
+
+ // Use fader to select arbitrary loop size.
+ rangeModelSize = new DoubleBoundedRangeModel("LoopSize", 10000, 0.01,
+ (1.0 - LOOP_START_FRACTION), 0.5);
+ rangeModelSize.addChangeListener(new ChangeListener() {
+ @Override
+ public void stateChanged(ChangeEvent e) {
+ queueNewLoop();
+ }
+
+ });
+ add(new DoubleBoundedRangeSlider(rangeModelSize, 3));
+
+ // Use fader to set the size of the crossfade region.
+ rangeModelCrossfade = new DoubleBoundedRangeModel("Crossfade", 1000, 0.0, 1000.0, 0.0);
+ rangeModelCrossfade.addChangeListener(new ChangeListener() {
+ @Override
+ public void stateChanged(ChangeEvent e) {
+ queueNewLoop();
+ }
+
+ });
+
+ add(new DoubleBoundedRangeSlider(rangeModelCrossfade, 3));
+
+ validate();
+ }
+
+ private void queueNewLoop() {
+ int loopSize = (int) (sample.getNumFrames() * rangeModelSize.getDoubleValue());
+ if ((loopStartFrame + loopSize) > sample.getNumFrames()) {
+ loopSize = sample.getNumFrames() - loopStartFrame;
+ }
+ int crossFadeSize = (int) (rangeModelCrossfade.getDoubleValue());
+
+ // For complex queuing operations, create a command and then customize it.
+ QueueDataCommand command = samplePlayer.dataQueue.createQueueDataCommand(sample,
+ loopStartFrame, loopSize);
+ command.setNumLoops(-1);
+ command.setSkipIfOthers(true);
+ command.setCrossFadeIn(crossFadeSize);
+
+ System.out.println("Queue: " + loopStartFrame + ", #" + loopSize + ", X=" + crossFadeSize);
+ synth.queueCommand(command);
+ }
+
+ @Override
+ public void start() {
+ // Start synthesizer using default stereo output at 44100 Hz.
+ synth.start();
+ // Start the LineOut. It will pull data from the oscillator.
+ lineOut.start();
+
+ // Queue attack portion of sample.
+ samplePlayer.dataQueue.queue(sample, 0, loopStartFrame);
+ queueNewLoop();
+ }
+
+ @Override
+ public void stop() {
+ synth.stop();
+ synth.stop();
+ }
+
+ /* Can be run as either an application or as an applet. */
+ public static void main(String args[]) {
+ PlaySampleCrossfade applet = new PlaySampleCrossfade();
+ JAppletFrame frame = new JAppletFrame("PlaySampleCrossfade", applet);
+ frame.setSize(440, 300);
+ frame.setVisible(true);
+ frame.test();
+ }
+
+}
diff --git a/tests/com/jsyn/examples/PlaySegmentedEnvelope.java b/tests/com/jsyn/examples/PlaySegmentedEnvelope.java
new file mode 100644
index 0000000..e7cc8f7
--- /dev/null
+++ b/tests/com/jsyn/examples/PlaySegmentedEnvelope.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.examples;
+
+import java.io.File;
+import java.io.IOException;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import com.jsyn.data.SegmentedEnvelope;
+import com.jsyn.unitgen.LineOut;
+import com.jsyn.unitgen.SawtoothOscillatorBL;
+import com.jsyn.unitgen.UnitOscillator;
+import com.jsyn.unitgen.VariableRateDataReader;
+import com.jsyn.unitgen.VariableRateMonoReader;
+import com.jsyn.util.WaveRecorder;
+
+/**
+ * Modulate the amplitude of an oscillator using a segmented envelope.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class PlaySegmentedEnvelope {
+ private Synthesizer synth;
+ private UnitOscillator osc;
+ private LineOut lineOut;
+ private SegmentedEnvelope envelope;
+ private VariableRateDataReader envelopePlayer;
+ private WaveRecorder recorder;
+ private final static boolean useRecorder = true;
+
+ private void test() throws IOException {
+ // Create a context for the synthesizer.
+ synth = JSyn.createSynthesizer();
+ // Add a tone generator.
+ synth.add(osc = new SawtoothOscillatorBL());
+ // Add an envelope player.
+ synth.add(envelopePlayer = new VariableRateMonoReader());
+
+ if (useRecorder) {
+ File waveFile = new File("temp_recording.wav");
+ // Default is stereo, 16 bits.
+ recorder = new WaveRecorder(synth, waveFile);
+ System.out.println("Writing to WAV file " + waveFile.getAbsolutePath());
+ }
+
+ // Create an envelope consisting of (duration,value) pairs.
+ double[] pairs = {
+ 0.1, 1.0, 0.2, 0.3, 0.6, 0.0
+ };
+ envelope = new SegmentedEnvelope(pairs);
+
+ // Add an output mixer.
+ synth.add(lineOut = new LineOut());
+ envelopePlayer.output.connect(osc.amplitude);
+ // Connect the oscillator to the output.
+ osc.output.connect(0, lineOut.input, 0);
+ osc.output.connect(0, lineOut.input, 1);
+
+ // Start synthesizer using default stereo output at 44100 Hz.
+ synth.start();
+
+ if (useRecorder) {
+ osc.output.connect(0, recorder.getInput(), 0);
+ envelopePlayer.output.connect(0, recorder.getInput(), 1);
+ // When we start the recorder it will pull data from the oscillator
+ // and sweeper.
+ recorder.start();
+ }
+
+ // We only need to start the LineOut. It will pull data from the other
+ // units.
+ lineOut.start();
+
+ try {
+ // ---------------------------------------------
+ // Queue the entire envelope to play once.
+ envelopePlayer.dataQueue.queue(envelope);
+ synth.sleepFor(2.0);
+
+ // ---------------------------------------------
+ // Queue the attack, then sustain for a while, then queue the
+ // release.
+ osc.frequency.set(750.0);
+ envelopePlayer.dataQueue.queue(envelope, 0, 2); // attack
+ synth.sleepFor(2.0);
+ envelopePlayer.dataQueue.queue(envelope, 2, 1); // release
+ synth.sleepFor(2.0);
+
+ // ---------------------------------------------
+ // Queue the attack, then sustain for a while, then queue the
+ // release. But this time use the sustain loop points.
+ osc.frequency.set(950.0);
+ // For noteOn, we want to play frames 0 and 1 then stop before 2.
+ envelope.setSustainBegin(2);
+ envelope.setSustainEnd(2);
+ envelopePlayer.dataQueue.queueOn(envelope); // attack
+ synth.sleepFor(2.0);
+ envelopePlayer.dataQueue.queueOff(envelope); // release
+ synth.sleepFor(2.0);
+
+ // ---------------------------------------------
+ // Queue the entire envelope to play 4 times (3 loops back).
+ osc.frequency.set(350.0);
+ envelopePlayer.dataQueue.queueLoop(envelope, 0, envelope.getNumFrames(), 3);
+ synth.sleepFor(5.0);
+
+ // ---------------------------------------------
+ // Queue the entire envelope as a repeating loop.
+ // It will loop until something else is queued.
+ osc.frequency.set(450.0);
+ envelopePlayer.dataQueue.queueLoop(envelope, 0, envelope.getNumFrames());
+ envelopePlayer.rate.set(3.0);
+ synth.sleepFor(5.0);
+ // Queue last frame to stop the looping.
+ envelopePlayer.dataQueue.queue(envelope, envelope.getNumFrames() - 1, 1);
+ synth.sleepFor(1.0);
+
+ if (recorder != null) {
+ recorder.stop();
+ recorder.close();
+ }
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ // Stop everything.
+ synth.stop();
+ }
+
+ public static void main(String[] args) {
+ try {
+ new PlaySegmentedEnvelope().test();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/tests/com/jsyn/examples/PlaySegmentedEnvelopeCallback.java b/tests/com/jsyn/examples/PlaySegmentedEnvelopeCallback.java
new file mode 100644
index 0000000..cf2441e
--- /dev/null
+++ b/tests/com/jsyn/examples/PlaySegmentedEnvelopeCallback.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.examples;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import com.jsyn.data.SegmentedEnvelope;
+import com.jsyn.ports.QueueDataCommand;
+import com.jsyn.ports.QueueDataEvent;
+import com.jsyn.ports.UnitDataQueueCallback;
+import com.jsyn.unitgen.LineOut;
+import com.jsyn.unitgen.SawtoothOscillatorBL;
+import com.jsyn.unitgen.UnitOscillator;
+import com.jsyn.unitgen.VariableRateDataReader;
+import com.jsyn.unitgen.VariableRateMonoReader;
+
+/**
+ * Use a UnitDataQueueCallback to notify us of the envelope's progress. Modulate the amplitude of an
+ * oscillator using a segmented envelope.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class PlaySegmentedEnvelopeCallback {
+ private Synthesizer synth;
+ private UnitOscillator osc;
+ private LineOut lineOut;
+ private SegmentedEnvelope envelope;
+ private VariableRateDataReader envelopePlayer;
+
+ private void test() {
+ // Create a context for the synthesizer.
+ synth = JSyn.createSynthesizer();
+ // Add a tone generator.
+ synth.add(osc = new SawtoothOscillatorBL());
+ // Add an envelope player.
+ synth.add(envelopePlayer = new VariableRateMonoReader());
+
+ // Create an envelope consisting of (duration,value) pairs.
+ double[] pairs = {
+ 0.1, 1.0, 0.2, 0.5, 0.6, 0.0
+ };
+ envelope = new SegmentedEnvelope(pairs);
+
+ // Add an output mixer.
+ synth.add(lineOut = new LineOut());
+ envelopePlayer.output.connect(osc.amplitude);
+ // Connect the oscillator to the output.
+ osc.output.connect(0, lineOut.input, 0);
+ osc.output.connect(0, lineOut.input, 1);
+
+ // Start synthesizer using default stereo output at 44100 Hz.
+ synth.start();
+ // We only need to start the LineOut. It will pull data from the other
+ // units.
+ lineOut.start();
+
+ try {
+ // Queue an envelope with callbacks.
+ QueueDataCommand command = envelopePlayer.dataQueue.createQueueDataCommand(envelope, 0,
+ envelope.getNumFrames());
+ // Create an object to be called when the queued data is done.
+ TestQueueCallback callback = new TestQueueCallback();
+ command.setCallback(callback);
+ command.setNumLoops(2);
+ envelopePlayer.rate.set(0.2);
+ synth.queueCommand(command);
+ synth.sleepFor(20.0);
+
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ // Stop everything.
+ synth.stop();
+ }
+
+ class TestQueueCallback implements UnitDataQueueCallback {
+ @Override
+ public void started(QueueDataEvent event) {
+ System.out.println("CALLBACK: Envelope started.");
+ }
+
+ @Override
+ public void looped(QueueDataEvent event) {
+ System.out.println("CALLBACK: Envelope looped.");
+ }
+
+ @Override
+ public void finished(QueueDataEvent event) {
+ System.out.println("CALLBACK: Envelope finished.");
+ // Queue the envelope again at a faster rate.
+ // (If this hangs we may have hit a deadlock.)
+ envelopePlayer.rate.set(2.0);
+ envelopePlayer.dataQueue.queue(envelope);
+ }
+ }
+
+ public static void main(String[] args) {
+ new PlaySegmentedEnvelopeCallback().test();
+ }
+}
diff --git a/tests/com/jsyn/examples/PlaySequence.java b/tests/com/jsyn/examples/PlaySequence.java
new file mode 100644
index 0000000..9d058b2
--- /dev/null
+++ b/tests/com/jsyn/examples/PlaySequence.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.examples;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import com.jsyn.unitgen.LineOut;
+import com.jsyn.unitgen.SawtoothOscillatorBL;
+import com.jsyn.unitgen.UnitOscillator;
+
+/**
+ * Use time stamps to change the frequency of an oscillator at precise times.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ */
+public class PlaySequence {
+ Synthesizer synth;
+ UnitOscillator osc;
+ LineOut lineOut;
+
+ private void test() {
+ // Create a context for the synthesizer.
+ synth = JSyn.createSynthesizer();
+
+ // Add a tone generator.
+ synth.add(osc = new SawtoothOscillatorBL());
+ // Add an output mixer.
+ synth.add(lineOut = new LineOut());
+
+ // Connect the oscillator to the left and right audio output.
+ osc.output.connect(0, lineOut.input, 0);
+ osc.output.connect(0, lineOut.input, 1);
+
+ // Start synthesizer using default stereo output at 44100 Hz.
+ synth.start();
+ // We only need to start the LineOut. It will pull data from the
+ // oscillator.
+ lineOut.start();
+
+ // Get synthesizer time in seconds.
+ double timeNow = synth.getCurrentTime();
+
+ // Advance to a near future time so we have a clean start.
+ double time = timeNow + 0.5;
+ double freq = 400.0; // hertz
+ osc.frequency.set(freq, time);
+
+ // Schedule this to happen a bit later.
+ time += 0.5;
+ freq *= 1.5; // up a perfect fifth
+ osc.frequency.set(freq, time);
+
+ time += 0.5;
+ freq *= 4.0 / 5.0; // down a major third
+ osc.frequency.set(freq, time);
+
+ // Sleep while the sound is being generated in the background thread.
+ try {
+ synth.sleepUntil(time + 0.5);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+
+ // Stop everything.
+ synth.stop();
+ }
+
+ public static void main(String[] args) {
+ new PlaySequence().test();
+ }
+}
diff --git a/tests/com/jsyn/examples/PlayTone.java b/tests/com/jsyn/examples/PlayTone.java
new file mode 100644
index 0000000..172c98a
--- /dev/null
+++ b/tests/com/jsyn/examples/PlayTone.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.examples;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import com.jsyn.unitgen.LineOut;
+import com.jsyn.unitgen.SineOscillator;
+import com.jsyn.unitgen.UnitOscillator;
+
+/**
+ * Play a tone using a JSyn oscillator.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class PlayTone {
+ Synthesizer synth;
+ UnitOscillator osc;
+ LineOut lineOut;
+
+ private void test() {
+
+ // Create a context for the synthesizer.
+ synth = JSyn.createSynthesizer();
+
+ // Start synthesizer using default stereo output at 44100 Hz.
+ synth.start();
+
+ // Add a tone generator.
+ synth.add(osc = new SineOscillator());
+ // Add a stereo audio output unit.
+ synth.add(lineOut = new LineOut());
+
+ // Connect the oscillator to both channels of the output.
+ osc.output.connect(0, lineOut.input, 0);
+ osc.output.connect(0, lineOut.input, 1);
+
+ // Set the frequency and amplitude for the sine wave.
+ osc.frequency.set(345.0);
+ osc.amplitude.set(0.6);
+
+ // We only need to start the LineOut. It will pull data from the
+ // oscillator.
+ lineOut.start();
+
+ System.out.println("You should now be hearing a sine wave. ---------");
+
+ // Sleep while the sound is generated in the background.
+ try {
+ double time = synth.getCurrentTime();
+ System.out.println("time = " + time);
+ // Sleep for a few seconds.
+ synth.sleepUntil(time + 4.0);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+
+ System.out.println("Stop playing. -------------------");
+ // Stop everything.
+ synth.stop();
+ }
+
+ public static void main(String[] args) {
+ new PlayTone().test();
+ }
+}
diff --git a/tests/com/jsyn/examples/RecordSineSweep.java b/tests/com/jsyn/examples/RecordSineSweep.java
new file mode 100644
index 0000000..bb248e8
--- /dev/null
+++ b/tests/com/jsyn/examples/RecordSineSweep.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * Test recording to disk in non-real-time.
+ * Play several frequencies of a sine wave.
+ * Save data in a WAV file format.
+ *
+ * @author (C) 2010 Phil Burk
+ */
+
+package com.jsyn.examples;
+
+import java.io.File;
+import java.io.IOException;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import com.jsyn.unitgen.ExponentialRamp;
+import com.jsyn.unitgen.LineOut;
+import com.jsyn.unitgen.SawtoothOscillatorBL;
+import com.jsyn.unitgen.SineOscillator;
+import com.jsyn.unitgen.UnitOscillator;
+import com.jsyn.util.WaveRecorder;
+
+public class RecordSineSweep {
+ final static double SONG_DURATION = 4.0;
+ private Synthesizer synth;
+ private UnitOscillator leftOsc;
+ private UnitOscillator rightOsc;
+ private ExponentialRamp sweeper;
+ private LineOut lineOut;
+ private WaveRecorder recorder;
+ private final static boolean useRecorder = true;
+
+ private void test() throws IOException {
+ // Create a context for the synthesizer.
+ synth = JSyn.createSynthesizer();
+ synth.setRealTime(false);
+
+ if (useRecorder) {
+ File waveFile = new File("temp_recording.wav");
+ // Default is stereo, 16 bits.
+ recorder = new WaveRecorder(synth, waveFile);
+ System.out.println("Writing to WAV file " + waveFile.getAbsolutePath());
+ }
+ // Add some tone generators.
+ synth.add(leftOsc = new SineOscillator());
+ synth.add(rightOsc = new SawtoothOscillatorBL());
+
+ // Add a controller that will sweep up.
+ synth.add(sweeper = new ExponentialRamp());
+ // Add an output unit.
+ synth.add(lineOut = new LineOut());
+
+ sweeper.current.set(50.0);
+ sweeper.input.set(1400.0);
+ sweeper.time.set(SONG_DURATION);
+ sweeper.output.connect(leftOsc.frequency);
+ sweeper.output.connect(rightOsc.frequency);
+
+ // Connect the oscillator to the left and right audio output.
+ leftOsc.output.connect(0, lineOut.input, 0);
+ rightOsc.output.connect(0, lineOut.input, 1);
+
+ // Start synthesizer using default stereo output at 44100 Hz.
+ synth.start();
+
+ if (useRecorder) {
+ leftOsc.output.connect(0, recorder.getInput(), 0);
+ rightOsc.output.connect(0, recorder.getInput(), 1);
+ // When we start the recorder it will pull data from the oscillator
+ // and sweeper.
+ recorder.start();
+ }
+
+ // We also need to start the LineOut if we want to hear it now.
+ lineOut.start();
+
+ // Get synthesizer time in seconds.
+ double timeNow = synth.getCurrentTime();
+
+ // Sleep while the sound is being generated in the background thread.
+ try {
+ synth.sleepUntil(timeNow + SONG_DURATION);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+
+ // Test stopping and restarting a recorder. This will cause a pop.
+ if (recorder != null) {
+ System.out.println("Stop and restart recorder.");
+ recorder.stop();
+ }
+ sweeper.input.set(100.0);
+ timeNow = synth.getCurrentTime();
+ if (recorder != null) {
+ recorder.start();
+ }
+
+ // Sleep while the sound is being generated in the background thread.
+ try {
+ synth.sleepUntil(timeNow + SONG_DURATION);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+
+ if (recorder != null) {
+ recorder.stop();
+ recorder.close();
+ }
+ // Stop everything.
+ synth.stop();
+ }
+
+ public static void main(String[] args) {
+ try {
+ new RecordSineSweep().test();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+}
diff --git a/tests/com/jsyn/examples/SampleHoldNoteBlaster.java b/tests/com/jsyn/examples/SampleHoldNoteBlaster.java
new file mode 100644
index 0000000..bc6b4d0
--- /dev/null
+++ b/tests/com/jsyn/examples/SampleHoldNoteBlaster.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2011 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.examples;
+
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.ports.UnitOutputPort;
+import com.jsyn.unitgen.Circuit;
+import com.jsyn.unitgen.EdgeDetector;
+import com.jsyn.unitgen.EnvelopeDAHDSR;
+import com.jsyn.unitgen.FilterLowPass;
+import com.jsyn.unitgen.Latch;
+import com.jsyn.unitgen.Multiply;
+import com.jsyn.unitgen.PassThrough;
+import com.jsyn.unitgen.PulseOscillator;
+import com.jsyn.unitgen.SawtoothOscillatorDPW;
+import com.jsyn.unitgen.SineOscillator;
+import com.jsyn.unitgen.UnitOscillator;
+import com.jsyn.unitgen.UnitSource;
+
+/**
+ * Classic osc-filter-envelope voice with a sample and hold.
+ *
+ * @author Phil Burk (C) 2011 Mobileer Inc
+ */
+public class SampleHoldNoteBlaster extends Circuit implements UnitSource {
+
+ public UnitInputPort frequency;
+ public UnitInputPort amplitude;
+ public UnitInputPort modRate;
+ public UnitInputPort modDepth;
+ private UnitInputPort cutoff;
+ private UnitInputPort resonance;
+ private UnitInputPort pulseRate;
+ private UnitInputPort sweepRate;
+ private UnitInputPort sweepDepth;
+ public UnitOutputPort output;
+
+ private static SampleHoldNoteBlaster soundMaker; // singleton
+
+ private UnitOscillator osc;
+ private UnitOscillator samplee; // for sample and hold
+ private PulseOscillator pulser;
+ private Latch latch;
+ private UnitOscillator lfo;
+ private FilterLowPass filter;
+ private PassThrough frequencyPin;
+ private Multiply modScaler;
+ private EnvelopeDAHDSR ampEnv;
+ private Multiply sweepScaler;
+ private EdgeDetector edgeDetector;
+ private UnitInputPort pulseWidth;
+ private UnitInputPort attack;
+ private UnitInputPort decay;
+ private UnitInputPort sustain;
+ private UnitInputPort release;
+
+ public static SampleHoldNoteBlaster getInstance() {
+ if (soundMaker == null) {
+ soundMaker = new SampleHoldNoteBlaster();
+ }
+ return soundMaker;
+ }
+
+ public SampleHoldNoteBlaster() {
+ add(frequencyPin = new PassThrough());
+ add(modScaler = new Multiply());
+ add(sweepScaler = new Multiply());
+ add(edgeDetector = new EdgeDetector());
+ add(latch = new Latch());
+ add(samplee = new SineOscillator());
+ add(pulser = new PulseOscillator());
+ add(lfo = new SineOscillator());
+ add(osc = new SawtoothOscillatorDPW());
+ add(filter = new FilterLowPass());
+ // Use an envelope to control the amplitude.
+ add(ampEnv = new EnvelopeDAHDSR());
+
+ samplee.output.connect(latch.input);
+ pulser.output.connect(edgeDetector.input);
+ edgeDetector.output.connect(latch.gate);
+ latch.output.connect(osc.frequency);
+
+ frequencyPin.output.connect(osc.frequency);
+
+ frequencyPin.output.connect(modScaler.inputA);
+ modScaler.output.connect(lfo.amplitude);
+
+ frequencyPin.output.connect(sweepScaler.inputA);
+ sweepScaler.output.connect(samplee.amplitude);
+
+ lfo.output.connect(osc.frequency);
+ osc.output.connect(filter.input);
+ filter.output.connect(ampEnv.amplitude);
+ pulser.output.connect(ampEnv.input);
+
+ // Setup ports ---------------
+ addPort(amplitude = osc.amplitude, "amplitude");
+ amplitude.set(0.6);
+
+ addPort(frequency = frequencyPin.input, "frequency");
+ frequency.setup(50.0, 800.0, 2000.0);
+
+ addPort(modRate = lfo.frequency, "modRate");
+ modRate.setup(0.0, 12, 20.0);
+
+ addPort(modDepth = modScaler.inputB, "modDepth");
+ modDepth.setup(0.0, 0.0, 0.5);
+
+ addPort(cutoff = filter.frequency, "cutoff");
+ cutoff.setup(20.0, 2000.0, 5000.0);
+ addPort(resonance = filter.Q, "Q");
+
+ addPort(sweepDepth = sweepScaler.inputB, "sweepDepth");
+ sweepDepth.setup(0.0, 0.6, 1.0);
+ addPort(sweepRate = samplee.frequency, "sweepRate");
+ sweepRate.setup(0.2, 5.9271, 20.0);
+
+ addPort(pulseRate = pulser.frequency, "pulseRate");
+ pulseRate.setup(0.2, 7.0, 20.0);
+ addPort(pulseWidth = pulser.width, "pulseWidth");
+ pulseWidth.setup(-0.9, 0.9, 0.9);
+
+ addPort(attack = ampEnv.attack, "attack");
+ attack.setup(0.001, 0.001, 2.0);
+ addPort(decay = ampEnv.decay, "decay");
+ decay.setup(0.001, 0.26, 2.0);
+ addPort(sustain = ampEnv.sustain, "sustain");
+ sustain.setup(0.000, 0.24, 1.0);
+ addPort(release = ampEnv.release, "release");
+ release.setup(0.001, 0.2, 2.0);
+
+ addPort(output = ampEnv.output);
+ }
+
+ @Override
+ public UnitOutputPort getOutput() {
+ return output;
+ }
+}
diff --git a/tests/com/jsyn/examples/SawFaders.java b/tests/com/jsyn/examples/SawFaders.java
new file mode 100644
index 0000000..eaa3d1b
--- /dev/null
+++ b/tests/com/jsyn/examples/SawFaders.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.examples;
+
+import java.awt.GridLayout;
+
+import javax.swing.JApplet;
+import javax.swing.JPanel;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import com.jsyn.swing.ExponentialRangeModel;
+import com.jsyn.swing.JAppletFrame;
+import com.jsyn.swing.PortControllerFactory;
+import com.jsyn.swing.PortModelFactory;
+import com.jsyn.swing.RotaryTextController;
+import com.jsyn.unitgen.LineOut;
+import com.jsyn.unitgen.LinearRamp;
+import com.jsyn.unitgen.SawtoothOscillatorBL;
+import com.jsyn.unitgen.UnitOscillator;
+
+/**
+ * Play a sawtooth using a JSyn oscillator and some knobs.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class SawFaders extends JApplet {
+ private static final long serialVersionUID = -2704222221111608377L;
+ private Synthesizer synth;
+ private UnitOscillator osc;
+ private LinearRamp lag;
+ private LineOut lineOut;
+
+ @Override
+ public void init() {
+ synth = JSyn.createSynthesizer();
+
+ // Add a tone generator. (band limited sawtooth)
+ synth.add(osc = new SawtoothOscillatorBL());
+ // Add a lag to smooth out amplitude changes and avoid pops.
+ synth.add(lag = new LinearRamp());
+ // Add an output mixer.
+ synth.add(lineOut = new LineOut());
+ // Connect the oscillator to both left and right output.
+ osc.output.connect(0, lineOut.input, 0);
+ osc.output.connect(0, lineOut.input, 1);
+
+ // Set the minimum, current and maximum values for the port.
+ lag.output.connect(osc.amplitude);
+ lag.input.setup(0.0, 0.5, 1.0);
+ lag.time.set(0.2);
+
+ // Arrange the faders in a stack.
+ setLayout(new GridLayout(0, 1));
+
+ ExponentialRangeModel amplitudeModel = PortModelFactory.createExponentialModel(lag.input);
+ RotaryTextController knob = new RotaryTextController(amplitudeModel, 5);
+ JPanel knobPanel = new JPanel();
+ knobPanel.add(knob);
+ add(knobPanel);
+
+ osc.frequency.setup(50.0, 300.0, 10000.0);
+ add(PortControllerFactory.createExponentialPortSlider(osc.frequency));
+ validate();
+ }
+
+ @Override
+ public void start() {
+ // Start synthesizer using default stereo output at 44100 Hz.
+ synth.start();
+ // We only need to start the LineOut. It will pull data from the
+ // oscillator.
+ lineOut.start();
+ }
+
+ @Override
+ public void stop() {
+ synth.stop();
+ }
+
+ /* Can be run as either an application or as an applet. */
+ public static void main(String args[]) {
+ SawFaders applet = new SawFaders();
+ JAppletFrame frame = new JAppletFrame("SawFaders", applet);
+ frame.setSize(440, 200);
+ frame.setVisible(true);
+ frame.test();
+ }
+
+}
diff --git a/tests/com/jsyn/examples/SeeGoogleWave.java b/tests/com/jsyn/examples/SeeGoogleWave.java
new file mode 100644
index 0000000..eb7a5ff
--- /dev/null
+++ b/tests/com/jsyn/examples/SeeGoogleWave.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.examples;
+
+import java.awt.BorderLayout;
+import java.awt.GridLayout;
+
+import javax.swing.JApplet;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import com.jsyn.scope.AudioScope;
+import com.jsyn.swing.JAppletFrame;
+import com.jsyn.swing.PortControllerFactory;
+import com.jsyn.unitgen.LineOut;
+
+/**
+ * Generate the waveform shown on the Google home page on 2/22/12.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class SeeGoogleWave extends JApplet {
+ private static final long serialVersionUID = -831590388347137926L;
+ private Synthesizer synth;
+ private GoogleWaveOscillator googleWaveUnit;
+ private LineOut lineOut;
+ private AudioScope scope;
+
+ /* Can be run as either an application or as an applet. */
+ public static void main(String args[]) {
+ SeeGoogleWave applet = new SeeGoogleWave();
+ JAppletFrame frame = new JAppletFrame("Google Wave", applet);
+ frame.setSize(640, 500);
+ frame.setVisible(true);
+ frame.test();
+ frame.validate();
+ }
+
+ private void setupGUI() {
+ setLayout(new BorderLayout());
+
+ add(BorderLayout.NORTH, new JLabel("GoogleWave - elliptical segments"));
+
+ scope = new AudioScope(synth);
+ scope.addProbe(googleWaveUnit.output);
+ scope.setTriggerMode(AudioScope.TriggerMode.NORMAL);
+ scope.getView().setShowControls(false);
+ scope.start();
+ add(BorderLayout.CENTER, scope.getView());
+
+ JPanel southPanel = new JPanel();
+ southPanel.setLayout(new GridLayout(0, 1));
+ add(BorderLayout.SOUTH, southPanel);
+
+ southPanel.add(PortControllerFactory.createExponentialPortSlider(googleWaveUnit.frequency));
+ southPanel.add(PortControllerFactory.createExponentialPortSlider(googleWaveUnit.variance));
+ southPanel.add(PortControllerFactory.createExponentialPortSlider(googleWaveUnit.amplitude));
+
+ validate();
+ }
+
+ @Override
+ public void start() {
+ synth = JSyn.createSynthesizer();
+ synth.add(googleWaveUnit = new GoogleWaveOscillator());
+ googleWaveUnit.amplitude.setup(0.02, 0.5, 1.0);
+ googleWaveUnit.variance.setup(0.0, 0.0, 1.0);
+ googleWaveUnit.frequency.setup(40.0, 200.0, 1000.0);
+
+ // Add an output so we can hear it.
+ synth.add(lineOut = new LineOut());
+
+ googleWaveUnit.output.connect(0, lineOut.input, 0);
+ googleWaveUnit.output.connect(0, lineOut.input, 1);
+
+ setupGUI();
+
+ // Start synthesizer using default stereo output at 44100 Hz.
+ synth.start();
+ // Start lineOut so it can pull data from other units.
+ lineOut.start();
+
+ // We only need to start the LineOut. It will pull data from the
+ // oscillator.
+ lineOut.start();
+
+ }
+
+ @Override
+ public void stop() {
+ scope.stop();
+ synth.stop();
+ }
+
+}
diff --git a/tests/com/jsyn/examples/SeeOscillators.java b/tests/com/jsyn/examples/SeeOscillators.java
new file mode 100644
index 0000000..b01e3a9
--- /dev/null
+++ b/tests/com/jsyn/examples/SeeOscillators.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.examples;
+
+import java.awt.BorderLayout;
+import java.awt.GridLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.util.ArrayList;
+
+import javax.swing.ButtonGroup;
+import javax.swing.JApplet;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JRadioButton;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import com.jsyn.scope.AudioScope;
+import com.jsyn.swing.JAppletFrame;
+import com.jsyn.swing.PortControllerFactory;
+import com.jsyn.unitgen.ImpulseOscillator;
+import com.jsyn.unitgen.ImpulseOscillatorBL;
+import com.jsyn.unitgen.LineOut;
+import com.jsyn.unitgen.LinearRamp;
+import com.jsyn.unitgen.Multiply;
+import com.jsyn.unitgen.PulseOscillator;
+import com.jsyn.unitgen.PulseOscillatorBL;
+import com.jsyn.unitgen.RedNoise;
+import com.jsyn.unitgen.SawtoothOscillator;
+import com.jsyn.unitgen.SawtoothOscillatorBL;
+import com.jsyn.unitgen.SawtoothOscillatorDPW;
+import com.jsyn.unitgen.SineOscillator;
+import com.jsyn.unitgen.SquareOscillator;
+import com.jsyn.unitgen.SquareOscillatorBL;
+import com.jsyn.unitgen.TriangleOscillator;
+import com.jsyn.unitgen.UnitOscillator;
+
+/**
+ * Display each oscillators waveform using the AudioScope. This is a reimplementation of the
+ * TJ_SeeOsc Applet from the old API.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class SeeOscillators extends JApplet {
+ private static final long serialVersionUID = -8315903842197137926L;
+ private Synthesizer synth;
+ private ArrayList<UnitOscillator> oscillators = new ArrayList<UnitOscillator>();
+ private LineOut lineOut;
+ private AudioScope scope;
+ private JPanel oscPanel;
+ private Multiply oscGain;
+ private ButtonGroup buttonGroup;
+ private LinearRamp freqRamp;
+
+ /* Can be run as either an application or as an applet. */
+ public static void main(String args[]) {
+ SeeOscillators applet = new SeeOscillators();
+ JAppletFrame frame = new JAppletFrame("ShowWaves", applet);
+ frame.setSize(640, 500);
+ frame.setVisible(true);
+ frame.test();
+ frame.validate();
+ }
+
+ private void setupGUI() {
+ setLayout(new BorderLayout());
+
+ add(BorderLayout.NORTH, new JLabel("Show Oscillators in an AudioScope"));
+
+ scope = new AudioScope(synth);
+ scope.addProbe(oscGain.output);
+ scope.setTriggerMode(AudioScope.TriggerMode.NORMAL);
+ // scope.getModel().getTriggerModel().getLevelModel().setDoubleValue( 0.0001 );
+ // Turn off the gain and trigger control GUI.
+ scope.getView().setShowControls(false);
+ scope.start();
+ add(BorderLayout.CENTER, scope.getView());
+
+ JPanel southPanel = new JPanel();
+ southPanel.setLayout(new GridLayout(0, 1));
+ add(BorderLayout.SOUTH, southPanel);
+
+ oscPanel = new JPanel();
+ oscPanel.setLayout(new GridLayout(2, 5));
+ southPanel.add(oscPanel);
+
+ southPanel.add(PortControllerFactory.createExponentialPortSlider(freqRamp.input));
+ southPanel.add(PortControllerFactory.createExponentialPortSlider(oscGain.inputB));
+
+ oscPanel.validate();
+ validate();
+ }
+
+ @Override
+ public void start() {
+ synth = JSyn.createSynthesizer();
+
+ // Use a multiplier for gain control and so we can hook up to the scope
+ // from a single unit.
+ synth.add(oscGain = new Multiply());
+ oscGain.inputB.setup(0.02, 0.5, 1.0);
+ oscGain.inputB.setName("Amplitude");
+
+ synth.add(freqRamp = new LinearRamp());
+ freqRamp.input.setup(50.0, 300.0, 20000.0);
+ freqRamp.input.setName("Frequency");
+ freqRamp.time.set(0.1);
+
+ // Add an output so we can hear the oscillators.
+ synth.add(lineOut = new LineOut());
+
+ oscGain.output.connect(lineOut.input);
+
+ setupGUI();
+
+ buttonGroup = new ButtonGroup();
+
+ addOscillator(new SineOscillator(), "Sine");
+ addOscillator(new TriangleOscillator(), "Triangle");
+ addOscillator(new SawtoothOscillator(), "Sawtooth");
+ addOscillator(new SawtoothOscillatorBL(), "SawBL");
+ addOscillator(new SawtoothOscillatorDPW(), "SawDPW");
+ addOscillator(new RedNoise(), "RedNoise");
+
+ addOscillator(new SquareOscillator(), "Square");
+ addOscillator(new SquareOscillatorBL(), "SquareBL");
+ addOscillator(new PulseOscillator(), "Pulse");
+ addOscillator(new PulseOscillatorBL(), "PulseBL");
+ addOscillator(new ImpulseOscillator(), "Impulse");
+ addOscillator(new ImpulseOscillatorBL(), "ImpulseBL");
+
+ // Start synthesizer using default stereo output at 44100 Hz.
+ synth.start();
+ // Start lineOut so it can pull data from other units.
+ lineOut.start();
+
+ // We only need to start the LineOut. It will pull data from the
+ // oscillator.
+ lineOut.start();
+
+ }
+
+ private void addOscillator(final UnitOscillator osc, String label) {
+ oscillators.add(osc);
+ synth.add(osc);
+ freqRamp.output.connect(osc.frequency);
+ osc.amplitude.set(1.0);
+ JRadioButton checkBox = new JRadioButton(label);
+ buttonGroup.add(checkBox);
+ checkBox.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent arg0) {
+ // Disconnect other oscillators.
+ oscGain.inputA.disconnectAll(0);
+ // Connect this one.
+ osc.output.connect(oscGain.inputA);
+ }
+ });
+ oscPanel.add(checkBox);
+ }
+
+ @Override
+ public void stop() {
+ scope.stop();
+ synth.stop();
+ }
+
+}
diff --git a/tests/com/jsyn/examples/ShowWaves.java b/tests/com/jsyn/examples/ShowWaves.java
new file mode 100644
index 0000000..b1dd215
--- /dev/null
+++ b/tests/com/jsyn/examples/ShowWaves.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.examples;
+
+import java.awt.BorderLayout;
+import java.util.ArrayList;
+
+import javax.swing.JApplet;
+import javax.swing.JLabel;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import com.jsyn.scope.AudioScope;
+import com.jsyn.swing.JAppletFrame;
+import com.jsyn.unitgen.Add;
+import com.jsyn.unitgen.LineOut;
+import com.jsyn.unitgen.SawtoothOscillatorBL;
+import com.jsyn.unitgen.SineOscillator;
+import com.jsyn.unitgen.TriangleOscillator;
+import com.jsyn.unitgen.UnitOscillator;
+
+/**
+ * Display waveforms using the AudioScope. The frequency of the oscillators is modulated by an LFO.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class ShowWaves extends JApplet {
+ private static final long serialVersionUID = -8315903842197137926L;
+ private Synthesizer synth;
+ private UnitOscillator lfo;
+ private Add adder;
+ private ArrayList<UnitOscillator> oscillators = new ArrayList<UnitOscillator>();
+ private LineOut lineOut;
+ private AudioScope scope;
+
+ /* Can be run as either an application or as an applet. */
+ public static void main(String args[]) {
+ ShowWaves applet = new ShowWaves();
+ JAppletFrame frame = new JAppletFrame("ShowWaves", applet);
+ frame.setSize(640, 300);
+ frame.setVisible(true);
+ frame.test();
+ }
+
+ private void setupGUI() {
+ setLayout(new BorderLayout());
+
+ add(BorderLayout.NORTH, new JLabel("ShowWaves in an AudioScope Mod001"));
+
+ scope = new AudioScope(synth);
+ for (UnitOscillator osc : oscillators) {
+ scope.addProbe(osc.output);
+ }
+ scope.setTriggerMode(AudioScope.TriggerMode.NORMAL);
+ scope.start();
+
+ // Turn on the gain and trigger control GUI.
+ scope.getView().setControlsVisible(true);
+ add(BorderLayout.CENTER, scope.getView());
+ validate();
+ }
+
+ @Override
+ public void start() {
+ synth = JSyn.createSynthesizer();
+
+ // Add an LFO.
+ synth.add(lfo = new SineOscillator());
+ synth.add(adder = new Add());
+
+ // Add an output so we can hear the oscillators.
+ synth.add(lineOut = new LineOut());
+
+ lfo.frequency.set(0.1);
+ lfo.amplitude.set(200.0);
+ adder.inputB.set(400.0);
+ lfo.output.connect(adder.inputA);
+
+ oscillators.add(new SawtoothOscillatorBL());
+ oscillators.add(new SineOscillator());
+ oscillators.add(new TriangleOscillator());
+ for (UnitOscillator osc : oscillators) {
+ synth.add(osc);
+ adder.output.connect(osc.frequency);
+ osc.output.connect(0, lineOut.input, 0);
+ osc.amplitude.set(0.2);
+ }
+
+ // Start synthesizer using default stereo output at 44100 Hz.
+ synth.start();
+ // Start lineOut so it can pull data from other units.
+ lineOut.start();
+ setupGUI();
+
+ // We only need to start the LineOut. It will pull data from the
+ // oscillator.
+ lineOut.start();
+
+ }
+
+ @Override
+ public void stop() {
+ scope.stop();
+ synth.stop();
+ }
+
+}
diff --git a/tests/com/jsyn/examples/SwarmOfOscillators.java b/tests/com/jsyn/examples/SwarmOfOscillators.java
new file mode 100644
index 0000000..9f7c19c
--- /dev/null
+++ b/tests/com/jsyn/examples/SwarmOfOscillators.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.examples;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import com.jsyn.ports.UnitOutputPort;
+import com.jsyn.unitgen.Add;
+import com.jsyn.unitgen.AsymptoticRamp;
+import com.jsyn.unitgen.Circuit;
+import com.jsyn.unitgen.LineOut;
+import com.jsyn.unitgen.Pan;
+import com.jsyn.unitgen.SawtoothOscillatorDPW;
+import com.jsyn.unitgen.SineOscillator;
+import com.jsyn.unitgen.UnitOscillator;
+import com.jsyn.unitgen.UnitSource;
+
+/**
+ * Make a bunch of oscillators that swarm around a moving frequency.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ */
+public class SwarmOfOscillators {
+ private Synthesizer synth;
+ Follower[] followers;
+ SineOscillator lfo;
+ LineOut lineOut;
+ private Add tiePoint;
+ private static final int NUM_FOLLOWERS = 30;
+
+ class Follower extends Circuit implements UnitSource {
+ UnitOscillator osc;
+ AsymptoticRamp lag;
+ Pan panner;
+
+ Follower() {
+ // Add a tone generator.
+ add(osc = new SawtoothOscillatorDPW());
+ osc.amplitude.set(0.03);
+
+ // Use a lag to smoothly change frequency.
+ add(lag = new AsymptoticRamp());
+ double hlife = 0.01 + (Math.random() * 0.9);
+ lag.halfLife.set(hlife);
+
+ // Set left/right pan randomly between -1.0 and +1.0.
+ add(panner = new Pan());
+ panner.pan.set((Math.random() * 2.0) - 1.0);
+
+ // Track the frequency coming through the tiePoint.
+ tiePoint.output.connect(lag.input);
+ // Add the LFO offset.
+ lfo.output.connect(lag.input);
+
+ lag.output.connect(osc.frequency);
+
+ // Connect the oscillator to the left and right audio output.
+ osc.output.connect(panner.input);
+ }
+
+ @Override
+ public UnitOutputPort getOutput() {
+ return panner.output;
+ }
+ }
+
+ private void test() {
+ synth = JSyn.createSynthesizer();
+
+ // Add an output mixer.
+ synth.add(lineOut = new LineOut());
+
+ // Add a unit just to distribute the control frequency.
+ synth.add(tiePoint = new Add());
+ synth.add(lfo = new SineOscillator());
+ lfo.amplitude.set(40.0);
+ lfo.frequency.set(2.3);
+
+ followers = new Follower[NUM_FOLLOWERS];
+ for (int i = 0; i < followers.length; i++) {
+ Follower follower = new Follower();
+ synth.add(follower);
+
+ // Every follower can connect directly to the lineOut because all input ports are
+ // mixers.
+ follower.getOutput().connect(0, lineOut.input, 0);
+ follower.getOutput().connect(1, lineOut.input, 1);
+
+ followers[i] = follower;
+ }
+
+ // Start synthesizer using default stereo output at 44100 Hz.
+ synth.start();
+ // We only need to start the LineOut. It will pull data from the
+ // oscillator.
+ lineOut.start();
+
+ // Get synthesizer time in seconds.
+ double timeNow = synth.getCurrentTime();
+
+ // Advance to a near future time so we have a clean start.
+ double duration = 0.9;
+ double time = timeNow + duration;
+ double freq = 400.0; // hertz
+ tiePoint.inputA.set(freq, time);
+
+ // Randomly change the target frequency for the followers.
+ try {
+ for (int i = 0; i < 20; i++) {
+ // Schedule this to happen a bit later.
+ time += duration;
+ freq = 200.0 + (Math.random() * 500.0);
+ tiePoint.inputA.set(freq, time);
+
+ // Sleep while the sound is being generated in the background
+ // thread.
+ synth.sleepUntil(time - 0.2);
+ }
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+
+ System.out.format("CPU usage = %4.2f%c\n", synth.getUsage() * 100, '%');
+
+ // Stop everything.
+ synth.stop();
+ }
+
+ public static void main(String[] args) {
+ new SwarmOfOscillators().test();
+ }
+}
diff --git a/tests/com/jsyn/examples/UseMidiKeyboard.java b/tests/com/jsyn/examples/UseMidiKeyboard.java
new file mode 100644
index 0000000..196f13c
--- /dev/null
+++ b/tests/com/jsyn/examples/UseMidiKeyboard.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.examples;
+
+import java.io.IOException;
+
+import javax.sound.midi.MidiDevice;
+import javax.sound.midi.MidiMessage;
+import javax.sound.midi.MidiUnavailableException;
+import javax.sound.midi.Receiver;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import com.jsyn.devices.javasound.MidiDeviceTools;
+import com.jsyn.instruments.SubtractiveSynthVoice;
+import com.jsyn.midi.MessageParser;
+import com.jsyn.midi.MidiConstants;
+import com.jsyn.unitgen.LineOut;
+import com.jsyn.unitgen.PowerOfTwo;
+import com.jsyn.unitgen.SineOscillator;
+import com.jsyn.unitgen.UnitOscillator;
+import com.jsyn.util.VoiceAllocator;
+import com.softsynth.shared.time.TimeStamp;
+
+/**
+ * Connect a USB MIDI Keyboard to the internal MIDI Synthesizer using JavaSound.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class UseMidiKeyboard {
+ private static final int MAX_VOICES = 8;
+ private Synthesizer synth;
+ private VoiceAllocator allocator;
+ private LineOut lineOut;
+ private double vibratoRate = 5.0;
+ private double vibratoDepth = 0.0;
+
+ private UnitOscillator lfo;
+ private PowerOfTwo powerOfTwo;
+ private MessageParser messageParser;
+ private SubtractiveSynthVoice[] voices;
+
+ public static void main(String[] args) {
+ UseMidiKeyboard app = new UseMidiKeyboard();
+ try {
+ app.test();
+ } catch (MidiUnavailableException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+
+ // Write a Receiver to get the messages from a Transmitter.
+ class CustomReceiver implements Receiver {
+ @Override
+ public void close() {
+ System.out.print("Closed.");
+ }
+
+ @Override
+ public void send(MidiMessage message, long timeStamp) {
+ byte[] bytes = message.getMessage();
+ messageParser.parse(bytes);
+ }
+ }
+
+ public int test() throws MidiUnavailableException, IOException, InterruptedException {
+ setupSynth();
+
+ messageParser = new MyParser();
+
+ int result = 2;
+ MidiDevice keyboard = MidiDeviceTools.findKeyboard();
+ Receiver receiver = new CustomReceiver();
+ // Just use default synthesizer.
+ if (keyboard != null) {
+ // If you forget to open them you will hear no sound.
+ keyboard.open();
+ // Put the receiver in the transmitter.
+ // This gives fairly low latency playing.
+ keyboard.getTransmitter().setReceiver(receiver);
+ System.out.println("Play MIDI keyboard: " + keyboard.getDeviceInfo().getDescription());
+ result = 0;
+ } else {
+ System.out.println("Could not find a keyboard.");
+ }
+ return result;
+ }
+
+ class MyParser extends MessageParser {
+ @Override
+ public void controlChange(int channel, int index, int value) {
+ // Mod Wheel
+ if (index == 1) {
+ vibratoDepth = 0.1 * value / 128.0;
+ // System.out.println( "vibratoDepth = " + vibratoDepth );
+ lfo.amplitude.set(vibratoDepth);
+ }
+ // 102 is the index of the first knob on my Axiom 25
+ else if (index == 102) {
+ final double bump = 0.95;
+ if (value < 64) {
+ vibratoRate *= bump;
+ } else {
+ vibratoRate *= 1.0 / bump;
+ }
+ System.out.println("vibratoRate = " + vibratoRate);
+ lfo.frequency.set(vibratoRate);
+ }
+
+ }
+
+ @Override
+ public void noteOff(int channel, int noteNumber, int velocity) {
+ allocator.noteOff(noteNumber, synth.createTimeStamp());
+ }
+
+ @Override
+ public void noteOn(int channel, int noteNumber, int velocity) {
+ double frequency = convertPitchToFrequency(noteNumber);
+ double amplitude = velocity / (4 * 128.0);
+ TimeStamp timeStamp = synth.createTimeStamp();
+ allocator.noteOn(noteNumber, frequency, amplitude, timeStamp);
+ }
+
+ @Override
+ public void pitchBend(int channel, int bend) {
+ double fraction = (bend - MidiConstants.PITCH_BEND_CENTER)
+ / ((double) MidiConstants.PITCH_BEND_CENTER);
+ System.out.println("bend = " + bend + ", fraction = " + fraction);
+ }
+ }
+
+ /**
+ * Calculate frequency in Hertz based on MIDI pitch. Middle C is 60.0. You can use fractional
+ * pitches so 60.5 would give you a pitch half way between C and C#.
+ */
+ double convertPitchToFrequency(double pitch) {
+ final double concertA = 440.0;
+ return concertA * Math.pow(2.0, ((pitch - 69) * (1.0 / 12.0)));
+ }
+
+ private void setupSynth() {
+ synth = JSyn.createSynthesizer();
+
+ // Add an output.
+ synth.add(lineOut = new LineOut());
+
+ synth.add(powerOfTwo = new PowerOfTwo());
+ synth.add(lfo = new SineOscillator());
+ // Sums pitch modulation.
+ lfo.output.connect(powerOfTwo.input);
+ lfo.amplitude.set(vibratoDepth);
+ lfo.frequency.set(vibratoRate);
+
+ voices = new SubtractiveSynthVoice[MAX_VOICES];
+ for (int i = 0; i < MAX_VOICES; i++) {
+ SubtractiveSynthVoice voice = new SubtractiveSynthVoice();
+ synth.add(voice);
+ powerOfTwo.output.connect(voice.pitchModulation);
+ voice.getOutput().connect(0, lineOut.input, 0);
+ voice.getOutput().connect(0, lineOut.input, 1);
+ voices[i] = voice;
+ }
+ allocator = new VoiceAllocator(voices);
+
+ // Start synthesizer using default stereo output at 44100 Hz.
+ synth.start();
+ // We only need to start the LineOut. It will pull data from the
+ // oscillator.
+ lineOut.start();
+
+ // Get synthesizer time in seconds.
+ double timeNow = synth.getCurrentTime();
+
+ // Advance to a near future time so we have a clean start.
+ double time = timeNow + 0.5;
+
+ }
+
+}
diff --git a/tests/com/jsyn/examples/WindCircuit.java b/tests/com/jsyn/examples/WindCircuit.java
new file mode 100644
index 0000000..1e3623e
--- /dev/null
+++ b/tests/com/jsyn/examples/WindCircuit.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 1997 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.examples;
+
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.ports.UnitOutputPort;
+import com.jsyn.unitgen.Circuit;
+import com.jsyn.unitgen.FilterStateVariable;
+import com.jsyn.unitgen.MultiplyAdd;
+import com.jsyn.unitgen.RedNoise;
+import com.jsyn.unitgen.UnitSource;
+import com.jsyn.unitgen.WhiteNoise;
+
+/**
+ * Wind Sound Create a wind-like sound by feeding white noise "shshshshsh" through a randomly
+ * varying state filter to make a "whooowhoosh" sound. The cuttoff frequency of the low pass filter
+ * is controlled by a RedNoise unit which creates a slowly varying random control signal.
+ *
+ * @author (C) 1997 Phil Burk, SoftSynth.com
+ */
+
+public class WindCircuit extends Circuit implements UnitSource {
+ /* Declare units that will be part of the circuit. */
+ WhiteNoise myNoise;
+ FilterStateVariable myFilter;
+ RedNoise myLFO;
+ MultiplyAdd myScalar;
+
+ /* Declare ports. */
+ public UnitInputPort noiseAmp;
+ public UnitInputPort modRate;
+ public UnitInputPort modDepth;
+ public UnitInputPort cutoff;
+ public UnitInputPort resonance;
+ public UnitInputPort amplitude;
+ public UnitOutputPort output;
+
+ public WindCircuit() {
+ /*
+ * Create various unit generators and add them to circuit.
+ */
+ add(myNoise = new WhiteNoise());
+ add(myFilter = new FilterStateVariable());
+ add(myLFO = new RedNoise());
+ add(myScalar = new MultiplyAdd());
+
+ /* Make ports on internal units appear as ports on circuit. */
+ /* Optionally give some circuit ports more meaningful names. */
+ addPort(noiseAmp = myNoise.amplitude, "NoiseAmp");
+ addPort(modRate = myLFO.frequency, "ModRate");
+ addPort(modDepth = myScalar.inputB, "ModDepth");
+ addPort(cutoff = myScalar.inputC, "Cutoff");
+ addPort(resonance = myFilter.resonance);
+ addPort(amplitude = myFilter.amplitude);
+ addPort(output = myFilter.output);
+
+ /* Connect SynthUnits to make control signal path. */
+ myLFO.output.connect(myScalar.inputA);
+ myScalar.output.connect(myFilter.frequency);
+ /* Connect SynthUnits to make audio signal path. */
+ myNoise.output.connect(myFilter.input);
+
+ /* Set ports to useful values and ranges. */
+ noiseAmp.setup(0.0, 0.3, 0.4);
+ modRate.setup(0.0, 1.0, 10.0);
+ modDepth.setup(0.0, 300.0, 1000.0);
+ cutoff.setup(0.0, 600.0, 1000.0);
+ resonance.setup(0.0, 0.066, 0.2);
+ amplitude.setup(0.0, 0.9, 0.999);
+ }
+
+ @Override
+ public UnitOutputPort getOutput() {
+ return output;
+ }
+}
diff --git a/tests/com/jsyn/ports/TestQueuedDataPort.java b/tests/com/jsyn/ports/TestQueuedDataPort.java
new file mode 100644
index 0000000..8c4714b
--- /dev/null
+++ b/tests/com/jsyn/ports/TestQueuedDataPort.java
@@ -0,0 +1,492 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.ports;
+
+import junit.framework.TestCase;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import com.jsyn.data.FloatSample;
+import com.jsyn.data.SequentialData;
+import com.jsyn.data.ShortSample;
+import com.jsyn.unitgen.FixedRateMonoReader;
+
+/**
+ * Test sample and envelope queuing and looping.
+ *
+ * @author Phil Burk, (C) 2009 Mobileer Inc
+ */
+public class TestQueuedDataPort extends TestCase {
+ Synthesizer synth;
+ float[] floatData = {
+ 0.0f, 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f
+ };
+ FloatSample floatSample;
+ FixedRateMonoReader reader;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ synth = JSyn.createSynthesizer();
+ synth.setRealTime(false);
+ synth.start();
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ synth.stop();
+ super.tearDown();
+ }
+
+ private void queueDirect(UnitDataQueuePort port, SequentialData data, int startFrame,
+ int numFrames) {
+ queueDirect(port, data, startFrame, numFrames, 0);
+ }
+
+ private void queueDirect(UnitDataQueuePort port, SequentialData data, int startFrame,
+ int numFrames, int numLoops) {
+ QueueDataCommand command = port.createQueueDataCommand(data, startFrame, numFrames);
+ command.setNumLoops(numLoops);
+ port.addQueuedBlock(command);
+ }
+
+ public void testQueueSingleShort() {
+ short[] data = {
+ 234, -9876, 4567
+ };
+ ShortSample sample = new ShortSample(data.length, 1);
+ sample.write(data);
+
+ UnitDataQueuePort dataQueue = new UnitDataQueuePort("test");
+ assertEquals("start empty", false, dataQueue.hasMore());
+
+ queueDirect(dataQueue, sample, 0, data.length);
+ checkQueuedData(data, dataQueue, 0, data.length);
+
+ assertEquals("end empty", false, dataQueue.hasMore());
+ }
+
+ public void testQueueSingleFloat() {
+ float[] data = {
+ 0.4f, 1.9f, 22.7f
+ };
+ FloatSample sample = new FloatSample(data.length, 1);
+ sample.write(data);
+
+ UnitDataQueuePort dataQueue = new UnitDataQueuePort("test");
+ assertEquals("start empty", false, dataQueue.hasMore());
+
+ queueDirect(dataQueue, sample, 0, data.length);
+ checkQueuedData(data, dataQueue, 0, data.length);
+
+ assertEquals("end empty", false, dataQueue.hasMore());
+ }
+
+ public void testQueueMultiple() {
+ short[] data = {
+ 234, 17777, -9876, 4567, -14287
+ };
+ ShortSample sample = new ShortSample(data.length, 1);
+ sample.write(data);
+
+ UnitDataQueuePort dataQueue = new UnitDataQueuePort("test");
+ assertEquals("start empty", false, dataQueue.hasMore());
+
+ queueDirect(dataQueue, sample, 1, 3);
+ queueDirect(dataQueue, sample, 0, 5);
+ queueDirect(dataQueue, sample, 2, 2);
+
+ checkQueuedData(data, dataQueue, 1, 3);
+ checkQueuedData(data, dataQueue, 0, 5);
+ checkQueuedData(data, dataQueue, 2, 2);
+
+ assertEquals("end empty", false, dataQueue.hasMore());
+ }
+
+ public void testQueueNoLoops() throws InterruptedException {
+ System.out.println("testQueueNoLoops() ================");
+ UnitDataQueuePort dataQueue = setupFloatSample();
+
+ dataQueue.queueOn(floatSample, synth.createTimeStamp());
+ // Advance synth so that the queue command propagates to the engine.
+ synth.sleepUntil(synth.getCurrentTime() + 0.01);
+
+ // play entire sample
+ checkQueuedData(floatData, dataQueue, 0, floatData.length);
+
+ assertEquals("end empty", false, dataQueue.hasMore());
+ }
+
+ public void testQueueLoopForever() throws InterruptedException {
+ System.out.println("testQueueLoopForever() ================");
+
+ UnitDataQueuePort dataQueue = setupFloatSample();
+
+ dataQueue.queue(floatSample, 0, 3);
+ dataQueue.queueLoop(floatSample, 3, 4);
+
+ // Advance synth so that the queue commands propagate to the engine.
+ synth.sleepUntil(synth.getCurrentTime() + 0.01);
+
+ checkQueuedData(floatData, dataQueue, 0, 3);
+ checkQueuedData(floatData, dataQueue, 3, 4);
+ checkQueuedData(floatData, dataQueue, 3, 4);
+ checkQueuedData(floatData, dataQueue, 3, 4);
+ checkQueuedData(floatData, dataQueue, 3, 1);
+
+ // queue final release
+ dataQueue.queue(floatSample, 3, 5);
+ synth.sleepUntil(synth.getCurrentTime() + 0.01);
+ // current loop will finish
+ checkQueuedData(floatData, dataQueue, 4, 3);
+ // release portion will play
+ checkQueuedData(floatData, dataQueue, 3, 5);
+
+ assertEquals("end empty", false, dataQueue.hasMore());
+ }
+
+ public void testQueueLoopAtLeastOnce() throws InterruptedException {
+ System.out.println("testQueueLoopAtLeastOnce() ================");
+
+ UnitDataQueuePort dataQueue = setupFloatSample();
+
+ dataQueue.queue(floatSample, 0, 3);
+ dataQueue.queueLoop(floatSample, 3, 2); // this should play at least once
+ dataQueue.queue(floatSample, 5, 2);
+
+ // Advance synth so that the queue commands propagate to the engine.
+ synth.sleepUntil(synth.getCurrentTime() + 0.01);
+
+ checkQueuedData(floatData, dataQueue, 0, 3);
+ checkQueuedData(floatData, dataQueue, 3, 2);
+ checkQueuedData(floatData, dataQueue, 5, 2);
+
+ assertEquals("end empty", false, dataQueue.hasMore());
+ }
+
+ public void testQueueNumLoops() throws InterruptedException {
+ System.out.println("testQueueNumLoops() ================");
+ UnitDataQueuePort dataQueue = setupFloatSample();
+
+ dataQueue.queue(floatSample, 0, 2);
+
+ int numLoopsA = 5;
+ dataQueue.queueLoop(floatSample, 2, 3, numLoopsA);
+
+ dataQueue.queue(floatSample, 4, 2);
+
+ int numLoopsB = 3;
+ dataQueue.queueLoop(floatSample, 3, 4, numLoopsB);
+
+ dataQueue.queue(floatSample, 5, 2);
+
+ // Advance synth so that the queue commands propagate to the engine.
+ synth.sleepUntil(synth.getCurrentTime() + 0.01);
+
+ checkQueuedData(floatData, dataQueue, 0, 2);
+ for (int i = 0; i < (numLoopsA + 1); i++) {
+ System.out.println("loop A #" + i);
+ checkQueuedData(floatData, dataQueue, 2, 3);
+ }
+ checkQueuedData(floatData, dataQueue, 4, 2);
+ for (int i = 0; i < (numLoopsB + 1); i++) {
+ System.out.println("loop B #" + i);
+ checkQueuedData(floatData, dataQueue, 3, 4);
+ }
+
+ checkQueuedData(floatData, dataQueue, 5, 2);
+
+ assertEquals("end empty", false, dataQueue.hasMore());
+ }
+
+ private UnitDataQueuePort setupFloatSample() {
+ floatSample = new FloatSample(floatData.length, 1);
+ floatSample.write(floatData);
+
+ synth.add(reader = new FixedRateMonoReader());
+ UnitDataQueuePort dataQueue = reader.dataQueue;
+ assertEquals("start empty", false, dataQueue.hasMore());
+ return dataQueue;
+ }
+
+ public void testQueueSustainLoop() throws InterruptedException {
+ System.out.println("testQueueSustainLoop() ================");
+
+ UnitDataQueuePort dataQueue = setupFloatSample();
+
+ // set up sustain loops ===========================
+ floatSample.setSustainBegin(2);
+ floatSample.setSustainEnd(4);
+ floatSample.setReleaseBegin(-1);
+ floatSample.setReleaseEnd(-1);
+
+ dataQueue.queueOn(floatSample, synth.createTimeStamp());
+ // Advance synth so that the queue command propagates to the engine.
+ synth.sleepUntil(synth.getCurrentTime() + 0.01);
+
+ checkQueuedData(floatData, dataQueue, 0, 2);
+ checkQueuedData(floatData, dataQueue, 2, 2);
+ checkQueuedData(floatData, dataQueue, 2, 2);
+ checkQueuedData(floatData, dataQueue, 2, 1); // looping
+
+ dataQueue.queueOff(floatSample, true); // queue off in middle of loop
+ synth.sleepUntil(synth.getCurrentTime() + 0.01);
+
+ checkQueuedData(floatData, dataQueue, 3, 5); // release
+ assertEquals("end empty", false, dataQueue.hasMore());
+ }
+
+ public void testQueueReleaseLoop() throws InterruptedException {
+ System.out.println("testQueueReleaseLoop() ================");
+ UnitDataQueuePort dataQueue = setupFloatSample();
+
+ // set up sustain loops ===========================
+ floatSample.setSustainBegin(-1);
+ floatSample.setSustainEnd(-1);
+ floatSample.setReleaseBegin(4);
+ floatSample.setReleaseEnd(6);
+
+ dataQueue.queueOn(floatSample, synth.createTimeStamp());
+ // Advance synth so that the queue command propagates to the engine.
+ synth.sleepUntil(synth.getCurrentTime() + 0.01);
+
+ checkQueuedData(floatData, dataQueue, 0, 4);
+ checkQueuedData(floatData, dataQueue, 4, 2);
+ checkQueuedData(floatData, dataQueue, 4, 2);
+ checkQueuedData(floatData, dataQueue, 4, 2); // looping in release cuz no
+ // sustain loop
+
+ dataQueue.queueOff(floatSample, true); // queue off in middle of loop
+ synth.sleepUntil(synth.getCurrentTime() + 0.01);
+
+ checkQueuedData(floatData, dataQueue, 4, 2);
+ checkQueuedData(floatData, dataQueue, 4, 2); // still looping
+ assertEquals("end full", true, dataQueue.hasMore());
+ }
+
+ public void testQueueSustainReleaseLoops() throws InterruptedException {
+ System.out.println("testQueueSustainReleaseLoops() ================");
+ UnitDataQueuePort dataQueue = setupFloatSample();
+
+ // set up sustain loops ===========================
+ floatSample.setSustainBegin(2);
+ floatSample.setSustainEnd(4);
+ floatSample.setReleaseBegin(5);
+ floatSample.setReleaseEnd(7);
+
+ dataQueue.queueOn(floatSample, synth.createTimeStamp());
+ // Advance synth so that the queue command propagates to the engine.
+ synth.sleepUntil(synth.getCurrentTime() + 0.01);
+
+ checkQueuedData(floatData, dataQueue, 0, 4);
+ checkQueuedData(floatData, dataQueue, 2, 2);
+ checkQueuedData(floatData, dataQueue, 2, 1); // middle of sustain loop
+
+ dataQueue.queueOff(floatSample, true); // queue off in middle of loop
+ synth.sleepUntil(synth.getCurrentTime() + 0.01);
+
+ checkQueuedData(floatData, dataQueue, 3, 2);
+ checkQueuedData(floatData, dataQueue, 5, 2); // release loop
+ checkQueuedData(floatData, dataQueue, 5, 2); // release loop
+ assertEquals("end full", true, dataQueue.hasMore());
+ }
+
+ private void checkQueuedData(short[] data, UnitDataQueuePort dataQueue, int offset,
+ int numFrames) {
+ for (int i = 0; i < numFrames; i++) {
+ assertEquals("got data", true, dataQueue.hasMore());
+ double value = dataQueue.readNextMonoDouble(synth.getFramePeriod());
+ assertEquals("data matches", data[i + offset] / 32768.0, value, 0.0001);
+ }
+ }
+
+ private void checkQueuedData(float[] data, UnitDataQueuePort dataQueue, int offset,
+ int numFrames) {
+ for (int i = 0; i < numFrames; i++) {
+ assertEquals("got data", true, dataQueue.hasMore());
+ double value = dataQueue.readNextMonoDouble(synth.getFramePeriod());
+ assertEquals("data matches", data[i + offset], value, 0.0001);
+ }
+ }
+
+ class TestQueueCallback implements UnitDataQueueCallback {
+ boolean gotStarted = false;
+ boolean gotLooped = false;
+ boolean gotFinished = false;
+ QueueDataEvent lastEvent;
+
+ @Override
+ public void started(QueueDataEvent event) {
+ System.out.println("Callback started.");
+ gotStarted = true;
+ lastEvent = event;
+ }
+
+ @Override
+ public void looped(QueueDataEvent event) {
+ System.out.println("Callback looped.");
+ gotLooped = true;
+ lastEvent = event;
+ }
+
+ @Override
+ public void finished(QueueDataEvent event) {
+ System.out.println("Callback finished.");
+ gotFinished = true;
+ lastEvent = event;
+ }
+ }
+
+ public void testQueueCallback() {
+ float[] data = {
+ 0.2f, -8.9f, 2.7f
+ };
+ FloatSample sample = new FloatSample(data.length, 1);
+ sample.write(data);
+
+ UnitDataQueuePort dataQueue = new UnitDataQueuePort("test");
+ assertEquals("start empty", false, dataQueue.hasMore());
+
+ // Create an object to be called when the queued data is done.
+ TestQueueCallback callback = new TestQueueCallback();
+
+ QueueDataCommand command = dataQueue.createQueueDataCommand(sample, 0, data.length);
+ command.setCallback(callback);
+ command.setNumLoops(2);
+ dataQueue.addQueuedBlock(command);
+
+ // Check to see if flags get set true by callback.
+ dataQueue.firePendingCallbacks();
+ assertEquals("not started yet", false, callback.gotStarted);
+ assertEquals("not looped yet", false, callback.gotLooped);
+ assertEquals("not finished yet", false, callback.gotFinished);
+
+ checkQueuedData(data, dataQueue, 0, 1);
+ dataQueue.firePendingCallbacks();
+ assertEquals("should be started now", true, callback.gotStarted);
+ assertEquals("not looped yet", false, callback.gotLooped);
+ assertEquals("not finished yet", false, callback.gotFinished);
+ assertEquals("check source of event", dataQueue, callback.lastEvent.getSource());
+ assertEquals("check sample", sample, callback.lastEvent.getSequentialData());
+ assertEquals("check loopCount", 2, callback.lastEvent.getLoopsLeft());
+
+ checkQueuedData(data, dataQueue, 1, data.length - 1);
+ dataQueue.firePendingCallbacks();
+ assertEquals("should be looped now", true, callback.gotLooped);
+ assertEquals("check loopCount", 1, callback.lastEvent.getLoopsLeft());
+ assertEquals("not finished yet", false, callback.gotFinished);
+
+ checkQueuedData(data, dataQueue, 0, data.length);
+ dataQueue.firePendingCallbacks();
+ assertEquals("check loopCount", 0, callback.lastEvent.getLoopsLeft());
+
+ checkQueuedData(data, dataQueue, 0, data.length);
+ dataQueue.firePendingCallbacks();
+ assertEquals("should be finished now", true, callback.gotFinished);
+
+ assertEquals("end empty", false, dataQueue.hasMore());
+ }
+
+ public void testImmediate() {
+ float[] data = {
+ 0.0f, 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f, 10.0f, 11.0f
+ };
+ FloatSample sample = new FloatSample(data.length, 1);
+ sample.write(data);
+
+ UnitDataQueuePort dataQueue = new UnitDataQueuePort("test");
+ dataQueue.queue(sample);
+
+ // Only play some of the data then interrupt it with an immediate block.
+ checkQueuedData(data, dataQueue, 0, 3);
+
+ QueueDataCommand command = dataQueue.createQueueDataCommand(sample, 7, 3);
+ command.setImmediate(true);
+ command.run(); // execute "immediate" operation and add to block list
+
+ // Should already be in new data.
+ checkQueuedData(data, dataQueue, 7, 3);
+ }
+
+ public void testCrossFade() {
+ float[] data1 = {
+ 0.0f, 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f
+ };
+ float[] data2 = {
+ 20.0f, 19.0f, 18.0f, 17.0f, 16.0f, 15.0f, 14.0f, 13.0f, 12.0f, 11.0f
+ };
+ FloatSample sample1 = new FloatSample(data1);
+ FloatSample sample2 = new FloatSample(data2);
+
+ UnitDataQueuePort dataQueue = new UnitDataQueuePort("test");
+ dataQueue.queue(sample1, 0, 4);
+
+ QueueDataCommand command = dataQueue.createQueueDataCommand(sample2, 1, 8);
+ command.setCrossFadeIn(3);
+ command.run(); // execute "immediate" operation and add to block list
+
+ // Only play some of the data then crossfade to another sample.
+ checkQueuedData(data1, dataQueue, 0, 4);
+
+ for (int i = 0; i < 3; i++) {
+ double factor = i / 3.0;
+ double value = ((1.0 - factor) * data1[i + 4]) + (factor * data2[i + 1]);
+ System.out.println("i = " + i + ", factor = " + factor + ", value = " + value);
+
+ double actual = dataQueue.readNextMonoDouble(synth.getFramePeriod());
+ assertEquals("crossfade " + i, value, actual, 0.00001);
+ }
+
+ // Should already be in new data.
+ checkQueuedData(data2, dataQueue, 4, 5);
+ }
+
+ public void testImmediateCrossFade() {
+ float[] data1 = {
+ 0.0f, 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f
+ };
+ float[] data2 = {
+ 20.0f, 19.0f, 18.0f, 17.0f, 16.0f, 15.0f, 14.0f, 13.0f, 12.0f, 11.0f
+ };
+ FloatSample sample1 = new FloatSample(data1);
+ FloatSample sample2 = new FloatSample(data2);
+
+ UnitDataQueuePort dataQueue = new UnitDataQueuePort("test");
+ dataQueue.queue(sample1, 0, 4);
+
+ // Only play some of the data then crossfade to another sample.
+ int beforeInterrupt = 2;
+ checkQueuedData(data1, dataQueue, 0, beforeInterrupt);
+
+ QueueDataCommand command = dataQueue.createQueueDataCommand(sample2, 1, 8);
+ command.setImmediate(true);
+ command.setCrossFadeIn(3);
+ command.run(); // execute "immediate" operation and add to block list
+
+ for (int i = 0; i < 3; i++) {
+ double factor = i / 3.0;
+ double value = ((1.0 - factor) * data1[i + beforeInterrupt]) + (factor * data2[i + 1]);
+ System.out.println("i = " + i + ", factor = " + factor + ", value = " + value);
+
+ double actual = dataQueue.readNextMonoDouble(synth.getFramePeriod());
+ assertEquals("crossfade " + i, value, actual, 0.00001);
+ }
+
+ // Should already be in new data.
+ checkQueuedData(data2, dataQueue, 4, 5);
+ }
+}
diff --git a/tests/com/jsyn/ports/TestSequentialData.java b/tests/com/jsyn/ports/TestSequentialData.java
new file mode 100644
index 0000000..1328c78
--- /dev/null
+++ b/tests/com/jsyn/ports/TestSequentialData.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.ports;
+
+import junit.framework.TestCase;
+
+import com.jsyn.data.FloatSample;
+
+public class TestSequentialData extends TestCase {
+
+ float[] data1 = {
+ 0.0f, 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f
+ };
+ FloatSample sample1;
+ float[] data2 = {
+ 20.0f, 19.0f, 18.0f, 17.0f, 16.0f, 15.0f, 14.0f, 13.0f, 12.0f, 11.0f
+ };
+ FloatSample sample2;
+
+ public void testCrossfade() {
+ sample1 = new FloatSample(data1);
+ sample2 = new FloatSample(data2);
+ SequentialDataCrossfade xfade = new SequentialDataCrossfade();
+ xfade.setup(sample1, 4, 3, sample2, 1, 6);
+
+ for (int i = 0; i < 3; i++) {
+ double factor = i / 3.0;
+ double value = ((1.0 - factor) * data1[i + 4]) + (factor * data2[i + 1]);
+ System.out.println("i = " + i + ", factor = " + factor + ", value = " + value);
+ assertEquals("crossfade " + i, value, xfade.readDouble(i), 0.00001);
+ }
+ for (int i = 3; i < 6; i++) {
+ assertEquals("crossfade " + i, sample2.readDouble(i + 1), xfade.readDouble(i), 0.00001);
+ }
+ }
+}
diff --git a/tests/com/jsyn/ports/TestSet.java b/tests/com/jsyn/ports/TestSet.java
new file mode 100644
index 0000000..8d1f3ea
--- /dev/null
+++ b/tests/com/jsyn/ports/TestSet.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.ports;
+
+import junit.framework.TestCase;
+
+import com.jsyn.engine.SynthesisEngine;
+import com.jsyn.unitgen.Minimum;
+
+public class TestSet extends TestCase {
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ /** Internal value setting. */
+ public void testSetValue() {
+ int numParts = 4;
+ UnitInputPort port = new UnitInputPort(numParts, "Tester");
+ port.setValueInternal(0, 100.0);
+ port.setValueInternal(2, 120.0);
+ port.setValueInternal(1, 110.0);
+ port.setValueInternal(3, 130.0);
+ assertEquals("check port value", 100.0, port.getValue(0));
+ assertEquals("check port value", 120.0, port.getValue(2));
+ assertEquals("check port value", 110.0, port.getValue(1));
+ assertEquals("check port value", 130.0, port.getValue(3));
+ }
+
+ public void testSet() throws InterruptedException {
+ SynthesisEngine synthesisEngine = new SynthesisEngine();
+ synthesisEngine.setRealTime(false);
+ synthesisEngine.start();
+ synthesisEngine.sleepUntil(0.01);
+ Minimum min;
+ synthesisEngine.add(min = new Minimum());
+
+ double x = 33.99;
+ double y = 8.31;
+ min.inputA.set(x);
+ min.inputB.set(y);
+ synthesisEngine.sleepFor(0.01);
+ assertEquals("min set A", x, min.inputA.getValue());
+ assertEquals("min set B", y, min.inputB.getValue());
+ min.start();
+ synthesisEngine.sleepFor(0.01);
+
+ assertEquals("min output", y, min.output.getValue());
+ synthesisEngine.stop();
+ }
+
+ /** if we use a port index out of range we want to know now and not blow up the engine. */
+ public void testSetBadPort() throws InterruptedException {
+ SynthesisEngine synthesisEngine = new SynthesisEngine();
+ synthesisEngine.setRealTime(false);
+ synthesisEngine.start();
+ Minimum min;
+ synthesisEngine.add(min = new Minimum());
+
+ min.start();
+ Exception caught = null;
+ try {
+ min.inputA.set(1, 23.45);
+ } catch (Exception e) {
+ caught = e;
+ }
+ assertTrue("Catch port out of range, caught " + caught,
+ (caught instanceof ArrayIndexOutOfBoundsException));
+
+ // Don't blow up here.
+ synthesisEngine.sleepUntil(0.01);
+
+ synthesisEngine.stop();
+ }
+
+}
diff --git a/tests/com/jsyn/research/BenchMultiThreading.java b/tests/com/jsyn/research/BenchMultiThreading.java
new file mode 100644
index 0000000..79b20bb
--- /dev/null
+++ b/tests/com/jsyn/research/BenchMultiThreading.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.research;
+
+import java.util.ArrayList;
+
+import junit.framework.TestCase;
+
+public class BenchMultiThreading extends TestCase {
+ private static final int FRAMES_PER_BLOCK = 64;
+ int numThreads = 4;
+ int numLoops = 100000;
+ private ArrayList<CustomThread> threadList;
+
+ class CustomThread extends Thread {
+ long frameCount = 0;
+ long desiredFrame = 0;
+ Object semaphore = new Object();
+ Object goSemaphore = new Object();
+ volatile boolean go = true;
+ long startNano;
+ long stopNano;
+ long maxElapsed;
+
+ @Override
+ public void run() {
+ try {
+ startNano = System.nanoTime();
+ while (go) {
+ // Watch for long delays.
+ stopNano = System.nanoTime();
+ long elapsed = stopNano - startNano;
+ startNano = System.nanoTime();
+ if (elapsed > maxElapsed) {
+ maxElapsed = elapsed;
+ }
+
+ synchronized (semaphore) {
+ // Audio synthesis would occur here.
+ frameCount += 1;
+ // System.out.println( this + " generating frame " +
+ // frameCount );
+ semaphore.notify();
+ }
+ synchronized (goSemaphore) {
+ while (desiredFrame <= frameCount) {
+ goSemaphore.wait();
+ }
+ }
+ long stopNano = System.nanoTime();
+ }
+ } catch (InterruptedException e) {
+ System.out.println("CustomThread interrupted. ");
+ }
+ System.out.println("Finishing " + this);
+ }
+
+ public void abort() {
+ go = false;
+ interrupt();
+ }
+
+ public void waitForFrame(long targetFrame) throws InterruptedException {
+ synchronized (semaphore) {
+ while (frameCount < targetFrame) {
+ semaphore.wait();
+ }
+ }
+ }
+
+ public void generateFrame(long desiredFrame) {
+ synchronized (goSemaphore) {
+ this.desiredFrame = desiredFrame;
+ goSemaphore.notify();
+ }
+ }
+
+ }
+
+ public void testMultiThreads() {
+ threadList = new ArrayList<CustomThread>();
+ for (int i = 0; i < numThreads; i++) {
+ CustomThread thread = new CustomThread();
+ threadList.add(thread);
+ thread.start();
+ }
+
+ long frameCount = 0;
+ long startTime = System.currentTimeMillis();
+ try {
+ for (int i = 0; i < numLoops; i++) {
+ frameCount += 1;
+ waitForThreads(frameCount);
+ // System.out.println("got frame " + frameCount );
+ }
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ long stopTime = System.currentTimeMillis();
+ long elapsedTime = stopTime - startTime;
+ double elapsedSeconds = 0.001 * elapsedTime;
+ double blocksPerSecond = numLoops / elapsedSeconds;
+ System.out.format("blocksPerSecond = %10.3f\n", blocksPerSecond);
+ double framesPerSecond = blocksPerSecond * FRAMES_PER_BLOCK;
+ System.out.format("audio framesPerSecond = %10.3f at %d frames per block\n",
+ framesPerSecond, FRAMES_PER_BLOCK);
+
+ for (CustomThread thread : threadList) {
+ System.out.format("max elapsed time is %d nanos or %f msec\n", thread.maxElapsed,
+ (thread.maxElapsed / 1000000.0));
+ }
+ for (CustomThread thread : threadList) {
+ assertEquals("BlockCount must match ", frameCount, thread.frameCount);
+ thread.abort();
+ }
+
+ }
+
+ private void waitForThreads(long frameCount) throws InterruptedException {
+ for (CustomThread thread : threadList) {
+ // Ask threads to wake up and generate up to this frame.
+ thread.generateFrame(frameCount);
+ }
+ for (CustomThread thread : threadList) {
+ // Wait for all the threads to catch up.
+ thread.waitForFrame(frameCount);
+ }
+ }
+}
diff --git a/tests/com/jsyn/research/RecordVariousRamps.java b/tests/com/jsyn/research/RecordVariousRamps.java
new file mode 100644
index 0000000..c90ea9a
--- /dev/null
+++ b/tests/com/jsyn/research/RecordVariousRamps.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright 2014 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/**
+ * Generate steps, linear ramps and smooth ramps.
+ *
+ * @author (C) 2014 Phil Burk
+ */
+
+package com.jsyn.research;
+
+import java.io.File;
+import java.io.IOException;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import com.jsyn.unitgen.ContinuousRamp;
+import com.jsyn.unitgen.LineOut;
+import com.jsyn.unitgen.LinearRamp;
+import com.jsyn.unitgen.Multiply;
+import com.jsyn.unitgen.PassThrough;
+import com.jsyn.unitgen.PowerOfTwo;
+import com.jsyn.unitgen.SawtoothOscillatorBL;
+import com.jsyn.unitgen.UnitFilter;
+import com.jsyn.unitgen.UnitOscillator;
+import com.jsyn.util.WaveRecorder;
+
+public class RecordVariousRamps {
+ private Synthesizer synth;
+ private UnitOscillator osc;
+ private Multiply multiplier;
+ private UnitFilter ramp;
+ private LinearRamp linearRamp;
+ private ContinuousRamp continuousRamp;
+ private LineOut lineOut;
+ private WaveRecorder recorder;
+ private PowerOfTwo powerOfTwo;
+ private static final int MODE_STEP = 0;
+ private static final int MODE_LINEAR = 1;
+ private static final int MODE_SMOOTH = 2;
+ private static final String[] modeNames = {
+ "step", "linear", "smooth"
+ };
+
+ private RampEvent[] rampData = {
+ new RampEvent(1.0, 1.5, 2.0), new RampEvent(-0.9, 0.5, 1.0),
+ new RampEvent(0.9, 0.5, 0.8), new RampEvent(-0.3, 0.5, 0.8),
+ new RampEvent(0.9, 0.5, 0.3), new RampEvent(-0.5, 0.5, 0.3),
+ new RampEvent(0.8, 2.0, 1.0),
+ };
+
+ private static class RampEvent {
+ double target;
+ double eventDuration;
+ double rampDuration;
+
+ RampEvent(double target, double eventDuration, double rampDuration) {
+ this.target = target;
+ this.eventDuration = eventDuration;
+ this.rampDuration = rampDuration;
+ }
+ }
+
+ private void test(int mode) throws IOException {
+ // Create a context for the synthesizer.
+ synth = JSyn.createSynthesizer();
+ synth.setRealTime(false);
+
+ File waveFile = new File("ramp_pitch_" + modeNames[mode] + ".wav");
+ // Mono 16 bits.
+ recorder = new WaveRecorder(synth, waveFile, 1, 16);
+ System.out.println("Writing to 16-bit WAV file " + waveFile.getAbsolutePath());
+
+ // Add some tone generators.
+ synth.add(osc = new SawtoothOscillatorBL());
+
+ // Add a controller that will sweep up.
+ synth.add(multiplier = new Multiply());
+ synth.add(powerOfTwo = new PowerOfTwo());
+ // Add an output unit.
+ synth.add(lineOut = new LineOut());
+ multiplier.inputB.set(660.0);
+
+ switch (mode) {
+ case MODE_STEP:
+ synth.add(ramp = new PassThrough());
+ break;
+ case MODE_LINEAR:
+ synth.add(ramp = linearRamp = new LinearRamp());
+ linearRamp.current.set(-1.0);
+ linearRamp.time.set(10.0);
+ break;
+ case MODE_SMOOTH:
+ synth.add(ramp = continuousRamp = new ContinuousRamp());
+ continuousRamp.current.set(-1.0);
+ continuousRamp.time.set(10.0);
+ break;
+ }
+
+ ramp.getInput().set(-1.0);
+ ramp.getOutput().connect(powerOfTwo.input);
+
+ powerOfTwo.output.connect(multiplier.inputA);
+ multiplier.output.connect(osc.frequency);
+
+ // Connect the oscillator to the left and right audio output.
+ osc.output.connect(0, lineOut.input, 0);
+ osc.output.connect(0, lineOut.input, 1);
+
+ // Start synthesizer using default stereo output at 44100 Hz.
+ synth.start();
+
+ osc.output.connect(0, recorder.getInput(), 0);
+ // When we start the recorder it will pull data from the oscillator
+ // and sweeper.
+ recorder.start();
+
+ // We also need to start the LineOut if we want to hear it now.
+ lineOut.start();
+
+ // Get synthesizer time in seconds.
+ double nextEventTime = synth.getCurrentTime() + 1.0;
+ try {
+ synth.sleepUntil(nextEventTime);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+
+ for (RampEvent rampEvent : rampData) {
+
+ switch (mode) {
+ case MODE_STEP:
+ break;
+ case MODE_LINEAR:
+ linearRamp.time.set(rampEvent.rampDuration);
+ break;
+ case MODE_SMOOTH:
+ continuousRamp.time.set(rampEvent.rampDuration);
+ break;
+ }
+ ramp.getInput().set(rampEvent.target);
+
+ nextEventTime += rampEvent.eventDuration;
+ System.out.println("target = " + rampEvent.target + ", rampDur = "
+ + rampEvent.rampDuration + ", eventDur = " + rampEvent.eventDuration);
+ try {
+ synth.sleepUntil(nextEventTime);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+
+ if (recorder != null) {
+ recorder.stop();
+ recorder.close();
+ }
+ // Stop everything.
+ synth.stop();
+ }
+
+ public static void main(String[] args) {
+ try {
+ new RecordVariousRamps().test(MODE_STEP);
+ new RecordVariousRamps().test(MODE_LINEAR);
+ new RecordVariousRamps().test(MODE_SMOOTH);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+}
diff --git a/tests/com/jsyn/swing/TestRangeModels.java b/tests/com/jsyn/swing/TestRangeModels.java
new file mode 100644
index 0000000..8bcd021
--- /dev/null
+++ b/tests/com/jsyn/swing/TestRangeModels.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.swing;
+
+import junit.framework.TestCase;
+
+public class TestRangeModels extends TestCase {
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ public void checkDoubleRange(double dmin, double dmax, double dval) {
+ int resolution = 1000;
+ DoubleBoundedRangeModel model = new DoubleBoundedRangeModel("test", resolution, dmin, dmax,
+ dval);
+ assertEquals("setup min", dmin, model.getDoubleMinimum(), 0.0001);
+ assertEquals("setup max", dmax, model.getDoubleMaximum(), 0.0001);
+ assertEquals("setup value", dval, model.getDoubleValue(), 0.0001);
+
+ model.setDoubleValue(dmin);
+ assertEquals("min double value", dmin, model.getDoubleValue(), 0.0001);
+ assertEquals("min value", 0, model.getValue());
+
+ double dmid = (dmax + dmin) / 2.0;
+ model.setDoubleValue(dmid);
+ assertEquals("middle double value", dmid, model.getDoubleValue(), 0.0001);
+ assertEquals("middle value", resolution / 2, model.getValue());
+
+ model.setDoubleValue(dmax);
+ assertEquals("max double value", dmax, model.getDoubleValue(), 0.0001);
+ assertEquals("max value", resolution, model.getValue());
+
+ }
+
+ public void testDoubleRange() {
+ checkDoubleRange(10.0, 20.0, 12.0);
+ checkDoubleRange(-1.0, 1.0, 0.5);
+ }
+}
diff --git a/tests/com/jsyn/unitgen/CalibrateMoogFilter.java b/tests/com/jsyn/unitgen/CalibrateMoogFilter.java
new file mode 100644
index 0000000..a830fcc
--- /dev/null
+++ b/tests/com/jsyn/unitgen/CalibrateMoogFilter.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import javax.swing.JApplet;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+
+/**
+ * Play a sawtooth through a 4-pole filter.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class CalibrateMoogFilter extends JApplet {
+ private Synthesizer synth;
+ private UnitOscillator oscillator;
+ private SineOscillator reference;
+ ZeroCrossingCounter zeroCounter;
+ PitchDetector pitchDetector;
+ ZeroCrossingCounter sineZeroCounter;
+ PitchDetector sinePitchDetector;
+ private FilterFourPoles filterMoog;
+ private LineOut lineOut;
+
+ @Override
+ public void init() {
+ synth = JSyn.createSynthesizer();
+ synth.setRealTime(false);
+ synth.add(oscillator = new SawtoothOscillatorBL());
+ synth.add(reference = new SineOscillator());
+ synth.add(filterMoog = new FilterFourPoles());
+ synth.add(pitchDetector = new PitchDetector());
+ synth.add(sinePitchDetector = new PitchDetector());
+ synth.add(zeroCounter = new ZeroCrossingCounter());
+ synth.add(sineZeroCounter = new ZeroCrossingCounter());
+ synth.add(lineOut = new LineOut());
+
+ oscillator.output.connect(filterMoog.input);
+ filterMoog.output.connect(zeroCounter.input);
+ zeroCounter.output.connect(pitchDetector.input);
+ reference.output.connect(0, lineOut.input, 0);
+ filterMoog.output.connect(0, lineOut.input, 1);
+
+ reference.output.connect(sineZeroCounter.input);
+ sineZeroCounter.output.connect(sinePitchDetector.input);
+
+ oscillator.frequency.set(130.0);
+ oscillator.amplitude.set(0.001);
+ filterMoog.frequency.set(440.0);
+ filterMoog.Q.set(4.1);
+ }
+
+ @Override
+ public void start() {
+ // Start synthesizer using default stereo output at 44100 Hz.
+ synth.start();
+ pitchDetector.start();
+ sinePitchDetector.start();
+ lineOut.start();
+ }
+
+ @Override
+ public void stop() {
+ pitchDetector.stop();
+ sinePitchDetector.stop();
+ lineOut.stop();
+ synth.stop();
+ }
+
+ public void test() {
+ init();
+ start();
+ try {
+ calibrate();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ stop();
+ }
+
+ private void calibrate() throws InterruptedException {
+ synth.sleepFor(2.0);
+ double freq = 100.0;
+ System.out
+ .printf("freq, moogFreq, ratio, moogConf, sineFreq, sineConf, moogZRate, sineZRate\n");
+ long startingFrameCount = synth.getFrameCount();
+ long startingMoogZeroCount = zeroCounter.getCount();
+ long startingSineZeroCount = sineZeroCounter.getCount();
+ for (int i = 0; i < 50; i++) {
+ reference.frequency.set(freq);
+ filterMoog.frequency.set(freq);
+ synth.sleepFor(2.0);
+
+ long endingFrameCount = synth.getFrameCount();
+ long elapsedFrames = endingFrameCount - startingFrameCount;
+ startingFrameCount = endingFrameCount;
+
+ long endingMoogZeroCount = zeroCounter.getCount();
+ long elapsedMoogZeros = endingMoogZeroCount - startingMoogZeroCount;
+ startingMoogZeroCount = endingMoogZeroCount;
+
+ long endingSineZeroCount = sineZeroCounter.getCount();
+ long elapsedSineZeros = endingSineZeroCount - startingSineZeroCount;
+ startingSineZeroCount = endingSineZeroCount;
+
+ double moogZeroRate = elapsedMoogZeros * (double) synth.getFrameRate() / elapsedFrames;
+ double sineZeroRate = elapsedSineZeros * (double) synth.getFrameRate() / elapsedFrames;
+
+ double moogMeasuredFreq = pitchDetector.frequency.get();
+ double moogConfidence = pitchDetector.confidence.get();
+ double sineMeasuredFreq = sinePitchDetector.frequency.get();
+ double sineConfidence = sinePitchDetector.confidence.get();
+ double ratio = freq / moogMeasuredFreq;
+ System.out.printf("%7.2f, %8.5f, %7.5f, %4.2f, %8.5f, %4.2f, %8.4f, %8.4f\n", freq,
+ moogMeasuredFreq, ratio, moogConfidence, sineMeasuredFreq, sineConfidence,
+ moogZeroRate, sineZeroRate);
+
+ freq *= 1.1;
+ }
+ }
+
+ public static void main(String args[]) {
+ new CalibrateMoogFilter().test();
+ }
+
+}
diff --git a/tests/com/jsyn/unitgen/EnablingGate.java b/tests/com/jsyn/unitgen/EnablingGate.java
new file mode 100644
index 0000000..daf36be
--- /dev/null
+++ b/tests/com/jsyn/unitgen/EnablingGate.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitInputPort;
+
+/**
+ * This can be used to block the execution of upstream units. It can be placed at the output of a
+ * circuit and driven with an amplitude envelope.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class EnablingGate extends UnitFilter {
+ public UnitInputPort gate;
+
+ /* Define Unit Ports used by connect() and set(). */
+ public EnablingGate() {
+ super();
+ addPort(gate = new UnitInputPort("Gate"));
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ double[] aValues = input.getValues();
+ double[] bValues = gate.getValues();
+ double[] outputs = output.getValues();
+ for (int i = start; i < limit; i++) {
+ outputs[i] = aValues[i] * bValues[i];
+ }
+ // If we end up at zero then disable pulling of data.
+ // We do this at the end so that envelope can get started.
+ if (outputs[limit - 1] <= 0.0) {
+ setEnabled(false);
+ }
+ }
+
+}
diff --git a/tests/com/jsyn/unitgen/NonRealTimeTestCase.java b/tests/com/jsyn/unitgen/NonRealTimeTestCase.java
new file mode 100644
index 0000000..5d332a9
--- /dev/null
+++ b/tests/com/jsyn/unitgen/NonRealTimeTestCase.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import junit.framework.TestCase;
+
+import com.jsyn.engine.SynthesisEngine;
+
+public abstract class NonRealTimeTestCase extends TestCase {
+
+ protected SynthesisEngine synthesisEngine;
+
+ public NonRealTimeTestCase() {
+ super();
+ }
+
+ public NonRealTimeTestCase(String name) {
+ super(name);
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ synthesisEngine = new SynthesisEngine();
+ synthesisEngine.setRealTime(false);
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ synthesisEngine.stop();
+ }
+
+}
diff --git a/tests/com/jsyn/unitgen/RecordMoogFilter.java b/tests/com/jsyn/unitgen/RecordMoogFilter.java
new file mode 100644
index 0000000..6af11fd
--- /dev/null
+++ b/tests/com/jsyn/unitgen/RecordMoogFilter.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+import javax.swing.JApplet;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import com.jsyn.util.WaveRecorder;
+
+/**
+ * Measure actual frequency as a function of input frequency and Q.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class RecordMoogFilter extends JApplet {
+ private final static boolean SWEEP_Q = false;
+ private final static boolean SWEEP_FREQUENCY = true;
+ private final static int NUM_STEPS = 11;
+
+ private final static double MIN_Q = 0.0;
+ private final static double DEFAULT_Q = 9.0;
+ private final static double MAX_Q = 10.0;
+
+ private final static double MIN_FREQUENCY = 100.0;
+ private final static double DEFAULT_FREQUENCY = 500.0;
+ private final static double MAX_FREQUENCY = 4000.0;
+
+ private Synthesizer synth;
+ private WhiteNoise source;
+ private SineOscillator reference;
+ private FilterFourPoles filterMoog;
+ private LineOut lineOut;
+ private WaveRecorder recorder;
+
+ @Override
+ public void init() {
+ synth = JSyn.createSynthesizer();
+ synth.setRealTime(false);
+ synth.add(source = new WhiteNoise());
+ synth.add(filterMoog = new FilterFourPoles());
+ synth.add(reference = new SineOscillator());
+ synth.add(lineOut = new LineOut());
+
+ source.output.connect(filterMoog.input);
+ reference.output.connect(0, lineOut.input, 0);
+ filterMoog.output.connect(0, lineOut.input, 1);
+
+ reference.amplitude.set(0.5);
+ source.amplitude.set(0.5);
+ filterMoog.frequency.set(DEFAULT_FREQUENCY);
+ filterMoog.Q.set(DEFAULT_Q);
+
+ File waveFile = new File("temp_recording.wav");
+ // Default is stereo, 16 bits.
+ try {
+ recorder = new WaveRecorder(synth, waveFile);
+ } catch (FileNotFoundException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ System.out.println("Writing to WAV file " + waveFile.getAbsolutePath());
+ }
+
+ @Override
+ public void start() {
+ // Start synthesizer using default stereo output at 44100 Hz.
+ synth.start();
+ lineOut.start();
+
+ reference.output.connect(0, recorder.getInput(), 0);
+ filterMoog.output.connect(0, recorder.getInput(), 1);
+ recorder.start();
+ }
+
+ @Override
+ public void stop() {
+ if (recorder != null) {
+ recorder.stop();
+ try {
+ recorder.close();
+ } catch (IOException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+ lineOut.stop();
+ synth.stop();
+ }
+
+ public void test() {
+ init();
+ start();
+ try {
+ calibrate();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ stop();
+ }
+
+ private void calibrate() throws InterruptedException {
+ synth.sleepFor(0.2);
+ double freq = SWEEP_FREQUENCY ? MIN_FREQUENCY : DEFAULT_FREQUENCY;
+ double q = SWEEP_Q ? MIN_Q : DEFAULT_Q;
+ double stepQ = (MAX_Q - MIN_Q) / (NUM_STEPS - 1);
+ double scaleFrequency = Math.pow((MAX_FREQUENCY / MIN_FREQUENCY), (1.0 / (NUM_STEPS - 1)));
+ System.out.printf("freq, q, measured\n");
+ for (int i = 0; i < NUM_STEPS; i++) {
+ double refAmp = reference.amplitude.get();
+ reference.amplitude.set(0.0);
+ synth.sleepFor(0.1);
+ reference.amplitude.set(refAmp);
+
+ System.out.printf("%8.2f, %6.3f, \n", freq, q);
+ filterMoog.frequency.set(freq);
+ reference.frequency.set(freq);
+ filterMoog.Q.set(q);
+
+ synth.sleepFor(2.0);
+
+ if (SWEEP_FREQUENCY) {
+ freq *= scaleFrequency;
+ }
+ if (SWEEP_Q) {
+ q += stepQ;
+ }
+ }
+ }
+
+ public static void main(String args[]) {
+ new RecordMoogFilter().test();
+ }
+
+}
diff --git a/tests/com/jsyn/unitgen/TestConnections.java b/tests/com/jsyn/unitgen/TestConnections.java
new file mode 100644
index 0000000..d15a257
--- /dev/null
+++ b/tests/com/jsyn/unitgen/TestConnections.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import junit.framework.TestCase;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+
+public class TestConnections extends TestCase {
+ Add add1;
+ Add add2;
+ Add add3;
+
+ Synthesizer synth;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ synth = JSyn.createSynthesizer();
+
+ synth.add(add1 = new Add());
+ synth.add(add2 = new Add());
+ synth.add(add3 = new Add());
+
+ add1.start();
+ add2.start();
+ add3.start();
+
+ add1.inputA.set(0.1);
+ add1.inputB.set(0.2);
+
+ add2.inputA.set(0.4);
+ add2.inputB.set(0.8);
+
+ add3.inputA.set(1.6);
+ add3.inputB.set(3.2);
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ public void testSet() throws InterruptedException {
+ synth.sleepFor(0.01);
+ assertEquals("set inputs of adder", 0.3, add1.output.getValue(), 0.0001);
+ }
+
+ public void testConnect() throws InterruptedException {
+ synth.sleepFor(0.01);
+ assertEquals("set inputs of adder", 0.3, add1.output.getValue(), 0.0001);
+ assertEquals("set inputs of adder", 1.2, add2.output.getValue(), 0.0001);
+
+ // Test different ways of connecting.
+ add1.output.connect(add2.inputB);
+ checkConnection();
+
+ add1.output.connect(0, add2.inputB, 0);
+ checkConnection();
+
+ add1.output.connect(add2.inputB.getConnectablePart(0));
+ checkConnection();
+
+ add1.output.getConnectablePart(0).connect(add2.inputB);
+ checkConnection();
+
+ add1.output.getConnectablePart(0).connect(add2.inputB.getConnectablePart(0));
+ checkConnection();
+
+ add2.inputB.connect(add1.output);
+ checkConnection();
+
+ add2.inputB.connect(0, add1.output, 0);
+ checkConnection();
+
+ add2.inputB.connect(add1.output.getConnectablePart(0));
+ checkConnection();
+
+ add2.inputB.getConnectablePart(0).connect(add1.output);
+ checkConnection();
+
+ add2.inputB.getConnectablePart(0).connect(add1.output.getConnectablePart(0));
+ checkConnection();
+ }
+
+ private void checkConnection() throws InterruptedException {
+ synth.sleepFor(0.01);
+ assertEquals("connection should not change output", 0.3, add1.output.getValue(), 0.0001);
+ assertEquals("replace set value with output", 0.7, add2.output.getValue(), 0.0001);
+
+ // Revert to set value after disconnection.
+ add1.output.disconnectAll();
+ synth.sleepFor(0.01);
+ assertEquals("still the same", 0.3, add1.output.getValue(), 0.0001);
+ assertEquals("should revert to original set() value", 1.2, add2.output.getValue(), 0.0001);
+ }
+
+}
diff --git a/tests/com/jsyn/unitgen/TestDelay.java b/tests/com/jsyn/unitgen/TestDelay.java
new file mode 100644
index 0000000..12af1cd
--- /dev/null
+++ b/tests/com/jsyn/unitgen/TestDelay.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.util.AudioStreamReader;
+
+public class TestDelay extends NonRealTimeTestCase {
+ public void testFloor() {
+ double x = -7.3;
+ int n = (int) Math.floor(x);
+ assertEquals("int", -8, n);
+ }
+
+ public void checkInterpolatingDelay(int maxFrames, double delayFrames)
+ throws InterruptedException {
+ synthesisEngine.start();
+
+ System.out.printf("test delayFrames = %7.5f\n", delayFrames);
+ InterpolatingDelay delay = new InterpolatingDelay();
+ synthesisEngine.add(delay);
+ delay.allocate(maxFrames);
+ delay.delay.set(delayFrames / 44100.0);
+ SawtoothOscillator osc = new SawtoothOscillator();
+ synthesisEngine.add(osc);
+ osc.frequency.set(synthesisEngine.getFrameRate() / 4.0);
+ osc.amplitude.set(1.0);
+ osc.output.connect(delay.input);
+
+ int samplesPerFrame = 1;
+ AudioStreamReader reader = new AudioStreamReader(synthesisEngine, samplesPerFrame);
+ delay.output.connect(reader.getInput());
+
+ delay.start();
+ for (int i = 0; i < (3 * maxFrames); i++) {
+ if (reader.available() == 0) {
+ synthesisEngine.sleepFor(0.01);
+ }
+ double actual = reader.read();
+ double expected = 1 + i - delayFrames;
+ if (expected < 0.0) {
+ expected = 0.0;
+ }
+ // System.out.printf( "[%d] expected = %7.3f, delayed = %7.3f\n", i, expected, actual );
+ // assertEquals("delayed output", expected, actual, 0.00001);
+ }
+ }
+
+ public void testSmall() throws InterruptedException {
+ checkInterpolatingDelay(40, 7.0);
+ }
+
+ public void testEven() throws InterruptedException {
+ checkInterpolatingDelay(44100, 13671.0);
+ }
+
+ public void testInterpolatingDelay() throws InterruptedException {
+ checkInterpolatingDelay(44100, 13671.4);
+ }
+}
diff --git a/tests/com/jsyn/unitgen/TestEnable.java b/tests/com/jsyn/unitgen/TestEnable.java
new file mode 100644
index 0000000..37a4a2b
--- /dev/null
+++ b/tests/com/jsyn/unitgen/TestEnable.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import junit.framework.TestCase;
+
+import com.jsyn.engine.SynthesisEngine;
+
+public class TestEnable extends TestCase {
+ SynthesisEngine synthesisEngine;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ synthesisEngine = new SynthesisEngine();
+ synthesisEngine.setRealTime(false);
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ synthesisEngine.stop();
+ }
+
+ public void testEnablingGate() throws InterruptedException {
+ LinearRamp ramp = new LinearRamp();
+ synthesisEngine.add(ramp);
+ EnablingGate enabler = new EnablingGate();
+ synthesisEngine.add(enabler);
+ Add adder = new Add();
+ synthesisEngine.add(adder);
+
+ ramp.output.connect(enabler.input);
+ enabler.output.connect(adder.inputA);
+
+ // set up so ramp should equal time
+ ramp.current.set(0.0);
+ ramp.input.set(1.0);
+ ramp.time.set(1.0);
+ enabler.gate.set(1.0);
+
+ synthesisEngine.start();
+ double startTime = synthesisEngine.getCurrentTime();
+ // pull from final adder
+ adder.start();
+ synthesisEngine.sleepUntil(startTime + 0.1);
+ double tolerance = 0.002;
+ assertEquals("ramp going up", 0.1, ramp.output.getValue(), tolerance);
+ assertEquals("enabler going up", 0.1, enabler.output.getValue(), tolerance);
+ assertEquals("adder going up", 0.1, adder.output.getValue(), tolerance);
+ synthesisEngine.sleepUntil(startTime + 0.2);
+ assertEquals("start enabled", 0.2, adder.output.getValue(), tolerance);
+
+ // disable everything upstream
+ enabler.gate.set(0.0);
+
+ synthesisEngine.sleepUntil(startTime + 0.3);
+ assertEquals("should not be pulled", 0.2, ramp.output.getValue(), tolerance);
+ assertEquals("should be disabled", false, enabler.isEnabled());
+ assertEquals("should be zero", 0.0, enabler.output.getValue(), tolerance);
+ assertEquals("zero", 0.0, adder.output.getValue(), tolerance);
+
+ }
+}
diff --git a/tests/com/jsyn/unitgen/TestEnvelopeAttackDecay.java b/tests/com/jsyn/unitgen/TestEnvelopeAttackDecay.java
new file mode 100644
index 0000000..50ecb15
--- /dev/null
+++ b/tests/com/jsyn/unitgen/TestEnvelopeAttackDecay.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.engine.SynthesisEngine;
+
+public class TestEnvelopeAttackDecay extends TestUnitGate {
+ double attackTime;
+ double decayTime;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ synthesisEngine = new SynthesisEngine();
+ synthesisEngine.setRealTime(false);
+ attackTime = 0.2;
+ decayTime = 0.4;
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ synthesisEngine.stop();
+ }
+
+ public void testOnOff() throws InterruptedException {
+ EnvelopeAttackDecay envelope = new EnvelopeAttackDecay();
+ synthesisEngine.add(envelope);
+
+ envelope.attack.set(0.1);
+ envelope.decay.set(0.2);
+
+ synthesisEngine.start();
+ envelope.start();
+ time = synthesisEngine.getCurrentTime();
+ synthesisEngine.sleepUntil(time + 0.1);
+ assertEquals("still idling", 0.0, envelope.output.getValue());
+
+ // Trigger the envelope using on/off
+ envelope.input.on();
+ time = synthesisEngine.getCurrentTime();
+ // Check end of attack cycle.
+ synthesisEngine.sleepUntil(time + 0.1);
+ assertTrue("at peak", (envelope.output.getValue() > 0.8));
+ envelope.input.off();
+ // Check end of decay cycle.
+ synthesisEngine.sleepUntil(time + 0.3);
+ assertTrue("at peak", (envelope.output.getValue() < 0.1));
+
+ synthesisEngine.sleepFor(0.1);
+
+ // Trigger the envelope using trigger()
+ envelope.input.trigger();
+ time = synthesisEngine.getCurrentTime();
+ // Check end of attack cycle.
+ synthesisEngine.sleepUntil(time + 0.1);
+ assertTrue("at peak", (envelope.output.getValue() > 0.8));
+ // Check end of decay cycle.
+ synthesisEngine.sleepUntil(time + 0.3);
+ assertTrue("at peak", (envelope.output.getValue() < 0.1));
+
+ }
+
+ public void testRetrigger() throws InterruptedException {
+ EnvelopeAttackDecay envelope = new EnvelopeAttackDecay();
+ synthesisEngine.add(envelope);
+
+ envelope.attack.set(0.1);
+ envelope.decay.set(0.2);
+
+ synthesisEngine.start();
+ envelope.start();
+ time = synthesisEngine.getCurrentTime();
+ synthesisEngine.sleepUntil(time + 0.1);
+ assertEquals("still idling", 0.0, envelope.output.getValue());
+
+ // Trigger the envelope using trigger()
+ envelope.input.trigger();
+ // Check end of attack cycle.
+ synthesisEngine.sleepFor(0.1);
+ assertEquals("at peak", 1.0, envelope.output.getValue(), 0.1);
+
+ // Decay half way.
+ synthesisEngine.sleepFor(0.1);
+ assertTrue("at peak", (envelope.output.getValue() < 0.7));
+
+ // Retrigger while decaying
+ envelope.input.trigger();
+ // Will get to top faster.
+ synthesisEngine.sleepFor(0.1);
+ assertEquals("at peak", 1.0, envelope.output.getValue(), 0.1);
+
+ // Check end of decay cycle.
+ synthesisEngine.sleepFor(0.2);
+ assertTrue("at peak", (envelope.output.getValue() < 0.1));
+
+ }
+
+ public void testAutoDisable() throws InterruptedException {
+
+ LinearRamp ramp = new LinearRamp();
+ synthesisEngine.add(ramp);
+ EnvelopeAttackDecay envelope = new EnvelopeAttackDecay();
+ envelope.attack.set(0.1);
+ envelope.decay.set(0.1);
+ synthesisEngine.add(envelope);
+ ramp.output.connect(envelope.amplitude);
+
+ checkAutoDisable(ramp, envelope);
+
+ }
+}
diff --git a/tests/com/jsyn/unitgen/TestEnvelopeDAHDSR.java b/tests/com/jsyn/unitgen/TestEnvelopeDAHDSR.java
new file mode 100644
index 0000000..8c781ac
--- /dev/null
+++ b/tests/com/jsyn/unitgen/TestEnvelopeDAHDSR.java
@@ -0,0 +1,339 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.engine.SynthesisEngine;
+
+public class TestEnvelopeDAHDSR extends TestUnitGate {
+ double delayTime;
+ double attackTime;
+ double holdTime;
+ double decayTime;
+ double sustainLevel;
+ double releaseTime;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ synthesisEngine = new SynthesisEngine();
+ synthesisEngine.setRealTime(false);
+ delayTime = 0.1;
+ attackTime = 0.2;
+ holdTime = 0.3;
+ decayTime = 0.4;
+ sustainLevel = 0.5;
+ releaseTime = 0.6;
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ synthesisEngine.stop();
+ }
+
+ public void testStages() throws InterruptedException {
+ EnvelopeDAHDSR ramp = checkToSustain();
+
+ // Change sustain level to simulate tremolo sustain.
+ sustainLevel = 0.7;
+ ramp.sustain.set(sustainLevel);
+ time += 0.01;
+ synthesisEngine.sleepUntil(time);
+ assertEquals("sustain moving delaying", sustainLevel, ramp.output.getValue(), 0.01);
+
+ // Gate off to let envelope release.
+ ramp.input.set(0.0);
+ synthesisEngine.sleepUntil(time + (releaseTime * 0.1));
+ double releaseValue = ramp.output.getValue();
+ assertEquals("partway down release", sustainLevel * 0.36, releaseValue, 0.01);
+ }
+
+ private EnvelopeDAHDSR checkToSustain() throws InterruptedException {
+ EnvelopeDAHDSR ramp = new EnvelopeDAHDSR();
+ synthesisEngine.add(ramp);
+
+ ramp.delay.set(delayTime);
+ ramp.attack.set(attackTime);
+ ramp.hold.set(holdTime);
+ ramp.decay.set(decayTime);
+ ramp.sustain.set(sustainLevel);
+ ramp.release.set(releaseTime);
+
+ synthesisEngine.start();
+ ramp.start();
+ time = synthesisEngine.getCurrentTime();
+ synthesisEngine.sleepUntil(time + (2.0 * delayTime));
+ assertEquals("still idling", 0.0, ramp.output.getValue());
+
+ // Trigger the envelope.
+ ramp.input.set(1.0);
+ time = synthesisEngine.getCurrentTime();
+ // Check end of delay cycle.
+ synthesisEngine.sleepUntil(time + (delayTime * 0.9));
+ assertEquals("still delaying", 0.0, ramp.output.getValue(), 0.01);
+ // Half way up attack ramp.
+ synthesisEngine.sleepUntil(time + delayTime + (attackTime * 0.5));
+ assertEquals("half attack", 0.5, ramp.output.getValue(), 0.01);
+ // Holding after attack.
+ synthesisEngine.sleepUntil(time + delayTime + attackTime + (holdTime * 0.1));
+ assertEquals("holding", 1.0, ramp.output.getValue(), 0.01);
+ synthesisEngine.sleepUntil(time + delayTime + attackTime + (holdTime * 0.9));
+ assertEquals("still holding", 1.0, ramp.output.getValue(), 0.01);
+ synthesisEngine.sleepUntil(time + delayTime + attackTime + holdTime + decayTime);
+ time = synthesisEngine.getCurrentTime();
+ assertEquals("at sustain", sustainLevel, ramp.output.getValue(), 0.01);
+ return ramp;
+ }
+
+ public void testRetrigger() throws InterruptedException {
+ EnvelopeDAHDSR ramp = checkToSustain();
+
+ // Gate off to let envelope release.
+ ramp.input.set(0.0);
+ synthesisEngine.sleepUntil(time + (releaseTime * 0.1));
+ double releaseValue = ramp.output.getValue();
+ assertEquals("partway down release", sustainLevel * 0.36, releaseValue, 0.01);
+
+ // Retrigger during release phase.
+ time = synthesisEngine.getCurrentTime();
+ ramp.input.set(1.0);
+ // Check end of delay cycle.
+ synthesisEngine.sleepUntil(time + (delayTime * 0.9));
+ assertEquals("still delaying", releaseValue, ramp.output.getValue(), 0.01);
+ // Half way up attack ramp from where it started.
+ synthesisEngine.sleepUntil(time + delayTime + (attackTime * 0.5));
+ assertEquals("half attack", releaseValue + 0.5, ramp.output.getValue(), 0.01);
+
+ }
+
+ // I noticed a hang while playing with knobs.
+ public void testHang() throws InterruptedException {
+
+ delayTime = 0.0;
+ attackTime = 0.0;
+ holdTime = 0.0;
+ decayTime = 0.0;
+ sustainLevel = 0.3;
+ releaseTime = 3.0;
+
+ EnvelopeDAHDSR ramp = new EnvelopeDAHDSR();
+ synthesisEngine.add(ramp);
+
+ ramp.delay.set(delayTime);
+ ramp.attack.set(attackTime);
+ ramp.hold.set(holdTime);
+ ramp.decay.set(decayTime);
+ ramp.sustain.set(sustainLevel);
+ ramp.release.set(releaseTime);
+
+ synthesisEngine.start();
+ ramp.start();
+ // Trigger the envelope.
+ ramp.input.set(1.0);
+ time = synthesisEngine.getCurrentTime();
+ synthesisEngine.sleepUntil(time + 0.01);
+ assertEquals("should jump to sustain level", sustainLevel, ramp.output.getValue());
+
+ // Gate off to let envelope release.
+ ramp.input.set(0.0);
+ synthesisEngine.sleepUntil(time + 1.0);
+ double releaseValue = ramp.output.getValue();
+ assertTrue("partway down release", sustainLevel > releaseValue);
+
+ holdTime = 0.5;
+ ramp.hold.set(holdTime);
+ decayTime = 0.5;
+ ramp.decay.set(decayTime);
+
+ // Retrigger during release phase and try to catch it at top of hold
+ time = synthesisEngine.getCurrentTime();
+ ramp.input.set(1.0);
+ // Check end of delay cycle.
+ synthesisEngine.sleepUntil(time + (holdTime * 0.1));
+ assertEquals("should jump to hold", 1.0, ramp.output.getValue(), 0.01);
+ }
+
+ public void testNegative() throws InterruptedException {
+ delayTime = -0.1;
+ attackTime = -0.2;
+ holdTime = -0.3;
+ decayTime = -0.4;
+ sustainLevel = 0.3;
+ releaseTime = -0.5;
+
+ EnvelopeDAHDSR ramp = new EnvelopeDAHDSR();
+ synthesisEngine.add(ramp);
+
+ ramp.delay.set(delayTime);
+ ramp.attack.set(attackTime);
+ ramp.hold.set(holdTime);
+ ramp.decay.set(decayTime);
+ ramp.sustain.set(sustainLevel);
+ ramp.release.set(releaseTime);
+
+ synthesisEngine.start();
+ ramp.start();
+ // Trigger the envelope.
+ ramp.input.set(1.0);
+ time = synthesisEngine.getCurrentTime();
+ time += 0.1;
+ synthesisEngine.sleepUntil(time + 0.01);
+ assertEquals("should jump to sustain level", sustainLevel, ramp.output.getValue());
+
+ ramp.sustain.set(sustainLevel = -0.4);
+ time += 0.1;
+ synthesisEngine.sleepUntil(time);
+ assertEquals("sustain should clip at zero", sustainLevel, ramp.output.getValue());
+
+ ramp.sustain.set(sustainLevel = 0.4);
+ time += 0.1;
+ synthesisEngine.sleepUntil(time);
+ assertEquals("sustain should come back", sustainLevel, ramp.output.getValue());
+
+ // Gate off to let envelope release.
+ ramp.input.set(0.0);
+ time += 0.1;
+ synthesisEngine.sleepUntil(time);
+ double releaseValue = ramp.output.getValue();
+ assertEquals("release quickly", 0.0, releaseValue);
+ }
+
+ public void testOnOff() throws InterruptedException {
+ EnvelopeDAHDSR ramp = new EnvelopeDAHDSR();
+ synthesisEngine.add(ramp);
+
+ ramp.delay.set(0.0);
+ ramp.attack.set(0.1);
+ ramp.hold.set(0.0);
+ ramp.decay.set(0.0);
+ ramp.sustain.set(0.9);
+ ramp.release.set(0.1);
+
+ synthesisEngine.start();
+ ramp.start();
+ time = synthesisEngine.getCurrentTime();
+ synthesisEngine.sleepUntil(time + 0.2);
+ assertEquals("still idling", 0.0, ramp.output.getValue());
+
+ // Trigger the envelope.
+ ramp.input.on();
+ time = synthesisEngine.getCurrentTime();
+ // Check end of delay cycle.
+ synthesisEngine.sleepUntil(time + 0.2);
+ assertEquals("at sustain", 0.9, ramp.output.getValue(), 0.01);
+
+ // Release the envelope.
+ ramp.input.off();
+ time = synthesisEngine.getCurrentTime();
+ // Check end of delay cycle.
+ synthesisEngine.sleepUntil(time + 0.2);
+ assertEquals("after release", 0.0, ramp.output.getValue(), 0.01);
+ }
+
+ public void testAutoDisable() throws InterruptedException {
+
+ LinearRamp ramp = new LinearRamp();
+ synthesisEngine.add(ramp);
+ EnvelopeDAHDSR envelope = new EnvelopeDAHDSR();
+ synthesisEngine.add(envelope);
+ envelope.attack.set(0.1);
+ envelope.decay.set(0.1);
+ envelope.release.set(0.1);
+ envelope.sustain.set(0.1);
+ ramp.output.connect(envelope.amplitude);
+
+ checkAutoDisable(ramp, envelope);
+
+ }
+
+ class GatedRampCircuit extends Circuit {
+ LinearRamp ramp;
+ EnvelopeDAHDSR envelope;
+
+ GatedRampCircuit() {
+ add(ramp = new LinearRamp());
+ add(envelope = new EnvelopeDAHDSR());
+ envelope.attack.set(0.1);
+ envelope.decay.set(0.1);
+ envelope.release.set(0.1);
+ envelope.sustain.set(0.1);
+
+ envelope.setupAutoDisable(this);
+ ramp.output.connect(envelope.amplitude);
+ }
+ }
+
+ public void testAutoDisableCircuit() throws InterruptedException {
+ GatedRampCircuit circuit = new GatedRampCircuit();
+ synthesisEngine.add(circuit);
+ checkAutoDisable(circuit.ramp, circuit.envelope);
+ }
+
+ public void checkReleaseTiming(double releaseTime, double tolerance)
+ throws InterruptedException {
+ delayTime = 0.0;
+ attackTime = 0.2;
+ holdTime = 0.0;
+ decayTime = 10.0;
+ sustainLevel = 1.0;
+
+ EnvelopeDAHDSR ramp = new EnvelopeDAHDSR();
+ synthesisEngine.add(ramp);
+
+ ramp.delay.set(delayTime);
+ ramp.attack.set(attackTime);
+ ramp.hold.set(holdTime);
+ ramp.decay.set(decayTime);
+ ramp.sustain.set(sustainLevel);
+ ramp.release.set(releaseTime);
+
+ synthesisEngine.start();
+ ramp.start();
+ // Trigger the envelope.
+ ramp.input.set(1.0);
+ time = synthesisEngine.getCurrentTime();
+ time += attackTime * 2;
+ synthesisEngine.sleepUntil(time);
+ assertEquals("should be at to sustain level", sustainLevel, ramp.output.getValue());
+
+ // Start envelope release.
+ ramp.input.set(0.0);
+ final double db90 = 20.0 * Math.log(1.0 / 32768.0) / Math.log(10.0);
+ System.out.println("JSyns DB90 is actually " + db90);
+ int numSteps = 10;
+ for (int i = 0; i < 10; i++) {
+ time += releaseTime / numSteps;
+ synthesisEngine.sleepUntil(time);
+ double expectedDB = db90 * (i + 1) / numSteps;
+ double expectedAmplitude = sustainLevel * Math.pow(10.0, expectedDB / 20.0);
+ double releaseValue = ramp.output.getValue();
+ assertEquals("release " + i + " at", expectedAmplitude, releaseValue, tolerance);
+ }
+ time += releaseTime / numSteps;
+ synthesisEngine.sleepUntil(time);
+ double releaseValue = ramp.output.getValue();
+ assertEquals("env after release time should go to zero", 0.0, releaseValue, 0.0001);
+ }
+
+ public void testReleaseTiming() throws InterruptedException {
+ checkReleaseTiming(0.1, 0.004);
+ checkReleaseTiming(1.0, 0.002);
+ checkReleaseTiming(2.5, 0.001);
+ checkReleaseTiming(10.0, 0.001);
+ }
+
+}
diff --git a/tests/com/jsyn/unitgen/TestFunction.java b/tests/com/jsyn/unitgen/TestFunction.java
new file mode 100644
index 0000000..a8bfac0
--- /dev/null
+++ b/tests/com/jsyn/unitgen/TestFunction.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import junit.framework.TestCase;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import com.jsyn.data.DoubleTable;
+import com.jsyn.data.Function;
+
+/**
+ * @author Phil Burk, (C) 2009 Mobileer Inc
+ */
+public class TestFunction extends TestCase {
+ Synthesizer synth;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ synth = JSyn.createSynthesizer();
+ synth.setRealTime(false);
+ synth.start();
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ synth.stop();
+ }
+
+ public void testDoubleTable() {
+ double[] data = {
+ 2.0, 0.0, 3.0
+ };
+ DoubleTable table = new DoubleTable(data);
+ assertEquals("DoubleTable below", 2.0, table.evaluate(-1.4));
+ assertEquals("DoubleTable edge", 2.0, table.evaluate(-1.0));
+ assertEquals("DoubleTable mid", 1.0, table.evaluate(-0.5));
+ assertEquals("DoubleTable zero", 0.0, table.evaluate(0.0));
+ assertEquals("DoubleTable mid", 0.75, table.evaluate(0.25));
+ assertEquals("DoubleTable above", 3.0, table.evaluate(1.3));
+
+ }
+
+ public void testFunctionEvaluator() throws InterruptedException {
+ FunctionEvaluator shaper = new FunctionEvaluator();
+ synth.add(shaper);
+ shaper.start();
+
+ Function cuber = new Function() {
+ @Override
+ public double evaluate(double x) {
+ return x * x * x;
+ }
+ };
+ shaper.function.set(cuber);
+
+ shaper.input.set(0.5);
+ synth.sleepFor(0.001);
+
+ assertEquals("Cuber", (0.5 * 0.5 * 0.5), shaper.output.getValue());
+ }
+
+}
diff --git a/tests/com/jsyn/unitgen/TestMath.java b/tests/com/jsyn/unitgen/TestMath.java
new file mode 100644
index 0000000..0fde9b5
--- /dev/null
+++ b/tests/com/jsyn/unitgen/TestMath.java
@@ -0,0 +1,392 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import junit.framework.TestCase;
+
+import com.jsyn.engine.SynthesisEngine;
+
+/**
+ * @author Phil Burk, (C) 2009 Mobileer Inc
+ */
+public class TestMath extends TestCase {
+ SynthesisEngine synthesisEngine;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ synthesisEngine = new SynthesisEngine();
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ public void testAdd() {
+ Add add = new Add();
+ add.setSynthesisEngine(synthesisEngine);
+
+ double x = 33.99;
+ double y = 8.31;
+ add.inputA.setValueInternal(x);
+ add.inputB.setValueInternal(y);
+
+ add.generate();
+
+ assertEquals("Add", x + y, add.output.getValue(), 0.001);
+ }
+
+ public void testPartialAdd() {
+ Add add = new Add();
+ add.setSynthesisEngine(synthesisEngine);
+
+ double x = 2.5;
+ double y = 9.7;
+ add.inputA.setValueInternal(x);
+ add.inputB.setValueInternal(y);
+
+ // Only generate a few values in the middle.
+ // This is to test low latency feedback loops.
+ // Only generate values for 2,3,4
+ add.generate(2, 5);
+
+ assertEquals("Add partial", 0.0, add.output.getValues()[0], 0.001);
+ assertEquals("Add partial", 0.0, add.output.getValues()[1], 0.001);
+ assertEquals("Add partial", x + y, add.output.getValues()[2], 0.001);
+ assertEquals("Add partial", x + y, add.output.getValues()[3], 0.001);
+ assertEquals("Add partial", x + y, add.output.getValues()[4], 0.001);
+ assertEquals("Add partial", 0.0, add.output.getValues()[5], 0.001);
+ assertEquals("Add partial", 0.0, add.output.getValues()[6], 0.001);
+ assertEquals("Add partial", 0.0, add.output.getValues()[7], 0.001);
+
+ }
+
+ /**
+ * Unit test for Subtract.java - added by Lisa Tolentino 06/17/2009
+ */
+ public void testSubtract() {
+ Subtract sub = new Subtract();
+ sub.setSynthesisEngine(synthesisEngine);
+
+ double x = 33.99;
+ double y = 8.31;
+ sub.inputA.setValueInternal(x);
+ sub.inputB.setValueInternal(y);
+
+ sub.generate();
+
+ assertEquals("Subtract", x - y, sub.output.getValue(), 0.001);
+ }
+
+ public void testPartialSubtract() {
+ Subtract sub = new Subtract();
+ sub.setSynthesisEngine(synthesisEngine);
+
+ double x = 2.5;
+ double y = 9.7;
+ sub.inputA.setValueInternal(x);
+ sub.inputB.setValueInternal(y);
+
+ // Only generate a few values in the middle.
+ // This is to test low latency feedback loops.
+ // Only generate values for 2,3,4
+ sub.generate(2, 5);
+
+ assertEquals("Subtract partial", 0.0, sub.output.getValues()[0], 0.001);
+ assertEquals("Subtract partial", 0.0, sub.output.getValues()[1], 0.001);
+ assertEquals("Subtract partial", x - y, sub.output.getValues()[2], 0.001);
+ assertEquals("Subtract partial", x - y, sub.output.getValues()[3], 0.001);
+ assertEquals("Subtract partial", x - y, sub.output.getValues()[4], 0.001);
+ assertEquals("Subtract partial", 0.0, sub.output.getValues()[5], 0.001);
+ assertEquals("Subtract partial", 0.0, sub.output.getValues()[6], 0.001);
+ assertEquals("Subtract partial", 0.0, sub.output.getValues()[7], 0.001);
+
+ }
+
+ /**
+ * Unit test for Multiply.java - added by Lisa Tolentino 06/19/2009
+ */
+ public void testMultiply() {
+ Multiply mult = new Multiply();
+ mult.setSynthesisEngine(synthesisEngine);
+
+ double x = 33.99;
+ double y = 8.31;
+ mult.inputA.setValueInternal(x);
+ mult.inputB.setValueInternal(y);
+
+ mult.generate();
+
+ assertEquals("Multiply", x * y, mult.output.getValue(), 0.001);
+ }
+
+ public void testPartialMultiply() {
+ Multiply mult = new Multiply();
+ mult.setSynthesisEngine(synthesisEngine);
+
+ double x = 2.5;
+ double y = 9.7;
+ mult.inputA.setValueInternal(x);
+ mult.inputB.setValueInternal(y);
+
+ // Only generate a few values in the middle.
+ // This is to test low latency feedback loops.
+ // Only generate values for 2,3,4
+ mult.generate(2, 5);
+
+ assertEquals("Multiply partial", 0.0, mult.output.getValues()[0], 0.001);
+ assertEquals("Multiply partial", 0.0, mult.output.getValues()[1], 0.001);
+ assertEquals("Multiply partial", x * y, mult.output.getValues()[2], 0.001);
+ assertEquals("Multiply partial", x * y, mult.output.getValues()[3], 0.001);
+ assertEquals("Multiply partial", x * y, mult.output.getValues()[4], 0.001);
+ assertEquals("Multiply partial", 0.0, mult.output.getValues()[5], 0.001);
+ assertEquals("Multiply partial", 0.0, mult.output.getValues()[6], 0.001);
+ assertEquals("Multiply partial", 0.0, mult.output.getValues()[7], 0.001);
+
+ }
+
+ /**
+ * Unit test for Divide.java - added by Lisa Tolentino 06/19/2009
+ */
+ public void testDivide() {
+ Divide divide = new Divide();
+ divide.setSynthesisEngine(synthesisEngine);
+
+ double x = 33.99;
+ double y = 8.31;
+ divide.inputA.setValueInternal(x);
+ divide.inputB.setValueInternal(y);
+
+ divide.generate();
+
+ assertEquals("Divide", x / y, divide.output.getValue(), 0.001);
+ }
+
+ public void testPartialDivide() {
+ Divide divide = new Divide();
+ divide.setSynthesisEngine(synthesisEngine);
+
+ double x = 2.5;
+ double y = 9.7;
+ divide.inputA.setValueInternal(x);
+ divide.inputB.setValueInternal(y);
+
+ // Only generate a few values in the middle.
+ // This is to test low latency feedback loops.
+ // Only generate values for 2,3,4
+ divide.generate(2, 5);
+
+ assertEquals("Divide partial", 0.0, divide.output.getValues()[0], 0.001);
+ assertEquals("Divide partial", 0.0, divide.output.getValues()[1], 0.001);
+ assertEquals("Divide partial", x / y, divide.output.getValues()[2], 0.001);
+ assertEquals("Divide partial", x / y, divide.output.getValues()[3], 0.001);
+ assertEquals("Divide partial", x / y, divide.output.getValues()[4], 0.001);
+ assertEquals("Divide partial", 0.0, divide.output.getValues()[5], 0.001);
+ assertEquals("Divide partial", 0.0, divide.output.getValues()[6], 0.001);
+ assertEquals("Divide partial", 0.0, divide.output.getValues()[7], 0.001);
+
+ }
+
+ /**
+ * Unit test for MultiplyAdd.java - added by Lisa Tolentino 06/19/2009
+ */
+ public void testMultiplyAdd() {
+ MultiplyAdd multAdd = new MultiplyAdd();
+ multAdd.setSynthesisEngine(synthesisEngine);
+
+ double x = 33.99;
+ double y = 8.31;
+ double z = 2.28;
+ multAdd.inputA.setValueInternal(x);
+ multAdd.inputB.setValueInternal(y);
+ multAdd.inputC.setValueInternal(z);
+
+ multAdd.generate();
+
+ assertEquals("MultiplyAdd", (x * y) + z, multAdd.output.getValue(), 0.001);
+ }
+
+ public void testPartialMultiplyAdd() {
+ MultiplyAdd multAdd = new MultiplyAdd();
+ multAdd.setSynthesisEngine(synthesisEngine);
+
+ double x = 33.99;
+ double y = 8.31;
+ double z = 2.28;
+ multAdd.inputA.setValueInternal(x);
+ multAdd.inputB.setValueInternal(y);
+ multAdd.inputC.setValueInternal(z);
+
+ // Only generate a few values in the middle.
+ // This is to test low latency feedback loops.
+ // Only generate values for 2,3,4
+ multAdd.generate(2, 5);
+
+ assertEquals("MultiplyAdd partial", 0.0, multAdd.output.getValues()[0], 0.001);
+ assertEquals("MultiplyAdd partial", 0.0, multAdd.output.getValues()[1], 0.001);
+ assertEquals("MultiplyAdd partial", (x * y) + z, multAdd.output.getValues()[2], 0.001);
+ assertEquals("MultiplyAdd partial", (x * y) + z, multAdd.output.getValues()[3], 0.001);
+ assertEquals("MultiplyAdd partial", (x * y) + z, multAdd.output.getValues()[4], 0.001);
+ assertEquals("MultiplyAdd partial", 0.0, multAdd.output.getValues()[5], 0.001);
+ assertEquals("MultiplyAdd partial", 0.0, multAdd.output.getValues()[6], 0.001);
+ assertEquals("MultiplyAdd partial", 0.0, multAdd.output.getValues()[7], 0.001);
+
+ }
+
+ /**
+ * Unit test for Compare.java - added by Lisa Tolentino 06/19/2009
+ */
+ public void testCompare() {
+ UnitBinaryOperator compare = new Compare();
+ compare.setSynthesisEngine(synthesisEngine);
+
+ double x = 33.99;
+ double y = 8.31;
+ compare.inputA.setValueInternal(x);
+ compare.inputB.setValueInternal(y);
+
+ compare.generate();
+
+ assertEquals("Compare", (x > y ? 1 : 0), compare.output.getValue(), 0.001);
+ }
+
+ public void testPartialCompare() {
+ UnitBinaryOperator compare = new Compare();
+ compare.setSynthesisEngine(synthesisEngine);
+
+ double x = 33.99;
+ double y = 8.31;
+ compare.inputA.setValueInternal(x);
+ compare.inputB.setValueInternal(y);
+
+ // Only generate a few values in the middle.
+ // This is to test low latency feedback loops.
+ // Only generate values for 2,3,4
+ compare.generate(2, 5);
+
+ assertEquals("Compare partial", 0.0, compare.output.getValues()[0], 0.001);
+ assertEquals("Compare partial", 0.0, compare.output.getValues()[1], 0.001);
+ assertEquals("Compare partial", (x > y ? 1 : 0), compare.output.getValues()[2], 0.001);
+ assertEquals("Compare partial", (x > y ? 1 : 0), compare.output.getValues()[3], 0.001);
+ assertEquals("Compare partial", (x > y ? 1 : 0), compare.output.getValues()[4], 0.001);
+ assertEquals("Compare partial", 0.0, compare.output.getValues()[5], 0.001);
+ assertEquals("Compare partial", 0.0, compare.output.getValues()[6], 0.001);
+ assertEquals("Compare partial", 0.0, compare.output.getValues()[7], 0.001);
+
+ }
+
+ /**
+ * Unit test for Maximum.java - added by Lisa Tolentino 06/20/2009
+ */
+ public void testMaximum() {
+ Maximum max = new Maximum();
+ max.setSynthesisEngine(synthesisEngine);
+
+ double x = 33.99;
+ double y = 8.31;
+ max.inputA.setValueInternal(x);
+ max.inputB.setValueInternal(y);
+
+ max.generate();
+
+ assertEquals("Maximum", (x > y ? x : y), max.output.getValue(), 0.001);
+ }
+
+ public void testPartialMaximum() {
+ Maximum max = new Maximum();
+ max.setSynthesisEngine(synthesisEngine);
+
+ double x = 33.99;
+ double y = 8.31;
+ max.inputA.setValueInternal(x);
+ max.inputB.setValueInternal(y);
+
+ // Only generate a few values in the middle.
+ // This is to test low latency feedback loops.
+ // Only generate values for 2,3,4
+ max.generate(2, 5);
+
+ assertEquals("Maximum partial", 0.0, max.output.getValues()[0], 0.001);
+ assertEquals("Maximum partial", 0.0, max.output.getValues()[1], 0.001);
+ assertEquals("Maximum partial", (x > y ? x : y), max.output.getValues()[2], 0.001);
+ assertEquals("Maximum partial", (x > y ? x : y), max.output.getValues()[3], 0.001);
+ assertEquals("Maximum partial", (x > y ? x : y), max.output.getValues()[4], 0.001);
+ assertEquals("Maximum partial", 0.0, max.output.getValues()[5], 0.001);
+ assertEquals("Maximum partial", 0.0, max.output.getValues()[6], 0.001);
+ assertEquals("Maximum partial", 0.0, max.output.getValues()[7], 0.001);
+
+ }
+
+ /**
+ * Unit test for Minimum.java - added by Lisa Tolentino 06/20/2009
+ */
+ public void testMinimum() {
+ Minimum min = new Minimum();
+ min.setSynthesisEngine(synthesisEngine);
+
+ double x = 33.99;
+ double y = 8.31;
+ min.inputA.setValueInternal(x);
+ min.inputB.setValueInternal(y);
+
+ min.generate();
+
+ assertEquals("Minimum", (x < y ? x : y), min.output.getValue(), 0.001);
+ }
+
+ public void testPartialMinimum() {
+ Minimum min = new Minimum();
+ min.setSynthesisEngine(synthesisEngine);
+
+ double x = 33.99;
+ double y = 8.31;
+ min.inputA.setValueInternal(x);
+ min.inputB.setValueInternal(y);
+
+ // Only generate a few values in the middle.
+ // This is to test low latency feedback loops.
+ // Only generate values for 2,3,4
+ min.generate(2, 5);
+
+ assertEquals("Maximum partial", 0.0, min.output.getValues()[0], 0.001);
+ assertEquals("Maximum partial", 0.0, min.output.getValues()[1], 0.001);
+ assertEquals("Maximum partial", (x < y ? x : y), min.output.getValues()[2], 0.001);
+ assertEquals("Maximum partial", (x < y ? x : y), min.output.getValues()[3], 0.001);
+ assertEquals("Maximum partial", (x < y ? x : y), min.output.getValues()[4], 0.001);
+ assertEquals("Maximum partial", 0.0, min.output.getValues()[5], 0.001);
+ assertEquals("Maximum partial", 0.0, min.output.getValues()[6], 0.001);
+ assertEquals("Maximum partial", 0.0, min.output.getValues()[7], 0.001);
+
+ }
+
+ public void testPowerOfTwo() {
+ PowerOfTwo powerOfTwo = new PowerOfTwo();
+ powerOfTwo.setSynthesisEngine(synthesisEngine);
+ final double smallValue = -1.5308084989341915E-17;
+ double values[] = {
+ 0.0, 1.3, 4.5, -0.5, -1.0, -2.8, smallValue, -smallValue, 1.0 - smallValue,
+ 1.0 + smallValue
+ };
+ for (double in : values) {
+ powerOfTwo.input.setValueInternal(in);
+ powerOfTwo.generate();
+ assertEquals("PowerOfTwo", Math.pow(2.0, in), powerOfTwo.output.getValue(), 0.001);
+ }
+ }
+
+}
diff --git a/tests/com/jsyn/unitgen/TestRamps.java b/tests/com/jsyn/unitgen/TestRamps.java
new file mode 100644
index 0000000..83ebacf
--- /dev/null
+++ b/tests/com/jsyn/unitgen/TestRamps.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+public class TestRamps extends NonRealTimeTestCase {
+
+ public void viewContinuousRamp(double duration, double startValue, double targetValue)
+ throws InterruptedException {
+ ContinuousRamp ramp = new ContinuousRamp();
+ synthesisEngine.add(ramp);
+
+ ramp.current.set(startValue);
+ ramp.input.set(startValue);
+ ramp.time.set(duration);
+
+ synthesisEngine.setRealTime(false);
+ synthesisEngine.start();
+ ramp.start();
+ synthesisEngine.sleepUntil(synthesisEngine.getCurrentTime() + 0.01);
+ ramp.input.set(targetValue);
+
+ double time = synthesisEngine.getCurrentTime();
+ int numLoops = 20;
+ double increment = duration / numLoops;
+ for (int i = 0; i < (numLoops + 1); i++) {
+ double value = ramp.output.getValue();
+ System.out.printf("i = %d, t = %9.5f, value = %8.4f\n", i, time, value);
+ time += increment;
+ synthesisEngine.sleepUntil(time);
+ }
+
+ synthesisEngine.stop();
+ }
+
+ public void checkContinuousRamp(double duration, double startValue, double targetValue)
+ throws InterruptedException {
+ ContinuousRamp ramp = new ContinuousRamp();
+ synthesisEngine.add(ramp);
+
+ ramp.current.set(startValue);
+ ramp.input.set(startValue);
+ ramp.time.set(duration);
+
+ synthesisEngine.setRealTime(false);
+ synthesisEngine.start();
+ ramp.start();
+ synthesisEngine.sleepUntil(synthesisEngine.getCurrentTime() + 0.01);
+ assertEquals("start flat", ramp.input.getValue(), ramp.output.getValue());
+
+ ramp.input.set(targetValue);
+ double startTime = synthesisEngine.getCurrentTime();
+ synthesisEngine.sleepUntil(startTime + (duration / 2));
+ assertEquals("ramping up", (targetValue + startValue) / 2.0, ramp.output.getValue(), 0.01);
+ synthesisEngine.sleepUntil(startTime + duration);
+ assertEquals("ramping up", targetValue, ramp.output.getValue(), 0.01);
+ synthesisEngine.sleepUntil(startTime + duration + 0.1);
+ assertEquals("flat again", targetValue, ramp.output.getValue());
+
+ synthesisEngine.stop();
+ }
+
+ public void testContinuousRamp() throws InterruptedException {
+ viewContinuousRamp(4.0, 0.0, 1.0);
+ }
+
+ public void testExponentialRamp() throws InterruptedException {
+ ExponentialRamp ramp = new ExponentialRamp();
+ synthesisEngine.add(ramp);
+
+ double duration = 0.3;
+ ramp.current.set(1.0);
+ ramp.input.set(1.0);
+ ramp.time.set(duration);
+
+ synthesisEngine.start();
+ ramp.start();
+ synthesisEngine.sleepUntil(synthesisEngine.getCurrentTime() + 0.01);
+ assertEquals("start flat", ramp.input.getValue(), ramp.output.getValue());
+
+ ramp.input.set(8.0);
+ double startTime = synthesisEngine.getCurrentTime();
+ synthesisEngine.sleepUntil(startTime + 0.1);
+ assertEquals("ramping up", 2.0, ramp.output.getValue(), 0.01);
+ synthesisEngine.sleepUntil(startTime + 0.2);
+ assertEquals("ramping up", 4.0, ramp.output.getValue(), 0.01);
+ synthesisEngine.sleepUntil(startTime + 0.3);
+ assertEquals("ramping up", 8.0, ramp.output.getValue(), 0.01);
+ synthesisEngine.sleepUntil(startTime + 0.4);
+ assertEquals("flat again", 8.0, ramp.output.getValue());
+ }
+
+ public void testLinearRamp() throws InterruptedException {
+ LinearRamp ramp = new LinearRamp();
+ synthesisEngine.add(ramp);
+
+ double duration = 0.4;
+ ramp.current.set(0.0);
+ ramp.input.set(0.0);
+ ramp.time.set(duration);
+
+ synthesisEngine.start();
+ ramp.start();
+ synthesisEngine.sleepUntil(synthesisEngine.getCurrentTime() + 0.01);
+ assertEquals("start flat", ramp.input.getValue(), ramp.output.getValue());
+
+ ramp.input.set(8.0);
+ double startTime = synthesisEngine.getCurrentTime();
+ synthesisEngine.sleepUntil(startTime + 0.1);
+ assertEquals("ramping up", 2.0, ramp.output.getValue(), 0.01);
+ synthesisEngine.sleepUntil(startTime + 0.2);
+ assertEquals("ramping up", 4.0, ramp.output.getValue(), 0.01);
+ synthesisEngine.sleepUntil(startTime + 0.3);
+ assertEquals("ramping up", 6.0, ramp.output.getValue(), 0.01);
+ synthesisEngine.sleepUntil(startTime + 0.4);
+ assertEquals("flat again", 8.0, ramp.output.getValue());
+ }
+
+ public void testExponentialRampConnected() throws InterruptedException {
+ ExponentialRamp ramp = new ExponentialRamp();
+ PassThrough pass = new PassThrough();
+ synthesisEngine.add(ramp);
+ synthesisEngine.add(pass);
+
+ double duration = 0.3;
+ ramp.current.set(1.0);
+ pass.input.set(1.0);
+ ramp.time.set(duration);
+
+ // Send value through a connected unit.
+ pass.input.set(1.0);
+ pass.output.connect(ramp.input);
+
+ synthesisEngine.start();
+ ramp.start();
+ synthesisEngine.sleepUntil(synthesisEngine.getCurrentTime() + 0.01);
+ assertEquals("start flat", ramp.input.getValue(), ramp.output.getValue());
+
+ pass.input.set(8.0);
+ double startTime = synthesisEngine.getCurrentTime();
+ synthesisEngine.sleepUntil(startTime + 0.1);
+ assertEquals("ramping up", 2.0, ramp.output.getValue(), 0.01);
+ synthesisEngine.sleepUntil(startTime + 0.2);
+ assertEquals("ramping up", 4.0, ramp.output.getValue(), 0.01);
+ synthesisEngine.sleepUntil(startTime + 0.3);
+ assertEquals("ramping up", 8.0, ramp.output.getValue(), 0.01);
+ synthesisEngine.sleepUntil(startTime + 0.4);
+ assertEquals("flat again", 8.0, ramp.output.getValue());
+ }
+
+ public void testLinearRampConnected() throws InterruptedException {
+ LinearRamp ramp = new LinearRamp();
+ PassThrough pass = new PassThrough();
+ synthesisEngine.add(ramp);
+ synthesisEngine.add(pass);
+
+ double duration = 0.4;
+ ramp.current.set(0.0);
+ pass.input.set(0.0);
+ ramp.time.set(duration);
+
+ // Send value through a connected unit.
+ pass.input.set(0.0);
+ pass.output.connect(ramp.input);
+
+ synthesisEngine.start();
+ ramp.start();
+ synthesisEngine.sleepUntil(synthesisEngine.getCurrentTime() + 0.01);
+ assertEquals("start flat", ramp.input.getValue(), ramp.output.getValue());
+
+ pass.input.set(8.0);
+ double startTime = synthesisEngine.getCurrentTime();
+ synthesisEngine.sleepUntil(startTime + 0.1);
+ assertEquals("ramping up", 2.0, ramp.output.getValue(), 0.01);
+ synthesisEngine.sleepUntil(startTime + 0.2);
+ assertEquals("ramping up", 4.0, ramp.output.getValue(), 0.01);
+ synthesisEngine.sleepUntil(startTime + 0.3);
+ assertEquals("ramping up", 6.0, ramp.output.getValue(), 0.01);
+ synthesisEngine.sleepUntil(startTime + 0.4);
+ assertEquals("flat again", 8.0, ramp.output.getValue());
+ }
+
+}
diff --git a/tests/com/jsyn/unitgen/TestUnitGate.java b/tests/com/jsyn/unitgen/TestUnitGate.java
new file mode 100644
index 0000000..14129aa
--- /dev/null
+++ b/tests/com/jsyn/unitgen/TestUnitGate.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.unitgen;
+
+import junit.framework.TestCase;
+
+import com.jsyn.engine.SynthesisEngine;
+
+public class TestUnitGate extends TestCase {
+
+ protected SynthesisEngine synthesisEngine;
+ protected double time;
+
+ public void checkAutoDisable(LinearRamp ramp, UnitGate envelope) throws InterruptedException {
+ double tolerance = 0.01;
+ Add adder = new Add();
+ synthesisEngine.add(adder);
+
+ envelope.output.connect(adder.inputA);
+ if (ramp.getCircuit() != null) {
+ ramp.output.connect(adder.inputB);
+ }
+
+ envelope.input.setAutoDisableEnabled(true);
+ envelope.setEnabled(false);
+
+ // set up so ramp value should equal time
+ ramp.current.set(0.0);
+ ramp.input.set(1.0);
+ ramp.time.set(1.0);
+
+ synthesisEngine.start();
+ // pull from final adder
+ adder.start();
+
+ time = synthesisEngine.getCurrentTime();
+ time += 0.1;
+ synthesisEngine.sleepUntil(time);
+ assertEquals("still idling", 0.0, envelope.output.getValue());
+ assertEquals("ramp frozen at beginning", 0.0, ramp.output.getValue(), tolerance);
+
+ // run multiple times to make sure we can retrigger the envelope.
+ for (int i = 0; i < 3; i++) {
+ double level = ramp.output.getValue();
+ // Trigger the envelope using trigger()
+ envelope.input.on();
+ time += 0.1;
+ level += 0.1;
+ synthesisEngine.sleepUntil(time);
+ assertEquals("ramp going up " + i, level, ramp.output.getValue(), tolerance);
+ assertTrue("enabled at peak", envelope.isEnabled());
+
+ envelope.input.off();
+ time += 0.1;
+ level += 0.1;
+ synthesisEngine.sleepUntil(time);
+ assertEquals("ramp going up more " + i, level, ramp.output.getValue(), tolerance);
+ assertEquals("at bottom", 0.0, envelope.output.getValue(), 0.1);
+
+ time += 0.2;
+ synthesisEngine.sleepUntil(time);
+ assertEquals("ramp frozen " + i, level, ramp.output.getValue(), tolerance);
+ }
+ }
+
+}
diff --git a/tests/com/jsyn/util/DebugSampleLoader.java b/tests/com/jsyn/util/DebugSampleLoader.java
new file mode 100644
index 0000000..23945b5
--- /dev/null
+++ b/tests/com/jsyn/util/DebugSampleLoader.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2010 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util;
+
+import java.io.File;
+import java.io.IOException;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import com.jsyn.data.FloatSample;
+import com.jsyn.unitgen.LineOut;
+import com.jsyn.unitgen.VariableRateDataReader;
+import com.jsyn.unitgen.VariableRateMonoReader;
+import com.jsyn.unitgen.VariableRateStereoReader;
+
+/**
+ * Play a sample from a WAV file using JSyn.
+ *
+ * @author Phil Burk (C) 2010 Mobileer Inc
+ */
+public class DebugSampleLoader {
+ private Synthesizer synth;
+ private VariableRateDataReader samplePlayer;
+ private LineOut lineOut;
+
+ private void test() throws IOException {
+ // File sampleFile = new File("samples/cello_markers.wav");
+ // File sampleFile = new File("samples/Piano_A440_PT.aif");
+ File sampleFile = new File("samples/sine_400_loop_i16.wav");
+ // File sampleFile = new File("samples/TwoDiffPitchedSines_F32_PT.wav");
+ // File sampleFile = new File("samples/sine_400_u8.aif");
+ // File sampleFile = new File("samples/sine_400_s8.aif");
+ // File sampleFile = new File("samples/sine_400_ulaw.aif");
+ // File sampleFile = new File("samples/sine_400_ulaw.wav");
+
+ // File sampleFile = new File("samples/aaClarinet.wav");
+ // File sampleFile = new File("samples/sine_400_mono.wav");
+ // File sampleFile = new File("samples/sine_200_300_i16.wav");
+ // File sampleFile = new File("samples/sine_200_300_i24.wav");
+ // File sampleFile = new File("samples/M1F1-int16-AFsp.wav");
+ // File sampleFile = new File("samples/M1F1-int24-AFsp.wav");
+ // File sampleFile = new File("samples/M1F1-float32-AFsp.wav");
+ // File sampleFile = new File("samples/M1F1-int16WE-AFsp.wav");
+ // File sampleFile = new File("samples/M1F1-int24WE-AFsp.wav");
+ // File sampleFile = new File("samples/M1F1-float32WE-AFsp.wav");
+ // File sampleFile = new File("samples/sine_200_300_i16.aif");
+ // File sampleFile = new File("samples/sine_200_300_f32.wavex");
+ // File sampleFile = new File("samples/Sine32bit.aif");
+ // File sampleFile = new File("samples/Sine32bit.wav");
+ // File sampleFile = new File("samples/smartCue.wav");
+
+ // URL sampleFile = new URL("http://www.softsynth.com/samples/Clarinet.wav");
+
+ synth = JSyn.createSynthesizer();
+
+ FloatSample sample;
+ try {
+ // Add an output mixer.
+ synth.add(lineOut = new LineOut());
+
+ // Load the sample and display its properties.
+ SampleLoader.setJavaSoundPreferred(false);
+ sample = SampleLoader.loadFloatSample(sampleFile);
+ System.out.println("Sample has: channels = " + sample.getChannelsPerFrame());
+ System.out.println(" frames = " + sample.getNumFrames());
+ System.out.println(" rate = " + sample.getFrameRate());
+ System.out.println(" loopStart = " + sample.getSustainBegin());
+ System.out.println(" loopEnd = " + sample.getSustainEnd());
+
+ if (sample.getChannelsPerFrame() == 1) {
+ synth.add(samplePlayer = new VariableRateMonoReader());
+ samplePlayer.output.connect(0, lineOut.input, 0);
+ } else if (sample.getChannelsPerFrame() == 2) {
+ synth.add(samplePlayer = new VariableRateStereoReader());
+ samplePlayer.output.connect(0, lineOut.input, 0);
+ samplePlayer.output.connect(1, lineOut.input, 1);
+ } else {
+ throw new RuntimeException("Can only play mono or stereo samples.");
+ }
+
+ // Start synthesizer using default stereo output at 44100 Hz.
+ synth.start();
+
+ samplePlayer.rate.set(sample.getFrameRate());
+
+ // We only need to start the LineOut. It will pull data from the
+ // sample player.
+ lineOut.start();
+
+ // We can simply queue the entire file.
+ // Or if it has a loop we can play the loop for a while.
+ if (sample.getSustainBegin() < 0) {
+ System.out.println("queue the sample");
+ samplePlayer.dataQueue.queue(sample);
+ } else {
+ System.out.println("queueOn the sample");
+ samplePlayer.dataQueue.queueOn(sample);
+ synth.sleepFor(8.0);
+ System.out.println("queueOff the sample");
+ samplePlayer.dataQueue.queueOff(sample);
+ }
+
+ // Wait until the sample has finished playing.
+ do {
+ synth.sleepFor(1.0);
+ } while (samplePlayer.dataQueue.hasMore());
+
+ } catch (IOException e1) {
+ e1.printStackTrace();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ // Stop everything.
+ synth.stop();
+ }
+
+ public static void main(String[] args) {
+ try {
+ new DebugSampleLoader().test();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/tests/com/jsyn/util/TestFFT.java b/tests/com/jsyn/util/TestFFT.java
new file mode 100644
index 0000000..f7fcce6
--- /dev/null
+++ b/tests/com/jsyn/util/TestFFT.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util;
+
+import junit.framework.TestCase;
+
+import com.softsynth.math.FourierMath;
+
+public class TestFFT extends TestCase {
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ public void checkSingleSine(int size, int bin) {
+ double[] ar = new double[size];
+ double[] ai = new double[size];
+ double[] magnitudes = new double[size];
+
+ double amplitude = 1.0;
+ addSineWave(size, bin, ar, amplitude);
+
+ FourierMath.transform(1, size, ar, ai);
+ FourierMath.calculateMagnitudes(ar, ai, magnitudes);
+
+ assertTrue(magnitudes[bin - 1] < 0.001);
+ assertTrue(magnitudes[bin] > 0.5);
+ assertTrue(magnitudes[bin + 1] < 0.001);
+
+ }
+
+ private void addSineWave(int size, int bin, double[] ar, double amplitude) {
+ double phase = 0.0;
+ double phaseIncrement = 2.0 * Math.PI * bin / size;
+ for (int i = 0; i < size; i++) {
+ ar[i] += Math.sin(phase) * amplitude;
+ // System.out.println( i + " = " + ar[i] );
+ phase += phaseIncrement;
+ }
+ }
+
+ public void testSingles() {
+ checkSingleSine(32, 1);
+ checkSingleSine(32, 2);
+ checkSingleSine(64, 5);
+ checkSingleSine(256, 3);
+ }
+
+ public void checkInverseFFT(int size, int bin) {
+ double[] ar1 = new double[size];
+ double[] ai1 = new double[size];
+ double[] ar2 = new double[size];
+ double[] ai2 = new double[size];
+
+ double amplitude = 1.0;
+ addSineWave(size, bin, ar1, amplitude);
+
+ // Save a copy of the source.
+ System.arraycopy(ar1, 0, ar2, 0, size);
+ System.arraycopy(ai1, 0, ai2, 0, size);
+
+ FourierMath.transform(1, size, ar1, ai1); // FFT
+
+ FourierMath.transform(-1, size, ar1, ai1); // IFFT
+
+ for (int i = 0; i < size; i++) {
+ assertEquals(ar2[i], ar1[i], 0.00001);
+ assertEquals(ai2[i], ai1[i], 0.00001);
+ }
+ }
+
+ public void testInverse() {
+ checkInverseFFT(32, 1);
+ checkInverseFFT(32, 2);
+ checkInverseFFT(128, 17);
+ checkInverseFFT(512, 23);
+ }
+}
diff --git a/tests/com/jsyn/util/TestPseudoRandom.java b/tests/com/jsyn/util/TestPseudoRandom.java
new file mode 100644
index 0000000..0ef2fa3
--- /dev/null
+++ b/tests/com/jsyn/util/TestPseudoRandom.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util;
+
+import junit.framework.TestCase;
+
+public class TestPseudoRandom extends TestCase {
+ PseudoRandom pseudoRandom;
+ private int[] bins;
+ private final static int BIN_SHIFTER = 8;
+ private final static int BIN_COUNT = 1 << BIN_SHIFTER;
+ private final static int BIN_MASK = BIN_COUNT - 1;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ pseudoRandom = new PseudoRandom();
+ bins = new int[BIN_COUNT];
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ public void testMath() {
+ long seed = 3964771111L;
+ int positiveInt = (int) (seed & 0x7FFFFFFF);
+ assertTrue("masked random positive, " + positiveInt, (positiveInt >= 0));
+ double rand = positiveInt * (1.0 / (1L << 31));
+ assertTrue("not too low, " + rand, (rand >= 0.0));
+ assertTrue("not too high, " + rand, (rand < 1.0));
+ }
+
+ public void testIntegerDistribution() {
+ int scaler = 10;
+ for (int i = 0; i < (bins.length * scaler); i++) {
+ int rand = pseudoRandom.nextRandomInteger();
+ int positiveInt = rand & 0x7FFFFFFF;
+ assertTrue("masked random " + positiveInt, (positiveInt >= 0));
+ int index = (rand >> (32 - BIN_SHIFTER)) & BIN_MASK;
+ bins[index] += 1;
+ }
+ checkDistribution(scaler);
+ }
+
+ public void test01Distribution() {
+ int scaler = 10;
+ for (int i = 0; i < (bins.length * scaler); i++) {
+ double rand = pseudoRandom.random();
+ assertTrue("not too low, #" + i + " = " + rand, (rand >= 0.0));
+ assertTrue("not too high, #" + i + " = " + rand, (rand < 1.0));
+ int index = (int) (rand * BIN_COUNT);
+ bins[index] += 1;
+ }
+ checkDistribution(scaler);
+ }
+
+ private void checkDistribution(int scaler) {
+ // Generate running average that should stay near scaler
+ double average = scaler;
+ double coefficient = 0.9;
+ for (int i = 0; i < (bins.length); i++) {
+ average = (average * coefficient) + (bins[i] * (1.0 - coefficient));
+ assertEquals("average at " + i, scaler, average, 0.2 * scaler);
+ }
+ }
+}
diff --git a/tests/com/jsyn/util/TestVoiceAllocator.java b/tests/com/jsyn/util/TestVoiceAllocator.java
new file mode 100644
index 0000000..fd5ba0b
--- /dev/null
+++ b/tests/com/jsyn/util/TestVoiceAllocator.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2009 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.jsyn.util;
+
+import junit.framework.TestCase;
+
+import com.jsyn.instruments.SubtractiveSynthVoice;
+import com.jsyn.unitgen.UnitVoice;
+
+public class TestVoiceAllocator extends TestCase {
+ VoiceAllocator allocator;
+ int max = 4;
+ private UnitVoice[] voices;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ voices = new UnitVoice[max];
+ for (int i = 0; i < max; i++) {
+ voices[i] = new SubtractiveSynthVoice();
+ }
+
+ allocator = new VoiceAllocator(voices);
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ public void testAllocation() {
+ assertEquals("get max", max, allocator.getVoiceCount());
+
+ int tag1 = 61;
+ int tag2 = 62;
+ int tag3 = 63;
+ int tag4 = 64;
+ int tag5 = 65;
+ int tag6 = 66;
+ UnitVoice voice1 = allocator.allocate(tag1);
+ assertTrue("voice should be non-null", (voice1 != null));
+
+ UnitVoice voice2 = allocator.allocate(tag2);
+ assertTrue("voice should be non-null", (voice2 != null));
+ assertTrue("new voice ", (voice2 != voice1));
+
+ UnitVoice voice = allocator.allocate(tag1);
+ assertTrue("should be voice1 again ", (voice == voice1));
+
+ voice = allocator.allocate(tag2);
+ assertTrue("should be voice2 again ", (voice == voice2));
+
+ UnitVoice voice3 = allocator.allocate(tag3);
+ @SuppressWarnings("unused")
+ UnitVoice voice4 = allocator.allocate(tag4);
+
+ UnitVoice voice5 = allocator.allocate(tag5);
+ assertTrue("ran out so get voice1 as oldest", (voice5 == voice1));
+
+ voice = allocator.allocate(tag2);
+ assertTrue("should be voice2 again ", (voice == voice2));
+
+ // Now voice 3 should be the oldest cuz voice 2 was touched.
+ UnitVoice voice6 = allocator.allocate(tag6);
+ assertTrue("ran out so get voice3 as oldest", (voice6 == voice3));
+ }
+
+ public void testOff() {
+ int tag1 = 61;
+ int tag2 = 62;
+ int tag3 = 63;
+ int tag4 = 64;
+ int tag5 = 65;
+ int tag6 = 66;
+ UnitVoice voice1 = allocator.allocate(tag1);
+ UnitVoice voice2 = allocator.allocate(tag2);
+ UnitVoice voice3 = allocator.allocate(tag3);
+ UnitVoice voice4 = allocator.allocate(tag4);
+
+ assertTrue("voice 3 should start on", allocator.isOn(tag3));
+ allocator.off(tag3);
+ assertEquals("voice 3 should now be off", false, allocator.isOn(tag3));
+
+ allocator.off(tag2);
+
+ UnitVoice voice5 = allocator.allocate(tag5);
+ assertTrue("should get voice3 cuz off first", (voice5 == voice3));
+ UnitVoice voice6 = allocator.allocate(tag6);
+ assertTrue("should get voice2 cuz off second", (voice6 == voice2));
+ voice3 = allocator.allocate(tag3);
+ assertTrue("should get voice1 cuz on first", (voice3 == voice1));
+
+ voice1 = allocator.allocate(tag1);
+ assertTrue("should get voice4 cuz next up", (voice1 == voice4));
+ }
+}