aboutsummaryrefslogtreecommitdiffstats
path: root/src/main/java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java')
-rw-r--r--src/main/java/com/jsyn/JSyn.java78
-rw-r--r--src/main/java/com/jsyn/Synthesizer.java202
-rw-r--r--src/main/java/com/jsyn/apps/AboutJSyn.java114
-rw-r--r--src/main/java/com/jsyn/apps/InstrumentTester.java210
-rw-r--r--src/main/java/com/jsyn/data/AudioSample.java108
-rw-r--r--src/main/java/com/jsyn/data/DoubleTable.java109
-rw-r--r--src/main/java/com/jsyn/data/FloatSample.java164
-rw-r--r--src/main/java/com/jsyn/data/Function.java35
-rw-r--r--src/main/java/com/jsyn/data/HammingWindow.java41
-rw-r--r--src/main/java/com/jsyn/data/HannWindow.java36
-rw-r--r--src/main/java/com/jsyn/data/SampleMarker.java30
-rw-r--r--src/main/java/com/jsyn/data/SegmentedEnvelope.java125
-rw-r--r--src/main/java/com/jsyn/data/SequentialData.java96
-rw-r--r--src/main/java/com/jsyn/data/SequentialDataCommon.java136
-rw-r--r--src/main/java/com/jsyn/data/ShortSample.java123
-rw-r--r--src/main/java/com/jsyn/data/SpectralWindow.java21
-rw-r--r--src/main/java/com/jsyn/data/SpectralWindowFactory.java55
-rw-r--r--src/main/java/com/jsyn/data/Spectrum.java97
-rw-r--r--src/main/java/com/jsyn/devices/AudioDeviceFactory.java93
-rw-r--r--src/main/java/com/jsyn/devices/AudioDeviceInputStream.java31
-rw-r--r--src/main/java/com/jsyn/devices/AudioDeviceManager.java120
-rw-r--r--src/main/java/com/jsyn/devices/AudioDeviceOutputStream.java30
-rw-r--r--src/main/java/com/jsyn/devices/javasound/JavaSoundAudioDevice.java432
-rw-r--r--src/main/java/com/jsyn/devices/javasound/MidiDeviceTools.java86
-rw-r--r--src/main/java/com/jsyn/devices/jportaudio/JPortAudioDevice.java264
-rw-r--r--src/main/java/com/jsyn/engine/LoadAnalyzer.java61
-rw-r--r--src/main/java/com/jsyn/engine/MultiTable.java230
-rw-r--r--src/main/java/com/jsyn/engine/SynthesisEngine.java700
-rw-r--r--src/main/java/com/jsyn/exceptions/ChannelMismatchException.java35
-rw-r--r--src/main/java/com/jsyn/instruments/DrumWoodFM.java159
-rw-r--r--src/main/java/com/jsyn/instruments/DualOscillatorSynthVoice.java301
-rw-r--r--src/main/java/com/jsyn/instruments/JSynInstrumentLibrary.java48
-rw-r--r--src/main/java/com/jsyn/instruments/NoiseHit.java114
-rw-r--r--src/main/java/com/jsyn/instruments/SubtractiveSynthVoice.java182
-rw-r--r--src/main/java/com/jsyn/instruments/WaveShapingVoice.java187
-rw-r--r--src/main/java/com/jsyn/io/AudioFifo.java204
-rw-r--r--src/main/java/com/jsyn/io/AudioInputStream.java46
-rw-r--r--src/main/java/com/jsyn/io/AudioOutputStream.java29
-rw-r--r--src/main/java/com/jsyn/midi/MessageParser.java147
-rw-r--r--src/main/java/com/jsyn/midi/MidiConstants.java84
-rw-r--r--src/main/java/com/jsyn/midi/MidiSynthesizer.java121
-rw-r--r--src/main/java/com/jsyn/package.html17
-rw-r--r--src/main/java/com/jsyn/ports/ConnectableInput.java38
-rw-r--r--src/main/java/com/jsyn/ports/ConnectableOutput.java23
-rw-r--r--src/main/java/com/jsyn/ports/GettablePort.java27
-rw-r--r--src/main/java/com/jsyn/ports/InputMixingBlockPart.java112
-rw-r--r--src/main/java/com/jsyn/ports/PortBlockPart.java210
-rw-r--r--src/main/java/com/jsyn/ports/QueueDataCommand.java170
-rw-r--r--src/main/java/com/jsyn/ports/QueueDataEvent.java80
-rw-r--r--src/main/java/com/jsyn/ports/SequentialDataCrossfade.java139
-rw-r--r--src/main/java/com/jsyn/ports/SettablePort.java28
-rw-r--r--src/main/java/com/jsyn/ports/UnitBlockPort.java110
-rw-r--r--src/main/java/com/jsyn/ports/UnitDataQueueCallback.java31
-rw-r--r--src/main/java/com/jsyn/ports/UnitDataQueuePort.java466
-rw-r--r--src/main/java/com/jsyn/ports/UnitFunctionPort.java48
-rw-r--r--src/main/java/com/jsyn/ports/UnitGatePort.java158
-rw-r--r--src/main/java/com/jsyn/ports/UnitInputPort.java254
-rw-r--r--src/main/java/com/jsyn/ports/UnitOutputPort.java103
-rw-r--r--src/main/java/com/jsyn/ports/UnitPort.java85
-rw-r--r--src/main/java/com/jsyn/ports/UnitSpectralInputPort.java83
-rw-r--r--src/main/java/com/jsyn/ports/UnitSpectralOutputPort.java69
-rw-r--r--src/main/java/com/jsyn/ports/UnitVariablePort.java64
-rw-r--r--src/main/java/com/jsyn/ports/package.html13
-rw-r--r--src/main/java/com/jsyn/scope/AudioScope.java101
-rw-r--r--src/main/java/com/jsyn/scope/AudioScopeModel.java157
-rw-r--r--src/main/java/com/jsyn/scope/AudioScopeProbe.java94
-rw-r--r--src/main/java/com/jsyn/scope/DefaultWaveTraceModel.java48
-rw-r--r--src/main/java/com/jsyn/scope/MultiChannelScopeProbeUnit.java246
-rw-r--r--src/main/java/com/jsyn/scope/TriggerModel.java67
-rw-r--r--src/main/java/com/jsyn/scope/WaveTraceModel.java27
-rw-r--r--src/main/java/com/jsyn/scope/swing/AudioScopeProbeView.java45
-rw-r--r--src/main/java/com/jsyn/scope/swing/AudioScopeView.java112
-rw-r--r--src/main/java/com/jsyn/scope/swing/MultipleWaveDisplay.java58
-rw-r--r--src/main/java/com/jsyn/scope/swing/ScopeControlPanel.java46
-rw-r--r--src/main/java/com/jsyn/scope/swing/ScopeProbePanel.java87
-rw-r--r--src/main/java/com/jsyn/scope/swing/ScopeTriggerPanel.java47
-rw-r--r--src/main/java/com/jsyn/scope/swing/WaveTraceView.java122
-rw-r--r--src/main/java/com/jsyn/swing/ASCIIMusicKeyboard.java199
-rw-r--r--src/main/java/com/jsyn/swing/DoubleBoundedRangeModel.java86
-rw-r--r--src/main/java/com/jsyn/swing/DoubleBoundedRangeSlider.java101
-rw-r--r--src/main/java/com/jsyn/swing/DoubleBoundedTextField.java94
-rw-r--r--src/main/java/com/jsyn/swing/EnvelopeEditorBox.java573
-rw-r--r--src/main/java/com/jsyn/swing/EnvelopeEditorPanel.java164
-rw-r--r--src/main/java/com/jsyn/swing/EnvelopePoints.java234
-rw-r--r--src/main/java/com/jsyn/swing/ExponentialRangeModel.java110
-rw-r--r--src/main/java/com/jsyn/swing/InstrumentBrowser.java117
-rw-r--r--src/main/java/com/jsyn/swing/JAppletFrame.java65
-rw-r--r--src/main/java/com/jsyn/swing/PortBoundedRangeModel.java45
-rw-r--r--src/main/java/com/jsyn/swing/PortControllerFactory.java60
-rw-r--r--src/main/java/com/jsyn/swing/PortModelFactory.java64
-rw-r--r--src/main/java/com/jsyn/swing/PresetSelectionListener.java23
-rw-r--r--src/main/java/com/jsyn/swing/RotaryController.java335
-rw-r--r--src/main/java/com/jsyn/swing/RotaryTextController.java53
-rw-r--r--src/main/java/com/jsyn/swing/SoundTweaker.java120
-rw-r--r--src/main/java/com/jsyn/swing/XYController.java132
-rw-r--r--src/main/java/com/jsyn/unitgen/Add.java50
-rw-r--r--src/main/java/com/jsyn/unitgen/AsymptoticRamp.java81
-rw-r--r--src/main/java/com/jsyn/unitgen/BrownNoise.java75
-rw-r--r--src/main/java/com/jsyn/unitgen/ChannelIn.java59
-rw-r--r--src/main/java/com/jsyn/unitgen/ChannelOut.java62
-rw-r--r--src/main/java/com/jsyn/unitgen/Circuit.java122
-rw-r--r--src/main/java/com/jsyn/unitgen/Compare.java38
-rw-r--r--src/main/java/com/jsyn/unitgen/ContinuousRamp.java91
-rw-r--r--src/main/java/com/jsyn/unitgen/CrossFade.java60
-rw-r--r--src/main/java/com/jsyn/unitgen/Delay.java57
-rw-r--r--src/main/java/com/jsyn/unitgen/Divide.java53
-rw-r--r--src/main/java/com/jsyn/unitgen/DualInTwoOut.java59
-rw-r--r--src/main/java/com/jsyn/unitgen/EdgeDetector.java44
-rw-r--r--src/main/java/com/jsyn/unitgen/EnvelopeAttackDecay.java145
-rw-r--r--src/main/java/com/jsyn/unitgen/EnvelopeDAHDSR.java294
-rw-r--r--src/main/java/com/jsyn/unitgen/ExponentialRamp.java104
-rw-r--r--src/main/java/com/jsyn/unitgen/FFT.java36
-rw-r--r--src/main/java/com/jsyn/unitgen/FFTBase.java86
-rw-r--r--src/main/java/com/jsyn/unitgen/FilterAllPass.java62
-rw-r--r--src/main/java/com/jsyn/unitgen/FilterBandPass.java44
-rw-r--r--src/main/java/com/jsyn/unitgen/FilterBandStop.java49
-rw-r--r--src/main/java/com/jsyn/unitgen/FilterBiquad.java156
-rw-r--r--src/main/java/com/jsyn/unitgen/FilterBiquadCommon.java99
-rw-r--r--src/main/java/com/jsyn/unitgen/FilterBiquadShelf.java111
-rw-r--r--src/main/java/com/jsyn/unitgen/FilterFourPoles.java185
-rw-r--r--src/main/java/com/jsyn/unitgen/FilterHighPass.java46
-rw-r--r--src/main/java/com/jsyn/unitgen/FilterHighShelf.java38
-rw-r--r--src/main/java/com/jsyn/unitgen/FilterLowPass.java65
-rw-r--r--src/main/java/com/jsyn/unitgen/FilterLowShelf.java40
-rw-r--r--src/main/java/com/jsyn/unitgen/FilterOnePole.java62
-rw-r--r--src/main/java/com/jsyn/unitgen/FilterOnePoleOneZero.java68
-rw-r--r--src/main/java/com/jsyn/unitgen/FilterOneZero.java65
-rw-r--r--src/main/java/com/jsyn/unitgen/FilterPeakingEQ.java68
-rw-r--r--src/main/java/com/jsyn/unitgen/FilterStateVariable.java120
-rw-r--r--src/main/java/com/jsyn/unitgen/FilterTwoPoles.java66
-rw-r--r--src/main/java/com/jsyn/unitgen/FilterTwoPolesTwoZeros.java79
-rw-r--r--src/main/java/com/jsyn/unitgen/FixedRateMonoReader.java52
-rw-r--r--src/main/java/com/jsyn/unitgen/FixedRateMonoWriter.java54
-rw-r--r--src/main/java/com/jsyn/unitgen/FixedRateStereoReader.java59
-rw-r--r--src/main/java/com/jsyn/unitgen/FixedRateStereoWriter.java60
-rw-r--r--src/main/java/com/jsyn/unitgen/FourWayFade.java94
-rw-r--r--src/main/java/com/jsyn/unitgen/FunctionEvaluator.java76
-rw-r--r--src/main/java/com/jsyn/unitgen/FunctionOscillator.java58
-rw-r--r--src/main/java/com/jsyn/unitgen/Grain.java89
-rw-r--r--src/main/java/com/jsyn/unitgen/GrainCommon.java32
-rw-r--r--src/main/java/com/jsyn/unitgen/GrainEnvelope.java52
-rw-r--r--src/main/java/com/jsyn/unitgen/GrainFarm.java178
-rw-r--r--src/main/java/com/jsyn/unitgen/GrainScheduler.java44
-rw-r--r--src/main/java/com/jsyn/unitgen/GrainSource.java36
-rw-r--r--src/main/java/com/jsyn/unitgen/GrainSourceSine.java51
-rw-r--r--src/main/java/com/jsyn/unitgen/IFFT.java36
-rw-r--r--src/main/java/com/jsyn/unitgen/ImpulseOscillator.java59
-rw-r--r--src/main/java/com/jsyn/unitgen/ImpulseOscillatorBL.java39
-rw-r--r--src/main/java/com/jsyn/unitgen/Integrate.java82
-rw-r--r--src/main/java/com/jsyn/unitgen/InterpolatingDelay.java117
-rw-r--r--src/main/java/com/jsyn/unitgen/Latch.java53
-rw-r--r--src/main/java/com/jsyn/unitgen/LatchZeroCrossing.java72
-rw-r--r--src/main/java/com/jsyn/unitgen/LineIn.java51
-rw-r--r--src/main/java/com/jsyn/unitgen/LineOut.java57
-rw-r--r--src/main/java/com/jsyn/unitgen/LinearRamp.java94
-rw-r--r--src/main/java/com/jsyn/unitgen/Maximum.java42
-rw-r--r--src/main/java/com/jsyn/unitgen/Minimum.java43
-rw-r--r--src/main/java/com/jsyn/unitgen/MixerMono.java77
-rw-r--r--src/main/java/com/jsyn/unitgen/MixerMonoRamped.java54
-rw-r--r--src/main/java/com/jsyn/unitgen/MixerStereo.java94
-rw-r--r--src/main/java/com/jsyn/unitgen/MixerStereoRamped.java71
-rw-r--r--src/main/java/com/jsyn/unitgen/MonoStreamWriter.java49
-rw-r--r--src/main/java/com/jsyn/unitgen/MorphingOscillatorBL.java72
-rw-r--r--src/main/java/com/jsyn/unitgen/MultiPassThrough.java70
-rw-r--r--src/main/java/com/jsyn/unitgen/Multiply.java64
-rw-r--r--src/main/java/com/jsyn/unitgen/MultiplyAdd.java57
-rw-r--r--src/main/java/com/jsyn/unitgen/Pan.java64
-rw-r--r--src/main/java/com/jsyn/unitgen/PanControl.java61
-rw-r--r--src/main/java/com/jsyn/unitgen/ParabolicEnvelope.java110
-rw-r--r--src/main/java/com/jsyn/unitgen/PassThrough.java38
-rw-r--r--src/main/java/com/jsyn/unitgen/PeakFollower.java87
-rw-r--r--src/main/java/com/jsyn/unitgen/PhaseShifter.java90
-rw-r--r--src/main/java/com/jsyn/unitgen/PinkNoise.java128
-rw-r--r--src/main/java/com/jsyn/unitgen/PitchDetector.java120
-rw-r--r--src/main/java/com/jsyn/unitgen/PitchToFrequency.java26
-rw-r--r--src/main/java/com/jsyn/unitgen/PowerOfTwo.java108
-rw-r--r--src/main/java/com/jsyn/unitgen/PulseOscillator.java59
-rw-r--r--src/main/java/com/jsyn/unitgen/PulseOscillatorBL.java61
-rw-r--r--src/main/java/com/jsyn/unitgen/RaisedCosineEnvelope.java73
-rw-r--r--src/main/java/com/jsyn/unitgen/RangeConverter.java50
-rw-r--r--src/main/java/com/jsyn/unitgen/RectangularWindow.java39
-rw-r--r--src/main/java/com/jsyn/unitgen/RedNoise.java80
-rw-r--r--src/main/java/com/jsyn/unitgen/SampleGrainFarm.java71
-rw-r--r--src/main/java/com/jsyn/unitgen/SampleGrainSource.java69
-rw-r--r--src/main/java/com/jsyn/unitgen/SawtoothOscillator.java47
-rw-r--r--src/main/java/com/jsyn/unitgen/SawtoothOscillatorBL.java65
-rw-r--r--src/main/java/com/jsyn/unitgen/SawtoothOscillatorDPW.java76
-rw-r--r--src/main/java/com/jsyn/unitgen/SchmidtTrigger.java83
-rw-r--r--src/main/java/com/jsyn/unitgen/Select.java56
-rw-r--r--src/main/java/com/jsyn/unitgen/SequentialDataReader.java38
-rw-r--r--src/main/java/com/jsyn/unitgen/SequentialDataWriter.java44
-rw-r--r--src/main/java/com/jsyn/unitgen/SineOscillator.java84
-rw-r--r--src/main/java/com/jsyn/unitgen/SineOscillatorPhaseModulated.java74
-rw-r--r--src/main/java/com/jsyn/unitgen/SpectralFFT.java130
-rw-r--r--src/main/java/com/jsyn/unitgen/SpectralFilter.java130
-rw-r--r--src/main/java/com/jsyn/unitgen/SpectralIFFT.java92
-rw-r--r--src/main/java/com/jsyn/unitgen/SpectralProcessor.java73
-rw-r--r--src/main/java/com/jsyn/unitgen/SquareOscillator.java49
-rw-r--r--src/main/java/com/jsyn/unitgen/SquareOscillatorBL.java48
-rw-r--r--src/main/java/com/jsyn/unitgen/StereoStreamWriter.java53
-rw-r--r--src/main/java/com/jsyn/unitgen/StochasticGrainScheduler.java43
-rw-r--r--src/main/java/com/jsyn/unitgen/Subtract.java42
-rw-r--r--src/main/java/com/jsyn/unitgen/TriangleOscillator.java59
-rw-r--r--src/main/java/com/jsyn/unitgen/TunableFilter.java41
-rw-r--r--src/main/java/com/jsyn/unitgen/TwoInDualOut.java56
-rw-r--r--src/main/java/com/jsyn/unitgen/UnitBinaryOperator.java41
-rw-r--r--src/main/java/com/jsyn/unitgen/UnitFilter.java47
-rw-r--r--src/main/java/com/jsyn/unitgen/UnitGate.java54
-rw-r--r--src/main/java/com/jsyn/unitgen/UnitGenerator.java357
-rw-r--r--src/main/java/com/jsyn/unitgen/UnitOscillator.java93
-rw-r--r--src/main/java/com/jsyn/unitgen/UnitSink.java43
-rw-r--r--src/main/java/com/jsyn/unitgen/UnitSource.java30
-rw-r--r--src/main/java/com/jsyn/unitgen/UnitStreamWriter.java53
-rw-r--r--src/main/java/com/jsyn/unitgen/UnitVoice.java59
-rw-r--r--src/main/java/com/jsyn/unitgen/Unzipper.java47
-rw-r--r--src/main/java/com/jsyn/unitgen/VariableRateDataReader.java29
-rw-r--r--src/main/java/com/jsyn/unitgen/VariableRateMonoReader.java115
-rw-r--r--src/main/java/com/jsyn/unitgen/VariableRateStereoReader.java113
-rw-r--r--src/main/java/com/jsyn/unitgen/WhiteNoise.java56
-rw-r--r--src/main/java/com/jsyn/unitgen/ZeroCrossingCounter.java61
-rw-r--r--src/main/java/com/jsyn/util/AudioSampleLoader.java42
-rw-r--r--src/main/java/com/jsyn/util/AudioStreamReader.java85
-rw-r--r--src/main/java/com/jsyn/util/AutoCorrelator.java290
-rw-r--r--src/main/java/com/jsyn/util/Instrument.java38
-rw-r--r--src/main/java/com/jsyn/util/InstrumentLibrary.java32
-rw-r--r--src/main/java/com/jsyn/util/JavaSoundSampleLoader.java149
-rw-r--r--src/main/java/com/jsyn/util/JavaTools.java64
-rw-r--r--src/main/java/com/jsyn/util/MultiChannelSynthesizer.java404
-rw-r--r--src/main/java/com/jsyn/util/NumericOutput.java193
-rw-r--r--src/main/java/com/jsyn/util/PolyphonicInstrument.java155
-rw-r--r--src/main/java/com/jsyn/util/PseudoRandom.java89
-rw-r--r--src/main/java/com/jsyn/util/RecursiveSequenceGenerator.java214
-rw-r--r--src/main/java/com/jsyn/util/SampleLoader.java230
-rw-r--r--src/main/java/com/jsyn/util/SignalCorrelator.java48
-rw-r--r--src/main/java/com/jsyn/util/StreamingThread.java121
-rw-r--r--src/main/java/com/jsyn/util/TransportListener.java31
-rw-r--r--src/main/java/com/jsyn/util/TransportModel.java67
-rw-r--r--src/main/java/com/jsyn/util/VoiceAllocator.java258
-rw-r--r--src/main/java/com/jsyn/util/VoiceDescription.java68
-rw-r--r--src/main/java/com/jsyn/util/VoiceOperation.java7
-rw-r--r--src/main/java/com/jsyn/util/WaveFileWriter.java293
-rw-r--r--src/main/java/com/jsyn/util/WaveRecorder.java134
-rw-r--r--src/main/java/com/jsyn/util/soundfile/AIFFFileParser.java232
-rw-r--r--src/main/java/com/jsyn/util/soundfile/AudioFileParser.java129
-rw-r--r--src/main/java/com/jsyn/util/soundfile/ChunkHandler.java49
-rw-r--r--src/main/java/com/jsyn/util/soundfile/CustomSampleLoader.java60
-rw-r--r--src/main/java/com/jsyn/util/soundfile/IFFParser.java313
-rw-r--r--src/main/java/com/jsyn/util/soundfile/WAVEFileParser.java338
-rw-r--r--src/main/java/com/softsynth/math/AudioMath.java82
-rw-r--r--src/main/java/com/softsynth/math/ChebyshevPolynomial.java45
-rw-r--r--src/main/java/com/softsynth/math/FourierMath.java254
-rw-r--r--src/main/java/com/softsynth/math/JustRatio.java47
-rw-r--r--src/main/java/com/softsynth/math/Polynomial.java259
-rw-r--r--src/main/java/com/softsynth/math/PolynomialTableData.java64
-rw-r--r--src/main/java/com/softsynth/math/PrimeFactors.java244
-rw-r--r--src/main/java/com/softsynth/shared/time/ScheduledCommand.java21
-rw-r--r--src/main/java/com/softsynth/shared/time/ScheduledQueue.java85
-rw-r--r--src/main/java/com/softsynth/shared/time/TimeStamp.java56
258 files changed, 26076 insertions, 0 deletions
diff --git a/src/main/java/com/jsyn/JSyn.java b/src/main/java/com/jsyn/JSyn.java
new file mode 100644
index 0000000..bbc2891
--- /dev/null
+++ b/src/main/java/com/jsyn/JSyn.java
@@ -0,0 +1,78 @@
+/*
+ * 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:
+ *
+ * <pre><code>
+ // 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();
+</code> </pre>
+ *
+ * @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 = 8;
+ private final static int VERSION_REVISION = 1;
+ public final static int BUILD_NUMBER = 464;
+ private final static long BUILD_TIME = new GregorianCalendar(2017,
+ GregorianCalendar.OCTOBER, 16).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/main/java/com/jsyn/Synthesizer.java b/src/main/java/com/jsyn/Synthesizer.java
new file mode 100644
index 0000000..bfabb4c
--- /dev/null
+++ b/src/main/java/com/jsyn/Synthesizer.java
@@ -0,0 +1,202 @@
+/*
+ * 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.
+ * <p>
+ * Note that using more than 2 channels will probably require the use of JPortAudio because
+ * JavaSound currently does not support more than two channels.
+ * JPortAudio is available at
+ * <a href="http://www.softsynth.com/jsyn/developers/download.php">http://www.softsynth.com/jsyn/developers/download.php</a>.
+ * <p>
+ * If you use more than 2 inputs or outputs then you will probably want to use {@link com.jsyn.unitgen.ChannelIn}
+ * or {@link com.jsyn.unitgen.ChannelOut}, which can be associated with any indexed channel.
+ *
+ * @param frameRate in Hertz
+ * @param inputDeviceID obtained from an {@link AudioDeviceManager} or pass
+ * AudioDeviceManager.USE_DEFAULT_DEVICE
+ * @param numInputChannels 0 for no input, 1 for mono, 2 for stereo, etcetera
+ * @param ouputDeviceID obtained from an AudioDeviceManager or pass
+ * AudioDeviceManager.USE_DEFAULT_DEVICE
+ * @param numOutputChannels 0 for no output, 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 or ChannelOut.
+ */
+ public void startUnit(UnitGenerator unit, double time);
+
+ public void startUnit(UnitGenerator unit, TimeStamp timeStamp);
+
+ /**
+ * The startUnit and stopUnit methods are mainly for internal use.
+ * Please call unit.start() or unit.stop() instead.
+ * @param unit
+ */
+ public void startUnit(UnitGenerator unit);
+
+ public void stopUnit(UnitGenerator unit, double time);
+
+ public void stopUnit(UnitGenerator unit, TimeStamp timeStamp);
+
+ /**
+ * The startUnit and stopUnit methods are mainly for internal use.
+ * Please call unit.start() or unit.stop() instead.
+ * @param unit
+ */
+ 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);
+
+ /**
+ * Clear all scheduled commands from the queue.
+ * Commands will be discarded.
+ */
+ public void clearCommandQueue();
+
+ /**
+ * @return true if the Synthesizer has been started
+ */
+ public boolean isRunning();
+
+ /**
+ * Add a task that will be run repeatedly on the Audio Thread before it generates every 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 task
+ */
+ public void addAudioTask(Runnable task);
+
+ public void removeAudioTask(Runnable task);
+
+}
diff --git a/src/main/java/com/jsyn/apps/AboutJSyn.java b/src/main/java/com/jsyn/apps/AboutJSyn.java
new file mode 100644
index 0000000..0624591
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/apps/InstrumentTester.java b/src/main/java/com/jsyn/apps/InstrumentTester.java
new file mode 100644
index 0000000..2505759
--- /dev/null
+++ b/src/main/java/com/jsyn/apps/InstrumentTester.java
@@ -0,0 +1,210 @@
+/*
+ * 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 java.io.IOException;
+
+import javax.sound.midi.MidiDevice;
+import javax.sound.midi.MidiMessage;
+import javax.sound.midi.MidiUnavailableException;
+import javax.sound.midi.Receiver;
+import javax.swing.JApplet;
+
+import com.jsyn.JSyn;
+import com.jsyn.Synthesizer;
+import com.jsyn.devices.javasound.MidiDeviceTools;
+import com.jsyn.instruments.JSynInstrumentLibrary;
+import com.jsyn.midi.MessageParser;
+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;
+import com.softsynth.math.AudioMath;
+import com.softsynth.shared.time.TimeStamp;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Let the user select an instrument using the InstrumentBrowser and play
+ * them using the ASCII keyboard or with MIDI.
+ * Sound parameters can be tweaked using faders.
+ *
+ * @author Phil Burk (C) 2012 Mobileer Inc
+ */
+public class InstrumentTester extends JApplet {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(InstrumentTester.class);
+ private static final long serialVersionUID = -2704222221111608377L;
+
+ private Synthesizer synth;
+ private LineOut lineOut;
+ private SoundTweaker tweaker;
+ protected PolyphonicInstrument instrument;
+ private MyParser messageParser;
+
+ class MyParser extends MessageParser {
+
+ @Override
+ public void controlChange(int channel, int index, int value) {
+ }
+
+ @Override
+ public void noteOff(int channel, int noteNumber, int velocity) {
+ instrument.noteOff(noteNumber, synth.createTimeStamp());
+ }
+
+ @Override
+ public void noteOn(int channel, int noteNumber, int velocity) {
+ double frequency = AudioMath.pitchToFrequency(noteNumber);
+ double amplitude = velocity / (4 * 128.0);
+ TimeStamp timeStamp = synth.createTimeStamp();
+ instrument.noteOn(noteNumber, frequency, amplitude, timeStamp);
+ }
+
+ }
+
+ // 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 setupMidiKeyboard() throws MidiUnavailableException, IOException, InterruptedException {
+ 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);
+ LOGGER.debug("Play MIDI keyboard: " + keyboard.getDeviceInfo().getDescription());
+ result = 0;
+ } else {
+ LOGGER.debug("Could not find a keyboard.");
+ }
+ return result;
+ }
+
+ @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();
+ }
+ 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);
+
+ try {
+ setupMidiKeyboard();
+ } catch (MidiUnavailableException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (IOException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (InterruptedException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+
+ 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/main/java/com/jsyn/data/AudioSample.java b/src/main/java/com/jsyn/data/AudioSample.java
new file mode 100644
index 0000000..dcbbae5
--- /dev/null
+++ b/src/main/java/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 = 1;
+ private double frameRate = 44100.0;
+ 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/main/java/com/jsyn/data/DoubleTable.java b/src/main/java/com/jsyn/data/DoubleTable.java
new file mode 100644
index 0000000..ca64c94
--- /dev/null
+++ b/src/main/java/com/jsyn/data/DoubleTable.java
@@ -0,0 +1,109 @@
+/*
+ * 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 int length() {
+ return table.length;
+ }
+
+ 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/main/java/com/jsyn/data/FloatSample.java b/src/main/java/com/jsyn/data/FloatSample.java
new file mode 100644
index 0000000..2d8c973
--- /dev/null
+++ b/src/main/java/com/jsyn/data/FloatSample.java
@@ -0,0 +1,164 @@
+/*
+ * 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 implements Function {
+ 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 a 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;
+ }
+
+ /**
+ * Interpolate between two adjacent samples.
+ * Note that this will only work for mono, single channel samples.
+ *
+ * @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;
+ }
+
+ /**
+ * Note that this will only work for mono, single channel samples.
+ */
+ @Override
+ public double evaluate(double input) {
+ // Input ranges from -1 to +1
+ // Map it to range of sample with guard point.
+ double normalizedInput = (input + 1.0) * 0.5;
+ // Clip so it does not go out of range of the sample.
+ if (normalizedInput < 0.0) normalizedInput = 0.0;
+ else if (normalizedInput > 1.0) normalizedInput = 1.0;
+ double fractionalIndex = (getNumFrames() - 1.01) * normalizedInput;
+ return interpolate(fractionalIndex);
+ }
+}
diff --git a/src/main/java/com/jsyn/data/Function.java b/src/main/java/com/jsyn/data/Function.java
new file mode 100644
index 0000000..c0e6566
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/data/HammingWindow.java b/src/main/java/com/jsyn/data/HammingWindow.java
new file mode 100644
index 0000000..d8e1238
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/data/HannWindow.java b/src/main/java/com/jsyn/data/HannWindow.java
new file mode 100644
index 0000000..878d07c
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/data/SampleMarker.java b/src/main/java/com/jsyn/data/SampleMarker.java
new file mode 100644
index 0000000..d3db1d4
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/data/SegmentedEnvelope.java b/src/main/java/com/jsyn/data/SegmentedEnvelope.java
new file mode 100644
index 0000000..efdfd89
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/data/SequentialData.java b/src/main/java/com/jsyn/data/SequentialData.java
new file mode 100644
index 0000000..f567493
--- /dev/null
+++ b/src/main/java/com/jsyn/data/SequentialData.java
@@ -0,0 +1,96 @@
+/*
+ * 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/main/java/com/jsyn/data/SequentialDataCommon.java b/src/main/java/com/jsyn/data/SequentialDataCommon.java
new file mode 100644
index 0000000..5cc51df
--- /dev/null
+++ b/src/main/java/com/jsyn/data/SequentialDataCommon.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.
+ */
+
+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;
+ }
+
+ /**
+ * Set beginning of a sustain loop. When UnitDataQueuePort.queueOn() is called,
+ * if the loop is set then the attack portion will be queued followed by this sustain
+ * region using queueLoop().
+ * The number of frames in the loop will be (SustainEnd - SustainBegin).
+ * <p>
+ * For a steady sustain level, like in an ADSR envelope, set SustainBegin and
+ * SustainEnd to the same frame.
+ * <p>
+ * For a sustain that is modulated, include two or more frames in the loop.
+ *
+ * @param sustainBegin
+ */
+ 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;
+ }
+
+ /**
+ * The release loop behaves like the sustain loop but it is triggered
+ * by UnitDataQueuePort.queueOff().
+ *
+ * @param releaseBegin
+ */
+ 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/main/java/com/jsyn/data/ShortSample.java b/src/main/java/com/jsyn/data/ShortSample.java
new file mode 100644
index 0000000..4a4110e
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/data/SpectralWindow.java b/src/main/java/com/jsyn/data/SpectralWindow.java
new file mode 100644
index 0000000..0fcfac4
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/data/SpectralWindowFactory.java b/src/main/java/com/jsyn/data/SpectralWindowFactory.java
new file mode 100644
index 0000000..01cced6
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/data/Spectrum.java b/src/main/java/com/jsyn/data/Spectrum.java
new file mode 100644
index 0000000..66e4ee4
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/devices/AudioDeviceFactory.java b/src/main/java/com/jsyn/devices/AudioDeviceFactory.java
new file mode 100644
index 0000000..612c81d
--- /dev/null
+++ b/src/main/java/com/jsyn/devices/AudioDeviceFactory.java
@@ -0,0 +1,93 @@
+/*
+ * 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 {
+ @SuppressWarnings("unchecked")
+ 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/main/java/com/jsyn/devices/AudioDeviceInputStream.java b/src/main/java/com/jsyn/devices/AudioDeviceInputStream.java
new file mode 100644
index 0000000..a3d1854
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/devices/AudioDeviceManager.java b/src/main/java/com/jsyn/devices/AudioDeviceManager.java
new file mode 100644
index 0000000..ac8d47c
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/devices/AudioDeviceOutputStream.java b/src/main/java/com/jsyn/devices/AudioDeviceOutputStream.java
new file mode 100644
index 0000000..5c17efb
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/devices/javasound/JavaSoundAudioDevice.java b/src/main/java/com/jsyn/devices/javasound/JavaSoundAudioDevice.java
new file mode 100644
index 0000000..75c4a8a
--- /dev/null
+++ b/src/main/java/com/jsyn/devices/javasound/JavaSoundAudioDevice.java
@@ -0,0 +1,432 @@
+/*
+ * 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 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;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Use JavaSound to access the audio hardware.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ */
+public class JavaSoundAudioDevice implements AudioDeviceManager {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(JavaSoundAudioDevice.class);
+
+ 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;
+
+ 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.debug("" + 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;
+ }
+
+ static class DeviceInfo {
+ String name;
+ int maxInputs;
+ int maxOutputs;
+
+ @Override
+ public String toString() {
+ return "AudioDevice: " + name + ", max in = " + maxInputs + ", max out = " + maxOutputs;
+ }
+ }
+
+ private static 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);
+ return numFrames * samplesPerFrame * BYTES_PER_SAMPLE;
+ }
+
+ }
+
+ 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.error("JavaSoundOutputStream - not supported." + format);
+ } else {
+ try {
+ line = (SourceDataLine) getDataLine(info);
+ int bufferSize = calculateBufferSize(suggestedOutputLatency);
+ line.open(format, bufferSize);
+ LOGGER.debug("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
+ }
+
+ 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.error("JavaSoundInputStream - not supported." + format);
+ } else {
+ try {
+ line = (TargetDataLine) getDataLine(info);
+ int bufferSize = calculateBufferSize(suggestedInputLatency);
+ line.open(format, bufferSize);
+ LOGGER.debug("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/main/java/com/jsyn/devices/javasound/MidiDeviceTools.java b/src/main/java/com/jsyn/devices/javasound/MidiDeviceTools.java
new file mode 100644
index 0000000..9cff095
--- /dev/null
+++ b/src/main/java/com/jsyn/devices/javasound/MidiDeviceTools.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.devices.javasound;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+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 {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(MidiDeviceTools.class);
+
+ /** 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) {
+ LOGGER.debug("MIDI Info: " + info.getDescription() + ", " + info.getName() + ", "
+ + info.getVendor() + ", " + info.getVersion());
+ // Get the device for more information.
+ try {
+ MidiDevice device = MidiSystem.getMidiDevice(info);
+ LOGGER.debug(" 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;
+ LOGGER.debug("Chose: " + info.getDescription());
+ break;
+ }
+ }
+ }
+ } catch (MidiUnavailableException e) {
+ e.printStackTrace();
+ }
+ }
+ return keyboard;
+ }
+
+ public static MidiDevice findKeyboard() {
+ return findKeyboard(null);
+ }
+
+}
diff --git a/src/main/java/com/jsyn/devices/jportaudio/JPortAudioDevice.java b/src/main/java/com/jsyn/devices/jportaudio/JPortAudioDevice.java
new file mode 100644
index 0000000..15ab9ed
--- /dev/null
+++ b/src/main/java/com/jsyn/devices/jportaudio/JPortAudioDevice.java
@@ -0,0 +1,264 @@
+/*
+ * 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;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class JPortAudioDevice implements AudioDeviceManager {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(JPortAudioDevice.class);
+
+ 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 static 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;
+ LOGGER.debug("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;
+ LOGGER.debug("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/main/java/com/jsyn/engine/LoadAnalyzer.java b/src/main/java/com/jsyn/engine/LoadAnalyzer.java
new file mode 100644
index 0000000..cbf7ed5
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/engine/MultiTable.java b/src/main/java/com/jsyn/engine/MultiTable.java
new file mode 100644
index 0000000..6606639
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/engine/SynthesisEngine.java b/src/main/java/com/jsyn/engine/SynthesisEngine.java
new file mode 100644
index 0000000..30872a8
--- /dev/null
+++ b/src/main/java/com/jsyn/engine/SynthesisEngine.java
@@ -0,0 +1,700 @@
+/*
+ * 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 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;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+//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 Synthesizer {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(SynthesisEngine.class);
+
+ private final static int BLOCKS_PER_BUFFER = 8;
+ private final static int FRAMES_PER_BUFFER = Synthesizer.FRAMES_PER_BLOCK * BLOCKS_PER_BUFFER;
+ // I have measured JavaSound taking 1200 msec to close devices.
+ private static final int MAX_THREAD_STOP_TIME = 2000;
+
+ public static final int DEFAULT_FRAME_RATE = 44100;
+
+ private final AudioDeviceManager audioDeviceManager;
+ private EngineThread engineThread;
+ private final ScheduledQueue<ScheduledCommand> commandQueue = new ScheduledQueue<ScheduledCommand>();
+
+ 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 final ArrayList<UnitGenerator> allUnitList = new ArrayList<UnitGenerator>();
+ // List of running units.
+ private final ArrayList<UnitGenerator> runningUnitList = new ArrayList<UnitGenerator>();
+ // List of units stopping because of autoStop.
+ private final ArrayList<UnitGenerator> stoppingUnitList = new ArrayList<UnitGenerator>();
+
+ private LoadAnalyzer loadAnalyzer;
+ // private int numOutputChannels;
+ // private int numInputChannels;
+ private final CopyOnWriteArrayList<Runnable> audioTasks = new CopyOnWriteArrayList<Runnable>();
+ private double mOutputLatency;
+ private double mInputLatency;
+ /** 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));
+
+ 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 final 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();
+ }
+ }
+ }
+
+ static class ChannelBlockBuffer {
+ private final 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;
+
+ 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) {
+ engineThread = new EngineThread(inputDeviceID, numInputChannels,
+ outputDeviceID, numOutputChannels);
+ LOGGER.debug("Synth thread old priority = " + engineThread.getPriority());
+ int engineThreadPriority = engineThread.getPriority() + 2 > Thread.MAX_PRIORITY ?
+ Thread.MAX_PRIORITY : engineThread.getPriority() + 2;
+ engineThread.setPriority(engineThreadPriority);
+ LOGGER.debug("Synth thread new priority = " + engineThread.getPriority());
+ engineThread.start();
+ }
+
+ started = true;
+ }
+
+ @Override
+ public boolean isRunning() {
+ Thread thread = engineThread;
+ return (thread != null) && thread.isAlive();
+ }
+
+ @Override
+ public synchronized void stop() {
+ if (!started) {
+ LOGGER.info("JSyn already stopped.");
+ return;
+ }
+
+ if (useRealTime) {
+ // Stop audio synthesis and all units.
+ if (engineThread != null) {
+ try {
+ // Interrupt now, otherwise audio thread will wait for audio I/O.
+ engineThread.requestStop();
+ engineThread.join(MAX_THREAD_STOP_TIME);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ synchronized (runningUnitList) {
+ runningUnitList.clear();
+ }
+ started = false;
+ }
+
+ private class EngineThread extends Thread
+ {
+ private AudioDeviceOutputStream audioOutputStream;
+ private AudioDeviceInputStream audioInputStream;
+ private volatile boolean go = true;
+
+ EngineThread(int inputDeviceID, int numInputChannels,
+ int outputDeviceID, int numOutputChannels) {
+ if (numInputChannels > 0) {
+ audioInputStream = audioDeviceManager.createInputStream(inputDeviceID, frameRate,
+ numInputChannels);
+ }
+ if (numOutputChannels > 0) {
+ audioOutputStream = audioDeviceManager.createOutputStream(outputDeviceID,
+ frameRate, numOutputChannels);
+ }
+ }
+
+ public void requestStop() {
+ go = false;
+ interrupt();
+ }
+
+ @Override
+ public void run() {
+ LOGGER.debug("JSyn synthesis thread starting.");
+ try {
+ if (audioInputStream != null) {
+ LOGGER.debug("JSyn synthesis thread trying to start audio INPUT!");
+ audioInputStream.start();
+ mInputLatency = audioInputStream.getLatency();
+ String msg = String.format("Input Latency in = %5.1f msec",
+ 1000 * mInputLatency);
+ LOGGER.debug(msg);
+ }
+ if (audioOutputStream != null) {
+ LOGGER.debug("JSyn synthesis thread trying to start audio OUTPUT!");
+ audioOutputStream.start();
+ mOutputLatency = audioOutputStream.getLatency();
+ String msg = String.format("Output Latency = %5.1f msec",
+ 1000 * mOutputLatency);
+ LOGGER.debug(msg);
+ // Buy some time while we fill the buffer.
+ audioOutputStream.write(outputBuffer.interleavedBuffer);
+ }
+ loadAnalyzer = new LoadAnalyzer();
+ while (go) {
+ boolean throttled = false;
+ if (audioInputStream != null) {
+ // This call will block when the input is empty.
+ audioInputStream.read(inputBuffer.interleavedBuffer);
+ throttled = true;
+ }
+
+ loadAnalyzer.start();
+ runAudioTasks();
+ generateNextBuffer();
+ loadAnalyzer.stop();
+
+ if (audioOutputStream != null) {
+ // This call will block when the output is full.
+ audioOutputStream.write(outputBuffer.interleavedBuffer);
+ throttled = true;
+ }
+ if (!throttled && isRealTime()) {
+ Thread.sleep(2); // avoid spinning and eating up CPU
+ }
+ }
+
+ } 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.debug("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.debug("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() == engineThread) && (timeStamp.getTime() <= getCurrentTime())) {
+ command.run();
+ } else {
+ LOGGER.debug("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);
+ }
+
+ @Override
+ public void clearCommandQueue() {
+ commandQueue.clear();
+ }
+
+ 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() {
+ return mOutputLatency;
+ }
+
+ public double getInputLatency() {
+ return mInputLatency;
+ }
+
+ @Override
+ public void add(UnitGenerator ugen) {
+ ugen.setSynthesisEngine(this);
+ allUnitList.add(ugen);
+ }
+
+ @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/main/java/com/jsyn/exceptions/ChannelMismatchException.java b/src/main/java/com/jsyn/exceptions/ChannelMismatchException.java
new file mode 100644
index 0000000..a1554cd
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/instruments/DrumWoodFM.java b/src/main/java/com/jsyn/instruments/DrumWoodFM.java
new file mode 100644
index 0000000..ba6cd1b
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/instruments/DualOscillatorSynthVoice.java b/src/main/java/com/jsyn/instruments/DualOscillatorSynthVoice.java
new file mode 100644
index 0000000..c81041f
--- /dev/null
+++ b/src/main/java/com/jsyn/instruments/DualOscillatorSynthVoice.java
@@ -0,0 +1,301 @@
+/*
+ * 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.FilterFourPoles;
+import com.jsyn.unitgen.MorphingOscillatorBL;
+import com.jsyn.unitgen.Multiply;
+import com.jsyn.unitgen.UnitVoice;
+import com.jsyn.util.VoiceDescription;
+import com.softsynth.math.AudioMath;
+import com.softsynth.shared.time.TimeStamp;
+
+/**
+ * Synthesizer voice with two morphing oscillators and a four-pole resonant filter.
+ * Modulate the amplitude and filter using DAHDSR envelopes.
+ */
+public class DualOscillatorSynthVoice extends Circuit implements UnitVoice {
+ private Multiply frequencyMultiplier;
+ private Multiply amplitudeMultiplier;
+ private Multiply detuneScaler1;
+ private Multiply detuneScaler2;
+ private Multiply amplitudeBoost;
+ private MorphingOscillatorBL osc1;
+ private MorphingOscillatorBL osc2;
+ private FilterFourPoles filter;
+ private EnvelopeDAHDSR ampEnv;
+ private EnvelopeDAHDSR filterEnv;
+ private Add cutoffAdder;
+
+ private static MyVoiceDescription voiceDescription;
+
+ 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. Set to 1.0 for no modulation.
+ */
+ public UnitInputPort frequencyScaler;
+ public UnitInputPort oscShape1;
+ public UnitInputPort oscShape2;
+// public UnitInputPort oscDetune1;
+// public UnitInputPort oscDetune2;
+ public UnitInputPort cutoff;
+ public UnitInputPort filterEnvDepth;
+ public UnitInputPort Q;
+
+ public DualOscillatorSynthVoice() {
+ add(frequencyMultiplier = new Multiply());
+ add(amplitudeMultiplier = new Multiply());
+ add(amplitudeBoost = new Multiply());
+ add(detuneScaler1 = new Multiply());
+ add(detuneScaler2 = new Multiply());
+ // Add tone generators.
+ add(osc1 = new MorphingOscillatorBL());
+ add(osc2 = new MorphingOscillatorBL());
+
+ // 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 FilterFourPoles());
+ add(cutoffAdder = new Add());
+
+ filterEnv.output.connect(cutoffAdder.inputA);
+ cutoffAdder.output.connect(filter.frequency);
+ frequencyMultiplier.output.connect(detuneScaler1.inputA);
+ frequencyMultiplier.output.connect(detuneScaler2.inputA);
+ detuneScaler1.output.connect(osc1.frequency);
+ detuneScaler2.output.connect(osc2.frequency);
+ osc1.output.connect(amplitudeMultiplier.inputA); // mix oscillators
+ osc2.output.connect(amplitudeMultiplier.inputA);
+ amplitudeMultiplier.output.connect(filter.input);
+ filter.output.connect(amplitudeBoost.inputA);
+ amplitudeBoost.output.connect(ampEnv.amplitude);
+
+ addPort(amplitude = amplitudeMultiplier.inputB, PORT_NAME_AMPLITUDE);
+ addPort(frequency = frequencyMultiplier.inputA, PORT_NAME_FREQUENCY);
+ addPort(oscShape1 = osc1.shape, "OscShape1");
+ addPort(oscShape2 = osc2.shape, "OscShape2");
+// addPort(oscDetune1 = osc1.shape, "OscDetune1");
+// addPort(oscDetune2 = osc2.shape, "OscDetune2");
+ addPort(cutoff = cutoffAdder.inputB, PORT_NAME_CUTOFF);
+ addPortAlias(cutoff, PORT_NAME_TIMBRE);
+ addPort(Q = filter.Q);
+ addPort(frequencyScaler = frequencyMultiplier.inputB, PORT_NAME_FREQUENCY_SCALER);
+ addPort(filterEnvDepth = filterEnv.amplitude, "FilterEnvDepth");
+
+ filterEnv.export(this, "Filter");
+ ampEnv.export(this, "Amp");
+
+ frequency.setup(osc1.frequency);
+ frequencyScaler.setup(0.2, 1.0, 4.0);
+ cutoff.setup(filter.frequency);
+ // Allow negative filter sweeps
+ filterEnvDepth.setup(-4000.0, 2000.0, 4000.0);
+
+ // set amplitudes slightly different so that they never entirely cancel
+ osc1.amplitude.set(0.5);
+ osc2.amplitude.set(0.4);
+ // Make the circuit turn off when the envelope finishes to reduce CPU load.
+ ampEnv.setupAutoDisable(this);
+ // Add named port for mapping pressure.
+ amplitudeBoost.inputB.setup(1.0, 1.0, 4.0);
+ addPortAlias(amplitudeBoost.inputB, PORT_NAME_PRESSURE);
+
+ usePreset(0);
+ }
+
+ /**
+ * The first oscillator will be tuned UP by semitoneOffset/2.
+ * The second oscillator will be tuned DOWN by semitoneOffset/2.
+ * @param semitoneOffset
+ */
+ private void setDetunePitch(double semitoneOffset) {
+ double halfOffset = semitoneOffset * 0.5;
+ setDetunePitch1(halfOffset);
+ setDetunePitch2(-halfOffset);
+ }
+
+ /**
+ * Set the detuning for osc1 in semitones.
+ * @param semitoneOffset
+ */
+ private void setDetunePitch1(double semitoneOffset) {
+ double scale = AudioMath.semitonesToFrequencyScaler(semitoneOffset);
+ detuneScaler1.inputB.set(scale);
+ }
+
+ /**
+ * Set the detuning for osc2 in semitones.
+ * @param semitoneOffset
+ */
+ private void setDetunePitch2(double semitoneOffset) {
+ double scale = AudioMath.semitonesToFrequencyScaler(semitoneOffset);
+ detuneScaler2.inputB.set(scale);
+ }
+
+ @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;
+ }
+
+ // Reset to basic voice.
+ public void reset() {
+ osc1.shape.set(0.0);
+ osc2.shape.set(0.0);
+ ampEnv.attack.set(0.005);
+ ampEnv.decay.set(0.2);
+ ampEnv.sustain.set(0.5);
+ ampEnv.release.set(1.0);
+ filterEnv.attack.set(0.01);
+ filterEnv.decay.set(0.6);
+ filterEnv.sustain.set(0.4);
+ filterEnv.release.set(1.0);
+ cutoff.set(500.0);
+ filterEnvDepth.set(3000.0);
+ filter.reset();
+ filter.Q.set(3.9);
+ setDetunePitch(0.02);
+ }
+
+ @Override
+ public void usePreset(int presetIndex) {
+ reset(); // start from known configuration
+ int n = presetIndex % presetNames.length;
+ switch (n) {
+ case 0:
+ break;
+ case 1:
+ ampEnv.attack.set(0.1);
+ ampEnv.decay.set(0.9);
+ ampEnv.sustain.set(0.1);
+ ampEnv.release.set(0.1);
+ cutoff.set(500.0);
+ filterEnvDepth.set(500.0);
+ filter.Q.set(3.0);
+ break;
+ case 2:
+ ampEnv.attack.set(0.1);
+ ampEnv.decay.set(0.3);
+ ampEnv.release.set(0.5);
+ cutoff.set(2000.0);
+ filterEnvDepth.set(500.0);
+ filter.Q.set(2.0);
+ break;
+ case 3:
+ osc1.shape.set(-0.9);
+ osc2.shape.set(-0.8);
+ ampEnv.attack.set(0.3);
+ ampEnv.decay.set(0.8);
+ ampEnv.release.set(0.2);
+ filterEnv.sustain.set(0.7);
+ cutoff.set(500.0);
+ filterEnvDepth.set(500.0);
+ filter.Q.set(3.0);
+ break;
+ case 4:
+ osc1.shape.set(1.0);
+ osc2.shape.set(0.0);
+ break;
+ case 5:
+ osc1.shape.set(1.0);
+ setDetunePitch1(0.0);
+ osc2.shape.set(0.9);
+ setDetunePitch1(7.0);
+ break;
+ case 6:
+ osc1.shape.set(0.6);
+ osc2.shape.set(-0.2);
+ setDetunePitch1(0.01);
+ ampEnv.attack.set(0.005);
+ ampEnv.decay.set(0.09);
+ ampEnv.sustain.set(0.0);
+ ampEnv.release.set(1.0);
+ filterEnv.attack.set(0.005);
+ filterEnv.decay.set(0.1);
+ filterEnv.sustain.set(0.4);
+ filterEnv.release.set(1.0);
+ cutoff.set(2000.0);
+ filterEnvDepth.set(5000.0);
+ filter.Q.set(7.02);
+ break;
+ default:
+ break;
+ }
+ }
+
+ private static final String[] presetNames = {
+ "FastSaw", "SlowSaw", "BrightSaw",
+ "SoftSine", "SquareSaw", "SquareFifth",
+ "Blip"
+ };
+
+ static class MyVoiceDescription extends VoiceDescription {
+ String[] tags = {
+ "electronic", "filter", "analog", "subtractive"
+ };
+
+ public MyVoiceDescription() {
+ super(DualOscillatorSynthVoice.class.getName(), presetNames);
+ }
+
+ @Override
+ public UnitVoice createUnitVoice() {
+ return new DualOscillatorSynthVoice();
+ }
+
+ @Override
+ public String[] getTags(int presetIndex) {
+ return tags;
+ }
+
+ @Override
+ public String getVoiceClassName() {
+ return DualOscillatorSynthVoice.class.getName();
+ }
+ }
+
+ public static VoiceDescription getVoiceDescription() {
+ if (voiceDescription == null) {
+ voiceDescription = new MyVoiceDescription();
+ }
+ return voiceDescription;
+ }
+
+
+}
diff --git a/src/main/java/com/jsyn/instruments/JSynInstrumentLibrary.java b/src/main/java/com/jsyn/instruments/JSynInstrumentLibrary.java
new file mode 100644
index 0000000..9f111c3
--- /dev/null
+++ b/src/main/java/com/jsyn/instruments/JSynInstrumentLibrary.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.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(),
+ DualOscillatorSynthVoice.getVoiceDescription(),
+ NoiseHit.getVoiceDescription(),
+ DrumWoodFM.getVoiceDescription()
+ };
+
+ @Override
+ public VoiceDescription[] getVoiceDescriptions() {
+ return descriptions;
+ }
+
+ @Override
+ public String getName() {
+ return "JSynInstruments";
+ }
+}
diff --git a/src/main/java/com/jsyn/instruments/NoiseHit.java b/src/main/java/com/jsyn/instruments/NoiseHit.java
new file mode 100644
index 0000000..b8714fc
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/instruments/SubtractiveSynthVoice.java b/src/main/java/com/jsyn/instruments/SubtractiveSynthVoice.java
new file mode 100644
index 0000000..5cfc4b9
--- /dev/null
+++ b/src/main/java/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 one oscillator and a biquad 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 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;
+ case 2:
+ 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/main/java/com/jsyn/instruments/WaveShapingVoice.java b/src/main/java/com/jsyn/instruments/WaveShapingVoice.java
new file mode 100644
index 0000000..5044f21
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/io/AudioFifo.java b/src/main/java/com/jsyn/io/AudioFifo.java
new file mode 100644
index 0000000..0c563e4
--- /dev/null
+++ b/src/main/java/com/jsyn/io/AudioFifo.java
@@ -0,0 +1,204 @@
+/*
+ * 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;
+
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * 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;
+ final Lock lock = new ReentrantLock();
+ final Condition notFull = lock.newCondition();
+ final Condition notEmpty = lock.newCondition();
+
+ /**
+ * @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) {
+ lock.lock();
+ try {
+ while (available() < 1) {
+ try {
+ notEmpty.await();
+ } catch (InterruptedException e) {
+ return Double.NaN;
+ }
+ }
+ value = readOneInternal();
+ } finally {
+ lock.unlock();
+ }
+
+ } else {
+ if (readIndex != writeIndex) {
+ value = readOneInternal();
+ }
+ }
+
+ if (writeWaitEnabled) {
+ lock.lock();
+ notFull.signal();
+ lock.unlock();
+ }
+
+ return value;
+ }
+
+ private double readOneInternal() {
+ double value = buffer[readIndex & accessMask];
+ readIndex = (readIndex + 1) & sizeMask;
+ return value;
+ }
+
+ @Override
+ public void write(double value) {
+ if (writeWaitEnabled) {
+ lock.lock();
+ try {
+ while (available() == buffer.length)
+ {
+ try {
+ notFull.await();
+ } catch (InterruptedException e) {
+ return; // Silently fail
+ }
+ }
+ writeOneInternal(value);
+ } finally {
+ lock.unlock();
+ }
+
+ } else {
+ if (available() != buffer.length) {
+ writeOneInternal(value);
+ }
+ }
+
+ if (readWaitEnabled) {
+ lock.lock();
+ notEmpty.signal();
+ lock.unlock();
+ }
+ }
+
+ private void writeOneInternal(double value) {
+ buffer[writeIndex & accessMask] = value;
+ writeIndex = (writeIndex + 1) & sizeMask;
+ }
+
+ @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/main/java/com/jsyn/io/AudioInputStream.java b/src/main/java/com/jsyn/io/AudioInputStream.java
new file mode 100644
index 0000000..f233ff1
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/io/AudioOutputStream.java b/src/main/java/com/jsyn/io/AudioOutputStream.java
new file mode 100644
index 0000000..dada577
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/midi/MessageParser.java b/src/main/java/com/jsyn/midi/MessageParser.java
new file mode 100644
index 0000000..d0f5d4d
--- /dev/null
+++ b/src/main/java/com/jsyn/midi/MessageParser.java
@@ -0,0 +1,147 @@
+/*
+ * 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 {
+ private int[] parameterIndices = new int[MidiConstants.MAX_CHANNELS];
+ private int[] parameterValues = new int[MidiConstants.MAX_CHANNELS];
+ private int BIT_NON_RPM = 1 << 14;
+ private int MASK_14BIT = (1 << 14) - 1;
+
+ 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.POLYPHONIC_AFTERTOUCH:
+ polyphonicAftertouch(channel, message[1], message[2]);
+ break;
+
+ case MidiConstants.CHANNEL_PRESSURE:
+ channelPressure(channel, message[1]);
+ break;
+
+ case MidiConstants.CONTROL_CHANGE:
+ rawControlChange(channel, message[1], message[2]);
+ break;
+
+ case MidiConstants.PROGRAM_CHANGE:
+ programChange(channel, message[1]);
+ break;
+
+ case MidiConstants.PITCH_BEND:
+ int bend = (message[2] << 7) + message[1];
+ pitchBend(channel, bend);
+ break;
+ }
+
+ }
+
+ public void rawControlChange(int channel, int index, int value) {
+ int paramIndex;
+ int paramValue;
+ switch(index) {
+ case MidiConstants.CONTROLLER_DATA_ENTRY:
+ parameterValues[channel] = value << 7;
+ fireParameterChange(channel);
+ break;
+ case MidiConstants.CONTROLLER_DATA_ENTRY_LSB:
+ paramValue = parameterValues[channel] & ~0x7F;
+ paramValue |= value;
+ parameterValues[channel] = paramValue;
+ fireParameterChange(channel);
+ break;
+ case MidiConstants.CONTROLLER_NRPN_LSB:
+ paramIndex = parameterIndices[channel] & ~0x7F;
+ paramIndex |= value | BIT_NON_RPM;
+ parameterIndices[channel] = paramIndex;
+ break;
+ case MidiConstants.CONTROLLER_NRPN_MSB:
+ parameterIndices[channel] = (value << 7) | BIT_NON_RPM;;
+ break;
+ case MidiConstants.CONTROLLER_RPN_LSB:
+ paramIndex = parameterIndices[channel] & ~0x7F;
+ paramIndex |= value;
+ parameterIndices[channel] = paramIndex;
+ break;
+ case MidiConstants.CONTROLLER_RPN_MSB:
+ parameterIndices[channel] = value << 7;
+ break;
+ default:
+ controlChange(channel, index, value);
+ break;
+
+ }
+ }
+
+ private void fireParameterChange(int channel) {
+ int paramIndex;
+ paramIndex = parameterIndices[channel];
+ if ((paramIndex & BIT_NON_RPM) == 0) {
+ registeredParameter(channel, paramIndex, parameterValues[channel]);
+ } else {
+ nonRegisteredParameter(channel, paramIndex & MASK_14BIT, parameterValues[channel]);
+ }
+ }
+
+ public void nonRegisteredParameter(int channel, int index14, int value14) {
+ }
+
+ public void registeredParameter(int channel, int index14, int value14) {
+ }
+
+ public void pitchBend(int channel, int bend) {
+ }
+
+ public void programChange(int channel, int program) {
+ }
+
+ public void polyphonicAftertouch(int channel, int pitch, int pressure) {
+ }
+
+ public void channelPressure(int channel, int pressure) {
+ }
+
+ public void controlChange(int channel, int index, int value) {
+ }
+
+ public void noteOn(int channel, int pitch, int velocity) {
+ }
+
+ // If a NOTE_ON with zero velocity is received then noteOff will be called.
+ public void noteOff(int channel, int pitch, int velocity) {
+ }
+}
diff --git a/src/main/java/com/jsyn/midi/MidiConstants.java b/src/main/java/com/jsyn/midi/MidiConstants.java
new file mode 100644
index 0000000..8c92119
--- /dev/null
+++ b/src/main/java/com/jsyn/midi/MidiConstants.java
@@ -0,0 +1,84 @@
+/*
+ * 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 {
+
+ public static final int MAX_CHANNELS = 16;
+ // 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 CHANNEL_PRESSURE = CHANNEL_AFTERTOUCH;
+ public static final int PITCH_BEND = 0xE0;
+ public static final int SYSTEM_COMMON = 0xF0;
+
+ public static final int PITCH_BEND_CENTER = 0x2000;
+
+ public static final int CONTROLLER_BANK_SELECT = 0;
+ public static final int CONTROLLER_MOD_WHEEL = 1;
+ public static final int CONTROLLER_BREATH = 2;
+ public static final int CONTROLLER_DATA_ENTRY = 6;
+ public static final int CONTROLLER_VOLUME = 7;
+ public static final int CONTROLLER_PAN = 10;
+
+ public static final int CONTROLLER_LSB_OFFSET = 32;
+ public static final int CONTROLLER_DATA_ENTRY_LSB = CONTROLLER_DATA_ENTRY + CONTROLLER_LSB_OFFSET;
+
+ public static final int CONTROLLER_TIMBRE = 74; // Often used by MPE for Y axis control.
+
+ public static final int CONTROLLER_DATA_INCREMENT = 96;
+ public static final int CONTROLLER_DATA_DECREMENT = 97;
+ public static final int CONTROLLER_NRPN_LSB = 98;
+ public static final int CONTROLLER_NRPN_MSB = 99;
+ public static final int CONTROLLER_RPN_LSB = 100;
+ public static final int CONTROLLER_RPN_MSB = 101;
+
+ public static final int RPN_BEND_RANGE = 0;
+ public static final int RPN_FINE_TUNING = 1;
+
+ 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/main/java/com/jsyn/midi/MidiSynthesizer.java b/src/main/java/com/jsyn/midi/MidiSynthesizer.java
new file mode 100644
index 0000000..e5dbae7
--- /dev/null
+++ b/src/main/java/com/jsyn/midi/MidiSynthesizer.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2016 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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;
+
+import com.jsyn.util.MultiChannelSynthesizer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Map MIDI messages into calls to a MultiChannelSynthesizer.
+ * Handles CONTROLLER_MOD_WHEEL, TIMBRE, VOLUME and PAN.
+ * Handles Bend Range RPN.
+ *
+ * <pre><code>
+ voiceDescription = DualOscillatorSynthVoice.getVoiceDescription();
+ multiSynth = new MultiChannelSynthesizer();
+ final int startChannel = 0;
+ multiSynth.setup(synth, startChannel, NUM_CHANNELS, VOICES_PER_CHANNEL, voiceDescription);
+ midiSynthesizer = new MidiSynthesizer(multiSynth);
+ // pass MIDI bytes
+ midiSynthesizer.onReceive(bytes, 0, bytes.length);
+ </code></pre>
+ *
+ * See the example UseMidiKeyboard.java
+ *
+ * @author Phil Burk (C) 2016 Mobileer Inc
+ */
+public class MidiSynthesizer extends MessageParser {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(MidiSynthesizer.class);
+
+ private MultiChannelSynthesizer multiSynth;
+
+ public MidiSynthesizer(MultiChannelSynthesizer multiSynth) {
+ this.multiSynth = multiSynth;
+ }
+
+ @Override
+ public void controlChange(int channel, int index, int value) {
+ //LOGGER.debug("controlChange(" + channel + ", " + index + ", " + value + ")");
+ double normalized = value * (1.0 / 127.0);
+ switch (index) {
+ case MidiConstants.CONTROLLER_MOD_WHEEL:
+ double vibratoDepth = 0.1 * normalized;
+ LOGGER.debug( "vibratoDepth = " + vibratoDepth );
+ multiSynth.setVibratoDepth(channel, vibratoDepth);
+ break;
+ case MidiConstants.CONTROLLER_TIMBRE:
+ multiSynth.setTimbre(channel, normalized);
+ break;
+ case MidiConstants.CONTROLLER_VOLUME:
+ multiSynth.setVolume(channel, normalized);
+ break;
+ case MidiConstants.CONTROLLER_PAN:
+ // convert to -1 to +1 range
+ multiSynth.setPan(channel, (normalized * 2.0) - 1.0);
+ break;
+ }
+ }
+
+ @Override
+ public void registeredParameter(int channel, int index14, int value14) {
+ switch(index14) {
+ case MidiConstants.RPN_BEND_RANGE:
+ int semitones = value14 >> 7;
+ int cents = value14 & 0x7F;
+ double bendRange = semitones + (cents * 0.01);
+ multiSynth.setBendRange(channel, bendRange);
+ break;
+ default:
+ break;
+ }
+ }
+
+ @Override
+ public void programChange(int channel, int program) {
+ multiSynth.programChange(channel, program);
+ }
+
+ @Override
+ public void channelPressure(int channel, int value) {
+ double normalized = value * (1.0 / 127.0);
+ multiSynth.setPressure(channel, normalized);
+ }
+
+ @Override
+ public void noteOff(int channel, int noteNumber, int velocity) {
+ multiSynth.noteOff(channel, noteNumber, velocity);
+ }
+
+ @Override
+ public void noteOn(int channel, int noteNumber, int velocity) {
+ multiSynth.noteOn(channel, noteNumber, velocity);
+ }
+
+ @Override
+ public void pitchBend(int channel, int bend) {
+ double offset = (bend - MidiConstants.PITCH_BEND_CENTER)
+ * (1.0 / (MidiConstants.PITCH_BEND_CENTER));
+ multiSynth.setPitchBend(channel, offset);
+ }
+
+ public void onReceive(byte[] bytes, int i, int length) {
+ parse(bytes); // TODO
+ }
+
+}
diff --git a/src/main/java/com/jsyn/package.html b/src/main/java/com/jsyn/package.html
new file mode 100644
index 0000000..cd73832
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/ports/ConnectableInput.java b/src/main/java/com/jsyn/ports/ConnectableInput.java
new file mode 100644
index 0000000..3dae876
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/ports/ConnectableOutput.java b/src/main/java/com/jsyn/ports/ConnectableOutput.java
new file mode 100644
index 0000000..f42a799
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/ports/GettablePort.java b/src/main/java/com/jsyn/ports/GettablePort.java
new file mode 100644
index 0000000..aabf5ca
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/ports/InputMixingBlockPart.java b/src/main/java/com/jsyn/ports/InputMixingBlockPart.java
new file mode 100644
index 0000000..2d28888
--- /dev/null
+++ b/src/main/java/com/jsyn/ports/InputMixingBlockPart.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.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;
+ private UnitInputPort unitInputPort;
+
+ InputMixingBlockPart(UnitInputPort unitInputPort, double defaultValue) {
+ super(unitInputPort, defaultValue);
+ this.unitInputPort = unitInputPort;
+ }
+
+ @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();
+ // LOGGER.debug("numConnection = " + numConnections + " for " +
+ // this );
+ if (numConnections == 0) {
+ // No connection so just use our own data.
+ result = super.getValues();
+ } else {
+ // Mix all of the connected ports.
+ double[] inputs;
+ int jCon = 0;
+ PortBlockPart otherPart;
+ // Choose value to initialize the mixer array.
+ if (unitInputPort.isValueAdded()) {
+ inputs = super.getValues(); // prime mixer with the set() values
+ jCon = 0;
+ } else {
+ otherPart = getConnection(jCon);
+ inputs = otherPart.getValues(); // prime mixer with first connected
+ jCon = 1;
+ }
+ for (int i = 0; i < mixer.length; i++) {
+ mixer[i] = inputs[i];
+ }
+ // Now mix in the remaining inputs.
+ for (; jCon < numConnections; jCon++) {
+ otherPart = getConnection(jCon);
+ inputs = otherPart.getValues();
+ for (int i = 0; i < mixer.length; i++) {
+ mixer[i] += inputs[i];
+ }
+ }
+ 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/main/java/com/jsyn/ports/PortBlockPart.java b/src/main/java/com/jsyn/ports/PortBlockPart.java
new file mode 100644
index 0000000..b1ced32
--- /dev/null
+++ b/src/main/java/com/jsyn/ports/PortBlockPart.java
@@ -0,0 +1,210 @@
+/*
+ * 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;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * 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 static final Logger LOGGER = LoggerFactory.getLogger(PortBlockPart.class);
+
+ 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) {
+ // LOGGER.debug("addConnection from " + this + " to " + otherPart
+ // );
+ if (connections.contains(otherPart)) {
+ LOGGER.debug("addConnection already had connection from " + this + " to "
+ + otherPart);
+ } else {
+ connections.add(otherPart);
+ }
+ }
+
+ private void removeConnection(PortBlockPart otherPart) {
+ // LOGGER.debug("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/main/java/com/jsyn/ports/QueueDataCommand.java b/src/main/java/com/jsyn/ports/QueueDataCommand.java
new file mode 100644
index 0000000..0ef36e2
--- /dev/null
+++ b/src/main/java/com/jsyn/ports/QueueDataCommand.java
@@ -0,0 +1,170 @@
+/*
+ * 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 )
+ * {
+ * LOGGER.debug("CALLBACK: Envelope started.");
+ * }
+ *
+ * public void looped( QueueDataEvent event )
+ * {
+ * LOGGER.debug("CALLBACK: Envelope looped.");
+ * }
+ *
+ * public void finished( QueueDataEvent event )
+ * {
+ * LOGGER.debug("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);
+
+ if ((startFrame + numFrames) > sequentialData.getNumFrames()) {
+ throw new IllegalArgumentException("tried to queue past end of data, " + (startFrame + numFrames));
+ } else if (startFrame < 0) {
+ throw new IllegalArgumentException("tried to queue before start of data, " + startFrame);
+ }
+ 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/main/java/com/jsyn/ports/QueueDataEvent.java b/src/main/java/com/jsyn/ports/QueueDataEvent.java
new file mode 100644
index 0000000..2b93fab
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/ports/SequentialDataCrossfade.java b/src/main/java/com/jsyn/ports/SequentialDataCrossfade.java
new file mode 100644
index 0000000..0c3d3b2
--- /dev/null
+++ b/src/main/java/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());
+
+ // LOGGER.debug( "WARNING! sourceStartFrame = " + sourceStartFrame
+ // + ", crossFadeFrames = " + crossFadeFrames + ", maxFrame = "
+ // + source.getNumFrames() + ", source = " + source );
+ // LOGGER.debug( " 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/main/java/com/jsyn/ports/SettablePort.java b/src/main/java/com/jsyn/ports/SettablePort.java
new file mode 100644
index 0000000..e0db05c
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/ports/UnitBlockPort.java b/src/main/java/com/jsyn/ports/UnitBlockPort.java
new file mode 100644
index 0000000..d7fc82f
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/ports/UnitDataQueueCallback.java b/src/main/java/com/jsyn/ports/UnitDataQueueCallback.java
new file mode 100644
index 0000000..dca4adc
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/ports/UnitDataQueuePort.java b/src/main/java/com/jsyn/ports/UnitDataQueuePort.java
new file mode 100644
index 0000000..13b2e2a
--- /dev/null
+++ b/src/main/java/com/jsyn/ports/UnitDataQueuePort.java
@@ -0,0 +1,466 @@
+/*
+ * 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 final 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);
+ // LOGGER.debug("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) {
+ // No loops
+ queueImmediate(queueableData, 0, queueableData.getNumFrames(), timeStamp);
+ } 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.
+ *
+ * @param 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;
+ // LOGGER.debug("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/main/java/com/jsyn/ports/UnitFunctionPort.java b/src/main/java/com/jsyn/ports/UnitFunctionPort.java
new file mode 100644
index 0000000..e45241a
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/ports/UnitGatePort.java b/src/main/java/com/jsyn/ports/UnitGatePort.java
new file mode 100644
index 0000000..700aef8
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/ports/UnitInputPort.java b/src/main/java/com/jsyn/ports/UnitInputPort.java
new file mode 100644
index 0000000..3eda1f6
--- /dev/null
+++ b/src/main/java/com/jsyn/ports/UnitInputPort.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.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;
+ private boolean valueAdded = false;
+
+ /**
+ * @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 boolean isValueAdded() {
+ return valueAdded;
+ }
+
+ /**
+ * If set false then the set() value will be ignored when other ports are connected to this port.
+ * The sum of the connected port values will be used instead.
+ *
+ * If set true then the set() value will be added to the sum of the connected port values.
+ * This is useful when you want to modulate the set value.
+ *
+ * The default is false.
+ *
+ * @param valueAdded
+ */
+ public void setValueAdded(boolean valueAdded) {
+ this.valueAdded = valueAdded;
+ }
+
+ 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/main/java/com/jsyn/ports/UnitOutputPort.java b/src/main/java/com/jsyn/ports/UnitOutputPort.java
new file mode 100644
index 0000000..6fcd758
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/ports/UnitPort.java b/src/main/java/com/jsyn/ports/UnitPort.java
new file mode 100644
index 0000000..a652e68
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/ports/UnitSpectralInputPort.java b/src/main/java/com/jsyn/ports/UnitSpectralInputPort.java
new file mode 100644
index 0000000..bdf0ff5
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/ports/UnitSpectralOutputPort.java b/src/main/java/com/jsyn/ports/UnitSpectralOutputPort.java
new file mode 100644
index 0000000..51633ce
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/ports/UnitVariablePort.java b/src/main/java/com/jsyn/ports/UnitVariablePort.java
new file mode 100644
index 0000000..60b64fd
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/ports/package.html b/src/main/java/com/jsyn/ports/package.html
new file mode 100644
index 0000000..3547618
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/scope/AudioScope.java b/src/main/java/com/jsyn/scope/AudioScope.java
new file mode 100644
index 0000000..7b2a98c
--- /dev/null
+++ b/src/main/java/com/jsyn/scope/AudioScope.java
@@ -0,0 +1,101 @@
+/*
+ * 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();
+ }
+
+ /**
+ * Not yet implemented.
+ * @param viewMode
+ */
+ public void setViewMode(ViewMode viewMode) {
+ // TODO Auto-generated method stub
+ }
+
+}
diff --git a/src/main/java/com/jsyn/scope/AudioScopeModel.java b/src/main/java/com/jsyn/scope/AudioScopeModel.java
new file mode 100644
index 0000000..85c4413
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/scope/AudioScopeProbe.java b/src/main/java/com/jsyn/scope/AudioScopeProbe.java
new file mode 100644
index 0000000..f1aad65
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/scope/DefaultWaveTraceModel.java b/src/main/java/com/jsyn/scope/DefaultWaveTraceModel.java
new file mode 100644
index 0000000..a123c0b
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/scope/MultiChannelScopeProbeUnit.java b/src/main/java/com/jsyn/scope/MultiChannelScopeProbeUnit.java
new file mode 100644
index 0000000..59bb635
--- /dev/null
+++ b/src/main/java/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.
+ static 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/main/java/com/jsyn/scope/TriggerModel.java b/src/main/java/com/jsyn/scope/TriggerModel.java
new file mode 100644
index 0000000..0367d71
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/scope/WaveTraceModel.java b/src/main/java/com/jsyn/scope/WaveTraceModel.java
new file mode 100644
index 0000000..e9d8bf9
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/scope/swing/AudioScopeProbeView.java b/src/main/java/com/jsyn/scope/swing/AudioScopeProbeView.java
new file mode 100644
index 0000000..59526e1
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/scope/swing/AudioScopeView.java b/src/main/java/com/jsyn/scope/swing/AudioScopeView.java
new file mode 100644
index 0000000..ec1afa3
--- /dev/null
+++ b/src/main/java/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 Use setControlsVisible() instead. */
+ @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/main/java/com/jsyn/scope/swing/MultipleWaveDisplay.java b/src/main/java/com/jsyn/scope/swing/MultipleWaveDisplay.java
new file mode 100644
index 0000000..0259850
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/scope/swing/ScopeControlPanel.java b/src/main/java/com/jsyn/scope/swing/ScopeControlPanel.java
new file mode 100644
index 0000000..7f3a026
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/scope/swing/ScopeProbePanel.java b/src/main/java/com/jsyn/scope/swing/ScopeProbePanel.java
new file mode 100644
index 0000000..a0dec91
--- /dev/null
+++ b/src/main/java/com/jsyn/scope/swing/ScopeProbePanel.java
@@ -0,0 +1,87 @@
+/*
+ * 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;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class ScopeProbePanel extends JPanel {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(ScopeProbePanel.class);
+ 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();
+ LOGGER.debug("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/main/java/com/jsyn/scope/swing/ScopeTriggerPanel.java b/src/main/java/com/jsyn/scope/swing/ScopeTriggerPanel.java
new file mode 100644
index 0000000..9c22aa1
--- /dev/null
+++ b/src/main/java/com/jsyn/scope/swing/ScopeTriggerPanel.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.scope.swing;
+
+import java.awt.BorderLayout;
+
+import javax.swing.DefaultComboBoxModel;
+import javax.swing.JComboBox;
+import javax.swing.JPanel;
+
+import com.jsyn.scope.AudioScopeModel;
+import com.jsyn.scope.TriggerModel;
+import com.jsyn.scope.AudioScope.TriggerMode;
+import com.jsyn.swing.RotaryTextController;
+
+public class ScopeTriggerPanel extends JPanel {
+ private static final long serialVersionUID = 4511589171299298548L;
+ private JComboBox<DefaultComboBoxModel<TriggerMode>> 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/main/java/com/jsyn/scope/swing/WaveTraceView.java b/src/main/java/com/jsyn/scope/swing/WaveTraceView.java
new file mode 100644
index 0000000..849a6f4
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/swing/ASCIIMusicKeyboard.java b/src/main/java/com/jsyn/swing/ASCIIMusicKeyboard.java
new file mode 100644
index 0000000..dc02259
--- /dev/null
+++ b/src/main/java/com/jsyn/swing/ASCIIMusicKeyboard.java
@@ -0,0 +1,199 @@
+/*
+ * 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 org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+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 static final Logger LOGGER = LoggerFactory.getLogger(ASCIIMusicKeyboard.class);
+
+ private final JCheckBox sustainBox;
+ private final 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 final KeyListener keyListener;
+ private final JLabel countLabel;
+ private int onCount;
+ private int offCount;
+ private int pressedCount;
+ private int releasedCount;
+ private final HashSet<Integer> pressedKeys = new HashSet<Integer>();
+ private final 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);
+ LOGGER.debug("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);
+ LOGGER.debug("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 keyIndex
+ */
+ public abstract void keyOff(int keyIndex);
+
+ /**
+ * This will be called when a key is pressed.
+ *
+ * @param keyIndex
+ */
+ public abstract void keyOn(int keyIndex);
+
+ public String getKeyboardLayout() {
+ return keyboardLayout;
+ }
+
+ /**
+ * Specify the keys that will be active for music.
+ * For example "qwertyui".
+ * 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/main/java/com/jsyn/swing/DoubleBoundedRangeModel.java b/src/main/java/com/jsyn/swing/DoubleBoundedRangeModel.java
new file mode 100644
index 0000000..647e8da
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/swing/DoubleBoundedRangeSlider.java b/src/main/java/com/jsyn/swing/DoubleBoundedRangeSlider.java
new file mode 100644
index 0000000..81b67df
--- /dev/null
+++ b/src/main/java/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.jsyn.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(sliderValue, new JLabel(text));
+ dval = nextLabelValue(dval, delta);
+ }
+ setLabelTable(labelTable);
+ setPaintLabels(true);
+ }
+
+}
diff --git a/src/main/java/com/jsyn/swing/DoubleBoundedTextField.java b/src/main/java/com/jsyn/swing/DoubleBoundedTextField.java
new file mode 100644
index 0000000..3301bb1
--- /dev/null
+++ b/src/main/java/com/jsyn/swing/DoubleBoundedTextField.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.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;
+
+/**
+ * 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(String.format("%6.4f", value));
+ markClean();
+ }
+}
diff --git a/src/main/java/com/jsyn/swing/EnvelopeEditorBox.java b/src/main/java/com/jsyn/swing/EnvelopeEditorBox.java
new file mode 100644
index 0000000..2db4c29
--- /dev/null
+++ b/src/main/java/com/jsyn/swing/EnvelopeEditorBox.java
@@ -0,0 +1,573 @@
+/*
+ * 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.jsyn.data.SegmentedEnvelope;
+import com.jsyn.unitgen.VariableRateDataReader;
+
+/**
+ * Edit a list of ordered duration,value pairs suitable for use with a SegmentedEnvelope.
+ *
+ * @author (C) 1997-2013 Phil Burk, SoftSynth.com
+ * @see EnvelopePoints
+ * @see SegmentedEnvelope
+ * @see VariableRateDataReader
+ */
+
+/* ========================================================================== */
+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;
+ }
+ // LOGGER.debug("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;
+ // LOGGER.debug("wxLow = " + wxLow + ", wxHigh = " + wxHigh );
+ double wyLow = wy - wyAperture;
+ double wyHigh = wy + wyAperture;
+ // LOGGER.debug("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];
+ // LOGGER.debug("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);
+ // LOGGER.debug("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;
+ }
+ // LOGGER.debug("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);
+ // LOGGER.debug("wxAp = " + wxAp );
+ double wyAp = convertGYtoWY(0) - convertGYtoWY(radius + 2);
+ // LOGGER.debug("wyAp = " + wyAp );
+ int pnt = pickPoint(wx, wxAp, wy, wyAp);
+ // LOGGER.debug("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);
+ // LOGGER.debug("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 envelope drag limit
+ else
+ dragLowLimit = xPicked - points.getPoint(dragIndex)[0];
+ dragHighLimit = xPicked + (maximumXRange - points.getTotalDuration());
+ // LOGGER.debug("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 = getHeight();
+ 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?
+ // LOGGER.debug("newMax = " + newMax );
+ if (newMax < minimumXRange)
+ newMax = minimumXRange;
+ setMaxWorldX(newMax);
+ }
+ }
+ // LOGGER.debug("total X = " + wx );
+
+ drawUnderlay(g);
+
+ drawAllPoints(g);
+
+ /* Show X,Y,TotalX as text. */
+ g.drawString(points.getName() + ", len=" + String.format("%7.3f", wx), 5, 15);
+ if ((draggedPoint != null) && (dragIndex >= 0)) {
+ String s = "i=" + dragIndex + ", dur="
+ + String.format("%7.3f", draggedPoint[0]) + ", y = "
+ + String.format("%8.4f", draggedPoint[1]);
+ g.drawString(s, 5, 30);
+ }
+ }
+}
diff --git a/src/main/java/com/jsyn/swing/EnvelopeEditorPanel.java b/src/main/java/com/jsyn/swing/EnvelopeEditorPanel.java
new file mode 100644
index 0000000..dc9f2cd
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/swing/EnvelopePoints.java b/src/main/java/com/jsyn/swing/EnvelopePoints.java
new file mode 100644
index 0000000..ab4ed03
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/swing/ExponentialRangeModel.java b/src/main/java/com/jsyn/swing/ExponentialRangeModel.java
new file mode 100644
index 0000000..c807000
--- /dev/null
+++ b/src/main/java/com/jsyn/swing/ExponentialRangeModel.java
@@ -0,0 +1,110 @@
+/*
+ * 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 org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * 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 Logger LOGGER = LoggerFactory.getLogger(ExponentialRangeModel.class);
+ 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();
+ return (a * Math.pow(root, span * x)) + b;
+ }
+
+ @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);
+ LOGGER.debug(sliderValue + " => " + dval + " => " + ival);
+ }
+
+}
diff --git a/src/main/java/com/jsyn/swing/InstrumentBrowser.java b/src/main/java/com/jsyn/swing/InstrumentBrowser.java
new file mode 100644
index 0000000..8e74660
--- /dev/null
+++ b/src/main/java/com/jsyn/swing/InstrumentBrowser.java
@@ -0,0 +1,117 @@
+/*
+ * 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 JScrollPane listScroller2;
+ private VoiceDescription voiceDescription;
+ private ArrayList<PresetSelectionListener> listeners = new ArrayList<>();
+
+ public InstrumentBrowser(InstrumentLibrary library) {
+ this.library = library;
+ JPanel horizontalPanel = new JPanel();
+ horizontalPanel.setLayout(new GridLayout(1, 2));
+
+ final JList<VoiceDescription> instrumentList = new JList<VoiceDescription>(library.getVoiceDescriptions());
+ setupList(instrumentList);
+ instrumentList.addListSelectionListener(new ListSelectionListener() {
+ @Override
+ public void valueChanged(ListSelectionEvent e) {
+ if (!e.getValueIsAdjusting()) {
+ int n = instrumentList.getSelectedIndex();
+ if (n >= 0) {
+ showPresetList(n);
+ }
+ }
+ }
+ });
+
+ JScrollPane listScroller1 = new JScrollPane(instrumentList);
+ listScroller1.setPreferredSize(new Dimension(250, 120));
+ add(listScroller1);
+
+ instrumentList.setSelectedIndex(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<String> presetList = new JList<String>(voiceDescription.getPresetNames());
+ setupList(presetList);
+ 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 void setupList(@SuppressWarnings("rawtypes") JList list) {
+ list.setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);
+ list.setLayoutOrientation(JList.VERTICAL);
+ list.setVisibleRowCount(-1);
+ }
+}
diff --git a/src/main/java/com/jsyn/swing/JAppletFrame.java b/src/main/java/com/jsyn/swing/JAppletFrame.java
new file mode 100644
index 0000000..53bd65b
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/swing/PortBoundedRangeModel.java b/src/main/java/com/jsyn/swing/PortBoundedRangeModel.java
new file mode 100644
index 0000000..a5cf841
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/swing/PortControllerFactory.java b/src/main/java/com/jsyn/swing/PortControllerFactory.java
new file mode 100644
index 0000000..a73d047
--- /dev/null
+++ b/src/main/java/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, 4);
+ }
+
+ 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, 4);
+ }
+
+}
diff --git a/src/main/java/com/jsyn/swing/PortModelFactory.java b/src/main/java/com/jsyn/swing/PortModelFactory.java
new file mode 100644
index 0000000..8bec76a
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/swing/PresetSelectionListener.java b/src/main/java/com/jsyn/swing/PresetSelectionListener.java
new file mode 100644
index 0000000..daf0310
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/swing/RotaryController.java b/src/main/java/com/jsyn/swing/RotaryController.java
new file mode 100644
index 0000000..c26c37f
--- /dev/null
+++ b/src/main/java/com/jsyn/swing/RotaryController.java
@@ -0,0 +1,335 @@
+/*
+ * 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 <b>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 final BoundedRangeModel model;
+
+ private final double minAngle = 1.4 * Math.PI;
+ private final double maxAngle = -0.4 * Math.PI;
+ private final 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) {
+ safeRepaint();
+ }
+
+ });
+ }
+
+ // This can be overridden in subclasses to workaround OpenJDK bugs.
+ public void safeRepaint() {
+ 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/main/java/com/jsyn/swing/RotaryTextController.java b/src/main/java/com/jsyn/swing/RotaryTextController.java
new file mode 100644
index 0000000..81d6614
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/swing/SoundTweaker.java b/src/main/java/com/jsyn/swing/SoundTweaker.java
new file mode 100644
index 0000000..dc48c8f
--- /dev/null
+++ b/src/main/java/com/jsyn/swing/SoundTweaker.java
@@ -0,0 +1,120 @@
+/*
+ * 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, 2));
+
+ UnitGenerator ugen = source.getUnitGenerator();
+ ArrayList<Component> sliders = new ArrayList<Component>();
+
+ add(new JLabel(title));
+
+ if (source instanceof Instrument) {
+ add(keyboard = createPolyphonicKeyboard());
+ } else if (source instanceof UnitVoice) {
+ add(keyboard = createMonophonicKeyboard());
+ }
+
+ // Arrange the faders in a stack.
+ // 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() {
+ return new ASCIIMusicKeyboard() {
+ @Override
+ public void keyOff(int pitch) {
+ ((Instrument) source).noteOff(pitch, synth.createTimeStamp());
+ }
+
+ @Override
+ public void keyOn(int pitch) {
+ double freq = AudioMath.pitchToFrequency(pitch);
+ ((Instrument) source).noteOn(pitch, freq, 0.5, synth.createTimeStamp());
+ }
+ };
+ }
+
+ @SuppressWarnings("serial")
+ private ASCIIMusicKeyboard createMonophonicKeyboard() {
+ return 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());
+ }
+ };
+ }
+
+}
diff --git a/src/main/java/com/jsyn/swing/XYController.java b/src/main/java/com/jsyn/swing/XYController.java
new file mode 100644
index 0000000..0d97c62
--- /dev/null
+++ b/src/main/java/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 = getWidth();
+ return minWorldX + ((maxWorldX - minWorldX) * gx) / width;
+ }
+
+ public double convertGYtoWY(int gy) {
+ int height = getHeight();
+ return minWorldY + ((maxWorldY - minWorldY) * (height - gy)) / height;
+ }
+
+ /** Convert from world coordinates to graphics coordinates (pixels). */
+ public int convertWXtoGX(double wx) {
+ int width = getWidth();
+ return (int) (((wx - minWorldX) * width) / (maxWorldX - minWorldX));
+ }
+
+ public int convertWYtoGY(double wy) {
+ int height = getHeight();
+ return height - (int) (((wy - minWorldY) * height) / (maxWorldY - minWorldY));
+ }
+
+ /** Clip wx to the min and 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 and 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/main/java/com/jsyn/unitgen/Add.java b/src/main/java/com/jsyn/unitgen/Add.java
new file mode 100644
index 0000000..c91fc85
--- /dev/null
+++ b/src/main/java/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();
+
+ // LOGGER.debug("adder = " + this);
+ // LOGGER.debug("A = " + aValues[0]);
+ for (int i = start; i < limit; i++) {
+ outputs[i] = aValues[i] + bValues[i];
+ }
+ // LOGGER.debug("add out = " + outputs[0]);
+ }
+}
diff --git a/src/main/java/com/jsyn/unitgen/AsymptoticRamp.java b/src/main/java/com/jsyn/unitgen/AsymptoticRamp.java
new file mode 100644
index 0000000..8b51294
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/BrownNoise.java b/src/main/java/com/jsyn/unitgen/BrownNoise.java
new file mode 100644
index 0000000..e70b7f4
--- /dev/null
+++ b/src/main/java/com/jsyn/unitgen/BrownNoise.java
@@ -0,0 +1,75 @@
+/*
+ * 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;
+
+/**
+ * BrownNoise unit. This unit uses a pseudo-random number generator to produce a noise related to
+ * Brownian Motion. A DC blocker is used to prevent runaway drift.
+ *
+ * <pre>
+ * <code>
+ * output = (previous * (1.0 - damping)) + (random * amplitude)
+ * </code>
+ * </pre>
+ *
+ * The output drifts quite a bit and will generally exceed the range of +/1 amplitude.
+ *
+ * @author (C) 1997-2011 Phil Burk, Mobileer Inc
+ * @see WhiteNoise
+ * @see RedNoise
+ * @see PinkNoise
+ */
+public class BrownNoise extends UnitGenerator implements UnitSource {
+ private PseudoRandom randomNum;
+ /** Increasing the damping will effectively increase the cutoff
+ * frequency of a high pass filter that is used to block DC bias.
+ * Warning: setting this too close to zero can result in very large output values.
+ */
+ public UnitInputPort damping;
+ public UnitInputPort amplitude;
+ public UnitOutputPort output;
+ private double previous;
+
+ public BrownNoise() {
+ randomNum = new PseudoRandom();
+ addPort(damping = new UnitInputPort("Damping"));
+ damping.setup(0.0001, 0.01, 0.1);
+ 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();
+ double damper = 1.0 - damping.getValues()[0];
+
+ for (int i = start; i < limit; i++) {
+ double r = randomNum.nextRandomDouble() * amplitudes[i];
+ outputs[i] = previous = (damper * previous) + r;
+ }
+ }
+
+ @Override
+ public UnitOutputPort getOutput() {
+ return output;
+ }
+}
diff --git a/src/main/java/com/jsyn/unitgen/ChannelIn.java b/src/main/java/com/jsyn/unitgen/ChannelIn.java
new file mode 100644
index 0000000..c440b4f
--- /dev/null
+++ b/src/main/java/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 {@link com.jsyn.Synthesizer} start() method with numInputChannels &gt; 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/main/java/com/jsyn/unitgen/ChannelOut.java b/src/main/java/com/jsyn/unitgen/ChannelOut.java
new file mode 100644
index 0000000..8ef0677
--- /dev/null
+++ b/src/main/java/com/jsyn/unitgen/ChannelOut.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.UnitInputPort;
+
+/**
+ * Provides access to one channel of the audio output.
+ * For more than two channels you must call
+ * the {@link com.jsyn.Synthesizer} start() method with numOutputChannels &gt; 2.
+ *
+ * @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;
+ }
+
+ /**
+ * This unit won't do anything unless you start() it.
+ */
+ @Override
+ public boolean isStartRequired() {
+ return true;
+ }
+
+ @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/main/java/com/jsyn/unitgen/Circuit.java b/src/main/java/com/jsyn/unitgen/Circuit.java
new file mode 100644
index 0000000..01cb860
--- /dev/null
+++ b/src/main/java/com/jsyn/unitgen/Circuit.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.unitgen;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+
+import com.jsyn.engine.SynthesisEngine;
+import com.jsyn.ports.UnitPort;
+
+/**
+ * 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>();
+
+ private final LinkedHashMap<String, UnitPort> portAliases = new LinkedHashMap<String, UnitPort>();
+
+ @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);
+ }
+ }
+
+ /**
+ * @deprecated ignored, frameRate comes from the SynthesisEngine
+ * @param frameRate
+ */
+ @Deprecated
+ @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) {
+ }
+
+
+ /**
+ * Add an alternate name for looking up a port.
+ * @param port
+ * @param alias
+ */
+ public void addPortAlias(UnitPort port, String alias) {
+ // Store in a hash table by an alternate name.
+ portAliases.put(alias.toLowerCase(), port);
+ }
+
+
+ /**
+ * Case-insensitive search for a port by its name or alias.
+ * @param portName
+ * @return matching port or null
+ */
+ @Override
+ public UnitPort getPortByName(String portName) {
+ UnitPort port = super.getPortByName(portName);
+ if (port == null) {
+ port = portAliases.get(portName.toLowerCase());
+ }
+ return port;
+ }
+
+}
diff --git a/src/main/java/com/jsyn/unitgen/Compare.java b/src/main/java/com/jsyn/unitgen/Compare.java
new file mode 100644
index 0000000..7de2e53
--- /dev/null
+++ b/src/main/java/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 &gt; 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/main/java/com/jsyn/unitgen/ContinuousRamp.java b/src/main/java/com/jsyn/unitgen/ContinuousRamp.java
new file mode 100644
index 0000000..dd90445
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/CrossFade.java b/src/main/java/com/jsyn/unitgen/CrossFade.java
new file mode 100644
index 0000000..4375fa6
--- /dev/null
+++ b/src/main/java/com/jsyn/unitgen/CrossFade.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.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"));
+ fade.setup(-1.0, 0.0, 1.0);
+ 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/main/java/com/jsyn/unitgen/Delay.java b/src/main/java/com/jsyn/unitgen/Delay.java
new file mode 100644
index 0000000..aa450a9
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/Divide.java b/src/main/java/com/jsyn/unitgen/Divide.java
new file mode 100644
index 0000000..cddcd7c
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/DualInTwoOut.java b/src/main/java/com/jsyn/unitgen/DualInTwoOut.java
new file mode 100644
index 0000000..ec7dff5
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/EdgeDetector.java b/src/main/java/com/jsyn/unitgen/EdgeDetector.java
new file mode 100644
index 0000000..e314f7d
--- /dev/null
+++ b/src/main/java/com/jsyn/unitgen/EdgeDetector.java
@@ -0,0 +1,44 @@
+/*
+ * 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;
+
+ 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 in = inputs[i];
+ outputs[i] = ((previous <= 0.0) && (in > 0.0)) ? 1.0 : 0.0;
+ previous = in;
+ }
+ }
+}
diff --git a/src/main/java/com/jsyn/unitgen/EnvelopeAttackDecay.java b/src/main/java/com/jsyn/unitgen/EnvelopeAttackDecay.java
new file mode 100644
index 0000000..db3ecaa
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/EnvelopeDAHDSR.java b/src/main/java/com/jsyn/unitgen/EnvelopeDAHDSR.java
new file mode 100644
index 0000000..c5ebe83
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/ExponentialRamp.java b/src/main/java/com/jsyn/unitgen/ExponentialRamp.java
new file mode 100644
index 0000000..36159b4
--- /dev/null
+++ b/src/main/java/com/jsyn/unitgen/ExponentialRamp.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.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"));
+ input.setup(0.0001, 1.0, 1.0);
+ 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/main/java/com/jsyn/unitgen/FFT.java b/src/main/java/com/jsyn/unitgen/FFT.java
new file mode 100644
index 0000000..63fce50
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/FFTBase.java b/src/main/java/com/jsyn/unitgen/FFTBase.java
new file mode 100644
index 0000000..055c04b
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/FilterAllPass.java b/src/main/java/com/jsyn/unitgen/FilterAllPass.java
new file mode 100644
index 0000000..749b2d6
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/FilterBandPass.java b/src/main/java/com/jsyn/unitgen/FilterBandPass.java
new file mode 100644
index 0000000..b103400
--- /dev/null
+++ b/src/main/java/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
+ * Tolentino.
+ */
+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/main/java/com/jsyn/unitgen/FilterBandStop.java b/src/main/java/com/jsyn/unitgen/FilterBandStop.java
new file mode 100644
index 0000000..d4f5249
--- /dev/null
+++ b/src/main/java/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
+ * Tolentino.
+ */
+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/main/java/com/jsyn/unitgen/FilterBiquad.java b/src/main/java/com/jsyn/unitgen/FilterBiquad.java
new file mode 100644
index 0000000..f9b792f
--- /dev/null
+++ b/src/main/java/com/jsyn/unitgen/FilterBiquad.java
@@ -0,0 +1,156 @@
+/*
+ * 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 FilterTwoPolesTwoZeros
+ */
+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:
+ *
+ * <pre>
+ * y(n) = A0*x(n) + A1*x(n-1) + A2*x(n-2) -vB1*y(n-1) - B2*y(n-2)
+ * </pre>
+ *
+ * Here is the equation that Robert Bristow-Johnson uses:
+ *
+ * <pre>
+ * 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]
+ * </pre>
+ *
+ * So to translate between JSyn coefficients and RBJ coefficients:
+ *
+ * <pre>
+ * JSyn =&gt; RBJ
+ * A0 =&gt; b0/a0
+ * A1 =&gt; b1/a0
+ * A2 =&gt; b2/a0
+ * B1 =&gt; a1/a0
+ * B2 =&gt; 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/main/java/com/jsyn/unitgen/FilterBiquadCommon.java b/src/main/java/com/jsyn/unitgen/FilterBiquadCommon.java
new file mode 100644
index 0000000..d84b39a
--- /dev/null
+++ b/src/main/java/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
+ * Tolentino.
+ */
+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
+ // LOGGER.debug("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/main/java/com/jsyn/unitgen/FilterBiquadShelf.java b/src/main/java/com/jsyn/unitgen/FilterBiquadShelf.java
new file mode 100644
index 0000000..737d18d
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/FilterFourPoles.java b/src/main/java/com/jsyn/unitgen/FilterFourPoles.java
new file mode 100644
index 0000000..39a47c7
--- /dev/null
+++ b/src/main/java/com/jsyn/unitgen/FilterFourPoles.java
@@ -0,0 +1,185 @@
+/*
+ * 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 = 1.0; // blows up if near 0.01
+ private static final double MINIMUM_Q = 0.00001;
+
+ //private static final double SATURATION_COEFFICIENT = 0.1666667;
+ private static final double SATURATION_COEFFICIENT = 0.2;
+ // Inflection point where slope is zero.
+ private static final double SATURATION_UPPER_INPUT = 1.0 / Math.sqrt(3.0 * SATURATION_COEFFICIENT);
+ private static final double SATURATION_LOWER_INPUT = 0.0 - SATURATION_UPPER_INPUT;
+ private static final double SATURATION_UPPER_OUTPUT = cubicPolynomial(SATURATION_UPPER_INPUT);
+ private static final double SATURATION_LOWER_OUTPUT = cubicPolynomial(SATURATION_LOWER_INPUT);
+
+ 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"));
+ frequency.setup(40.0, DEFAULT_FREQUENCY, 4000.0);
+ 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;
+ }
+
+ // Soft saturation. This used to blow up the filter!
+ private static double cubicPolynomial(double x) {
+ return x - (x * x * x * SATURATION_COEFFICIENT);
+ }
+
+ private static double clip(double x) {
+ if (x > SATURATION_UPPER_INPUT) {
+ return SATURATION_UPPER_OUTPUT;
+ } else if (x < SATURATION_LOWER_INPUT) {
+ return SATURATION_LOWER_OUTPUT;
+ } else {
+ return cubicPolynomial(x);
+ }
+ }
+
+ public void reset() {
+ x1 = 0.0;
+ x2 = 0.0;
+ x3 = 0.0;
+ x4 = 0.0;
+ y1 = 0.0;
+ y2 = 0.0;
+ y3 = 0.0;
+ y4 = 0.0;
+
+ previousFrequency = 0.0;
+ previousQ = 0.0;
+ f = 0.0;
+ fTo4th = 0.0;
+ feedback = 0.0;
+ }
+}
diff --git a/src/main/java/com/jsyn/unitgen/FilterHighPass.java b/src/main/java/com/jsyn/unitgen/FilterHighPass.java
new file mode 100644
index 0000000..76ad6b9
--- /dev/null
+++ b/src/main/java/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
+ * Tolentino.
+ */
+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/main/java/com/jsyn/unitgen/FilterHighShelf.java b/src/main/java/com/jsyn/unitgen/FilterHighShelf.java
new file mode 100644
index 0000000..449090a
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/FilterLowPass.java b/src/main/java/com/jsyn/unitgen/FilterLowPass.java
new file mode 100644
index 0000000..1557367
--- /dev/null
+++ b/src/main/java/com/jsyn/unitgen/FilterLowPass.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.
+ */
+/**
+ * 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
+ * Tolentino.
+ *
+ * @see FilterFourPoles
+ */
+public class FilterLowPass extends FilterBiquadCommon {
+
+ /**
+ * This method is by FilterBiquad to update coefficients for the 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/main/java/com/jsyn/unitgen/FilterLowShelf.java b/src/main/java/com/jsyn/unitgen/FilterLowShelf.java
new file mode 100644
index 0000000..cf41f45
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/FilterOnePole.java b/src/main/java/com/jsyn/unitgen/FilterOnePole.java
new file mode 100644
index 0000000..090e42b
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/FilterOnePoleOneZero.java b/src/main/java/com/jsyn/unitgen/FilterOnePoleOneZero.java
new file mode 100644
index 0000000..ed1868c
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/FilterOneZero.java b/src/main/java/com/jsyn/unitgen/FilterOneZero.java
new file mode 100644
index 0000000..2a07a16
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/FilterPeakingEQ.java b/src/main/java/com/jsyn/unitgen/FilterPeakingEQ.java
new file mode 100644
index 0000000..bec7096
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/FilterStateVariable.java b/src/main/java/com/jsyn/unitgen/FilterStateVariable.java
new file mode 100644
index 0000000..3a0f05c
--- /dev/null
+++ b/src/main/java/com/jsyn/unitgen/FilterStateVariable.java
@@ -0,0 +1,120 @@
+/*
+ * 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
+ * @see FilterFourPoles
+ */
+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 Multiply unit 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;
+ // LOGGER.debug("low = " + lowPassValue + ", band = " + bandPassValue +
+ // ", high = " + highPassValue );
+ highs[i] = highPassValue;
+
+ bandPassValue = (freqInternal * highPassValue) + bandPassValue;
+ bands[i] = bandPassValue;
+ // LOGGER.debug("low = " + lowPassValue + ", band = " + bandPassValue +
+ // ", high = " + highPassValue );
+ }
+ }
+
+}
diff --git a/src/main/java/com/jsyn/unitgen/FilterTwoPoles.java b/src/main/java/com/jsyn/unitgen/FilterTwoPoles.java
new file mode 100644
index 0000000..0c68a64
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/FilterTwoPolesTwoZeros.java b/src/main/java/com/jsyn/unitgen/FilterTwoPolesTwoZeros.java
new file mode 100644
index 0000000..cde279f
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/FixedRateMonoReader.java b/src/main/java/com/jsyn/unitgen/FixedRateMonoReader.java
new file mode 100644
index 0000000..c6edc23
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/FixedRateMonoWriter.java b/src/main/java/com/jsyn/unitgen/FixedRateMonoWriter.java
new file mode 100644
index 0000000..c215c55
--- /dev/null
+++ b/src/main/java/com/jsyn/unitgen/FixedRateMonoWriter.java
@@ -0,0 +1,54 @@
+/*
+ * 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.
+ *
+ * Note that you must call start() on this unit because it does not have an output for pulling data.
+ *
+ * @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/main/java/com/jsyn/unitgen/FixedRateStereoReader.java b/src/main/java/com/jsyn/unitgen/FixedRateStereoReader.java
new file mode 100644
index 0000000..6dcf72c
--- /dev/null
+++ b/src/main/java/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);
+ // LOGGER.debug("SampleReader_16F2: left = " + fdata );
+ double amp = amplitudes[i];
+ output0s[i] = fdata * amp;
+ fdata = dataQueue.readCurrentChannelDouble(1);
+ // LOGGER.debug("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/main/java/com/jsyn/unitgen/FixedRateStereoWriter.java b/src/main/java/com/jsyn/unitgen/FixedRateStereoWriter.java
new file mode 100644
index 0000000..e4502f9
--- /dev/null
+++ b/src/main/java/com/jsyn/unitgen/FixedRateStereoWriter.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.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.
+ *
+ * Note that you must call start() on this unit because it does not have an output for pulling data.
+ *
+ * @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/main/java/com/jsyn/unitgen/FourWayFade.java b/src/main/java/com/jsyn/unitgen/FourWayFade.java
new file mode 100644
index 0000000..8f88965
--- /dev/null
+++ b/src/main/java/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]-----&gt;
+ *
+ * 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/main/java/com/jsyn/unitgen/FunctionEvaluator.java b/src/main/java/com/jsyn/unitgen/FunctionEvaluator.java
new file mode 100644
index 0000000..0cc0c83
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/FunctionOscillator.java b/src/main/java/com/jsyn/unitgen/FunctionOscillator.java
new file mode 100644
index 0000000..30d32d5
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/Grain.java b/src/main/java/com/jsyn/unitgen/Grain.java
new file mode 100644
index 0000000..812061c
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/GrainCommon.java b/src/main/java/com/jsyn/unitgen/GrainCommon.java
new file mode 100644
index 0000000..a7a04fc
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/GrainEnvelope.java b/src/main/java/com/jsyn/unitgen/GrainEnvelope.java
new file mode 100644
index 0000000..e6ff24c
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/GrainFarm.java b/src/main/java/com/jsyn/unitgen/GrainFarm.java
new file mode 100644
index 0000000..78179bc
--- /dev/null
+++ b/src/main/java/com/jsyn/unitgen/GrainFarm.java
@@ -0,0 +1,178 @@
+/*
+ * 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.
+ *
+ * <pre><code>
+ synth.add( sampleGrainFarm = new GrainFarm() );
+ grainFarm.allocate( NUM_GRAINS );
+</code></pre>
+ *
+ * @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 final 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/main/java/com/jsyn/unitgen/GrainScheduler.java b/src/main/java/com/jsyn/unitgen/GrainScheduler.java
new file mode 100644
index 0000000..df9c25e
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/GrainSource.java b/src/main/java/com/jsyn/unitgen/GrainSource.java
new file mode 100644
index 0000000..1d5c522
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/GrainSourceSine.java b/src/main/java/com/jsyn/unitgen/GrainSourceSine.java
new file mode 100644
index 0000000..0af9cbd
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/IFFT.java b/src/main/java/com/jsyn/unitgen/IFFT.java
new file mode 100644
index 0000000..307acd2
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/ImpulseOscillator.java b/src/main/java/com/jsyn/unitgen/ImpulseOscillator.java
new file mode 100644
index 0000000..8c676f3
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/ImpulseOscillatorBL.java b/src/main/java/com/jsyn/unitgen/ImpulseOscillatorBL.java
new file mode 100644
index 0000000..23686b8
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/Integrate.java b/src/main/java/com/jsyn/unitgen/Integrate.java
new file mode 100644
index 0000000..50831d2
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/InterpolatingDelay.java b/src/main/java/com/jsyn/unitgen/InterpolatingDelay.java
new file mode 100644
index 0000000..24de4f9
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/Latch.java b/src/main/java/com/jsyn/unitgen/Latch.java
new file mode 100644
index 0000000..0518f69
--- /dev/null
+++ b/src/main/java/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 &gt; 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/main/java/com/jsyn/unitgen/LatchZeroCrossing.java b/src/main/java/com/jsyn/unitgen/LatchZeroCrossing.java
new file mode 100644
index 0000000..9e6c011
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/LineIn.java b/src/main/java/com/jsyn/unitgen/LineIn.java
new file mode 100644
index 0000000..aeef965
--- /dev/null
+++ b/src/main/java/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 &gt; 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/main/java/com/jsyn/unitgen/LineOut.java b/src/main/java/com/jsyn/unitgen/LineOut.java
new file mode 100644
index 0000000..29c8ce7
--- /dev/null
+++ b/src/main/java/com/jsyn/unitgen/LineOut.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;
+
+/**
+ * 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];
+ }
+ }
+
+ /**
+ * This unit won't do anything unless you start() it.
+ */
+ @Override
+ public boolean isStartRequired() {
+ return true;
+ }
+
+ @Override
+ public UnitInputPort getInput() {
+ return input;
+ }
+}
diff --git a/src/main/java/com/jsyn/unitgen/LinearRamp.java b/src/main/java/com/jsyn/unitgen/LinearRamp.java
new file mode 100644
index 0000000..cad53d5
--- /dev/null
+++ b/src/main/java/com/jsyn/unitgen/LinearRamp.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.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 currentValue = current.getValue();
+
+ // If input has changed, start new segment.
+ // Equality check is OK because we set them exactly equal below.
+ if (currentInput != target)
+ {
+ source = currentValue;
+ phase = 0.0;
+ target = currentInput;
+ }
+
+ if (currentValue == target) {
+ // at end of ramp
+ for (int i = start; i < limit; i++) {
+ outputs[i] = currentValue;
+ }
+ } else {
+ // in middle of ramp
+ double currentTime = time.getValues()[0];
+ // Has time changed?
+ if (currentTime != timeHeld) {
+ rate = convertTimeToRate(currentTime);
+ timeHeld = currentTime;
+ }
+
+ 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/main/java/com/jsyn/unitgen/Maximum.java b/src/main/java/com/jsyn/unitgen/Maximum.java
new file mode 100644
index 0000000..296e5da
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/Minimum.java b/src/main/java/com/jsyn/unitgen/Minimum.java
new file mode 100644
index 0000000..046387e
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/MixerMono.java b/src/main/java/com/jsyn/unitgen/MixerMono.java
new file mode 100644
index 0000000..f4c7d7d
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/MixerMonoRamped.java b/src/main/java/com/jsyn/unitgen/MixerMonoRamped.java
new file mode 100644
index 0000000..30f5342
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/MixerStereo.java b/src/main/java/com/jsyn/unitgen/MixerStereo.java
new file mode 100644
index 0000000..218546e
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/MixerStereoRamped.java b/src/main/java/com/jsyn/unitgen/MixerStereoRamped.java
new file mode 100644
index 0000000..6f3bfcc
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/MonoStreamWriter.java b/src/main/java/com/jsyn/unitgen/MonoStreamWriter.java
new file mode 100644
index 0000000..0fb6f40
--- /dev/null
+++ b/src/main/java/com/jsyn/unitgen/MonoStreamWriter.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 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.
+ *
+ * Note that you must call start() on this unit because it does not have an output for pulling data.
+ *
+ * @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 ignored) {
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/com/jsyn/unitgen/MorphingOscillatorBL.java b/src/main/java/com/jsyn/unitgen/MorphingOscillatorBL.java
new file mode 100644
index 0000000..7ca440d
--- /dev/null
+++ b/src/main/java/com/jsyn/unitgen/MorphingOscillatorBL.java
@@ -0,0 +1,72 @@
+/*
+ * 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;
+
+/**
+ * Oscillator that can change its shape from sine to sawtooth to pulse.
+ *
+ * @author Phil Burk (C) 2016 Mobileer Inc
+ */
+public class MorphingOscillatorBL extends PulseOscillatorBL {
+ /**
+ * Controls the shape of the waveform.
+ * The shape varies continuously from a sine wave at -1.0,
+ * to a sawtooth at 0.0 to a pulse wave at 1.0.
+ */
+ public UnitInputPort shape;
+
+ public MorphingOscillatorBL() {
+ addPort(shape = new UnitInputPort("Shape"));
+ shape.setMinimum(-1.0);
+ shape.setMaximum(1.0);
+ }
+
+ @Override
+ protected double generateBL(MultiTable multiTable, double currentPhase,
+ double positivePhaseIncrement, double flevel, int i) {
+ double[] shapes = shape.getValues();
+ double shape = shapes[i];
+
+ if (shape < 0.0) {
+ // Squeeze flevel towards the pure sine table.
+ flevel += flevel * shape;
+ return multiTable.calculateSawtooth(currentPhase, positivePhaseIncrement, flevel);
+ } else {
+ 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) * shape)); // apply shape morphing
+ }
+ }
+}
diff --git a/src/main/java/com/jsyn/unitgen/MultiPassThrough.java b/src/main/java/com/jsyn/unitgen/MultiPassThrough.java
new file mode 100644
index 0000000..9125fc3
--- /dev/null
+++ b/src/main/java/com/jsyn/unitgen/MultiPassThrough.java
@@ -0,0 +1,70 @@
+/*
+ * 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;
+
+/**
+ * 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.
+ *
+ * This is just like PassThrough except the input and output ports have multiple parts.
+ * The default is two parts, ie. stereo.
+ *
+ * @author Phil Burk (C) 2016 Mobileer Inc
+ * @see Circuit
+ * @see PassThrough
+ */
+public class MultiPassThrough extends UnitGenerator implements UnitSink, UnitSource {
+ public UnitInputPort input;
+ public UnitOutputPort output;
+ private final int mNumParts;
+
+ /* Define Unit Ports used by connect() and set(). */
+ public MultiPassThrough(int numParts) {
+ mNumParts = numParts;
+ addPort(input = new UnitInputPort(numParts, "Input"));
+ addPort(output = new UnitOutputPort(numParts, "Output"));
+ }
+
+ public MultiPassThrough() {
+ this(2); // stereo
+ }
+
+ @Override
+ public UnitInputPort getInput() {
+ return input;
+ }
+
+ @Override
+ public UnitOutputPort getOutput() {
+ return output;
+ }
+
+ @Override
+ public void generate(int start, int limit) {
+ for (int partIndex = 0; partIndex < mNumParts; partIndex++) {
+ double[] inputs = input.getValues(partIndex);
+ double[] outputs = output.getValues(partIndex);
+
+ for (int i = start; i < limit; i++) {
+ outputs[i] = inputs[i];
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/jsyn/unitgen/Multiply.java b/src/main/java/com/jsyn/unitgen/Multiply.java
new file mode 100644
index 0000000..ded7646
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/MultiplyAdd.java b/src/main/java/com/jsyn/unitgen/MultiplyAdd.java
new file mode 100644
index 0000000..adbee6c
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/Pan.java b/src/main/java/com/jsyn/unitgen/Pan.java
new file mode 100644
index 0000000..bc90984
--- /dev/null
+++ b/src/main/java/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 Select
+ */
+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/main/java/com/jsyn/unitgen/PanControl.java b/src/main/java/com/jsyn/unitgen/PanControl.java
new file mode 100644
index 0000000..63bddd8
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/ParabolicEnvelope.java b/src/main/java/com/jsyn/unitgen/ParabolicEnvelope.java
new file mode 100644
index 0000000..6de97d9
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/PassThrough.java b/src/main/java/com/jsyn/unitgen/PassThrough.java
new file mode 100644
index 0000000..8ac0b93
--- /dev/null
+++ b/src/main/java/com/jsyn/unitgen/PassThrough.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.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
+ * @see MultiPassThrough
+ */
+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/main/java/com/jsyn/unitgen/PeakFollower.java b/src/main/java/com/jsyn/unitgen/PeakFollower.java
new file mode 100644
index 0000000..7bf0508
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/PhaseShifter.java b/src/main/java/com/jsyn/unitgen/PhaseShifter.java
new file mode 100644
index 0000000..4b17245
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/PinkNoise.java b/src/main/java/com/jsyn/unitgen/PinkNoise.java
new file mode 100644
index 0000000..84aa2f2
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/PitchDetector.java b/src/main/java/com/jsyn/unitgen/PitchDetector.java
new file mode 100644
index 0000000..ff44c93
--- /dev/null
+++ b/src/main/java/com/jsyn/unitgen/PitchDetector.java
@@ -0,0 +1,120 @@
+/*
+ * 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;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * 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 {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(PitchDetector.class);
+
+ 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 TODO update for 48000
+ 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) {
+ LOGGER.debug("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/main/java/com/jsyn/unitgen/PitchToFrequency.java b/src/main/java/com/jsyn/unitgen/PitchToFrequency.java
new file mode 100644
index 0000000..9086749
--- /dev/null
+++ b/src/main/java/com/jsyn/unitgen/PitchToFrequency.java
@@ -0,0 +1,26 @@
+package com.jsyn.unitgen;
+
+import com.softsynth.math.AudioMath;
+
+public class PitchToFrequency extends PowerOfTwo {
+
+ public PitchToFrequency() {
+ input.setup(0.0, 60.0, 127.0);
+ }
+
+ /**
+ * Convert from MIDI pitch to an octave offset from Concert A.
+ */
+ @Override
+ public double adjustInput(double in) {
+ return (in - AudioMath.CONCERT_A_PITCH) * (1.0/12.0);
+ }
+
+ /**
+ * Convert scaler to a frequency relative to Concert A.
+ */
+ @Override
+ public double adjustOutput(double out) {
+ return out * AudioMath.getConcertAFrequency();
+ }
+}
diff --git a/src/main/java/com/jsyn/unitgen/PowerOfTwo.java b/src/main/java/com/jsyn/unitgen/PowerOfTwo.java
new file mode 100644
index 0000000..5916860
--- /dev/null
+++ b/src/main/java/com/jsyn/unitgen/PowerOfTwo.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.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 {
+ /**
+ * Offset in octaves.
+ */
+ public UnitInputPort input;
+ public UnitOutputPort output;
+
+ private static double[] table;
+ private static final int NUM_VALUES = 2048;
+ // 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();
+
+ 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 {
+ lastInput = in;
+ double adjustedInput = adjustInput(in);
+ int octave = (int) Math.floor(adjustedInput);
+ double normal = adjustedInput - 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;
+ }
+ double adjustedOutput = adjustOutput(value);
+ outputs[i] = adjustedOutput;
+ lastOutput = adjustedOutput;
+ }
+ }
+ }
+
+ public double adjustInput(double in) {
+ return in;
+ }
+
+ public double adjustOutput(double out) {
+ return out;
+ }
+}
diff --git a/src/main/java/com/jsyn/unitgen/PulseOscillator.java b/src/main/java/com/jsyn/unitgen/PulseOscillator.java
new file mode 100644
index 0000000..5ac7352
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/PulseOscillatorBL.java b/src/main/java/com/jsyn/unitgen/PulseOscillatorBL.java
new file mode 100644
index 0000000..c0e234c
--- /dev/null
+++ b/src/main/java/com/jsyn/unitgen/PulseOscillatorBL.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.unitgen;
+
+import com.jsyn.engine.MultiTable;
+import com.jsyn.ports.UnitInputPort;
+
+/**
+ * Pulse oscillator that uses two band limited sawtooth waveforms.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ */
+public class PulseOscillatorBL extends SawtoothOscillatorBL {
+ /** Controls the duty cycle of the pulse waveform.
+ * The width varies from -1.0 to +1.0.
+ * When width is zero the output is a square wave.
+ */
+ 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 and width. little less than half at
+ * Nyquist/2.0!
+ */
+ double scale = 1.0 - positivePhaseIncrement;
+ return scale * (val1 - val2 - width);
+ }
+}
diff --git a/src/main/java/com/jsyn/unitgen/RaisedCosineEnvelope.java b/src/main/java/com/jsyn/unitgen/RaisedCosineEnvelope.java
new file mode 100644
index 0000000..c32417c
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/RangeConverter.java b/src/main/java/com/jsyn/unitgen/RangeConverter.java
new file mode 100644
index 0000000..ae94b0f
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/RectangularWindow.java b/src/main/java/com/jsyn/unitgen/RectangularWindow.java
new file mode 100644
index 0000000..d61f763
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/RedNoise.java b/src/main/java/com/jsyn/unitgen/RedNoise.java
new file mode 100644
index 0000000..d3e4321
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/SampleGrainFarm.java b/src/main/java/com/jsyn/unitgen/SampleGrainFarm.java
new file mode 100644
index 0000000..3f908d6
--- /dev/null
+++ b/src/main/java/com/jsyn/unitgen/SampleGrainFarm.java
@@ -0,0 +1,71 @@
+/*
+ * 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.
+ *
+ * <pre><code>
+ 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);
+</code></pre>
+ *
+ * @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/main/java/com/jsyn/unitgen/SampleGrainSource.java b/src/main/java/com/jsyn/unitgen/SampleGrainSource.java
new file mode 100644
index 0000000..f33817f
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/SawtoothOscillator.java b/src/main/java/com/jsyn/unitgen/SawtoothOscillator.java
new file mode 100644
index 0000000..1b3dead
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/SawtoothOscillatorBL.java b/src/main/java/com/jsyn/unitgen/SawtoothOscillatorBL.java
new file mode 100644
index 0000000..8b58f6c
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/SawtoothOscillatorDPW.java b/src/main/java/com/jsyn/unitgen/SawtoothOscillatorDPW.java
new file mode 100644
index 0000000..27d0c5a
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/SchmidtTrigger.java b/src/main/java/com/jsyn/unitgen/SchmidtTrigger.java
new file mode 100644
index 0000000..64129ff
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/Select.java b/src/main/java/com/jsyn/unitgen/Select.java
new file mode 100644
index 0000000..6d8792e
--- /dev/null
+++ b/src/main/java/com/jsyn/unitgen/Select.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;
+
+/**
+ * SelectUnit unit. Select InputA or InputB based on value on Select port.
+ *
+ *<pre> <code>
+ output = ( select &gt; 0.0 ) ? inputB : inputA;
+</code> </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/main/java/com/jsyn/unitgen/SequentialDataReader.java b/src/main/java/com/jsyn/unitgen/SequentialDataReader.java
new file mode 100644
index 0000000..901767b
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/SequentialDataWriter.java b/src/main/java/com/jsyn/unitgen/SequentialDataWriter.java
new file mode 100644
index 0000000..cb3bb11
--- /dev/null
+++ b/src/main/java/com/jsyn/unitgen/SequentialDataWriter.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.
+ */
+
+package com.jsyn.unitgen;
+
+import com.jsyn.ports.UnitDataQueuePort;
+import com.jsyn.ports.UnitInputPort;
+
+/**
+ * Base class for writing to a sample.
+ *
+ * Note that you must call start() on subclasses of this unit because it does not have an output for pulling data.
+ *
+ * @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"));
+ }
+
+ /**
+ * This unit won't do anything unless you start() it.
+ */
+ @Override
+ public boolean isStartRequired() {
+ return true;
+ }
+}
diff --git a/src/main/java/com/jsyn/unitgen/SineOscillator.java b/src/main/java/com/jsyn/unitgen/SineOscillator.java
new file mode 100644
index 0000000..8112e46
--- /dev/null
+++ b/src/main/java/com/jsyn/unitgen/SineOscillator.java
@@ -0,0 +1,84 @@
+/*
+ * 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 */
+ return x
+ * (x2 * (x2 * (x2 * (x2 * ((x2 * (-IF11)) + IF9) - IF7) + IF5) - IF3) + 1);
+ }
+}
diff --git a/src/main/java/com/jsyn/unitgen/SineOscillatorPhaseModulated.java b/src/main/java/com/jsyn/unitgen/SineOscillatorPhaseModulated.java
new file mode 100644
index 0000000..7631dff
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/SpectralFFT.java b/src/main/java/com/jsyn/unitgen/SpectralFFT.java
new file mode 100644
index 0000000..f3e881a
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/SpectralFilter.java b/src/main/java/com/jsyn/unitgen/SpectralFilter.java
new file mode 100644
index 0000000..758c8e7
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/SpectralIFFT.java b/src/main/java/com/jsyn/unitgen/SpectralIFFT.java
new file mode 100644
index 0000000..c040e52
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/SpectralProcessor.java b/src/main/java/com/jsyn/unitgen/SpectralProcessor.java
new file mode 100644
index 0000000..de96877
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/SquareOscillator.java b/src/main/java/com/jsyn/unitgen/SquareOscillator.java
new file mode 100644
index 0000000..aaca2d0
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/SquareOscillatorBL.java b/src/main/java/com/jsyn/unitgen/SquareOscillatorBL.java
new file mode 100644
index 0000000..cb9e141
--- /dev/null
+++ b/src/main/java/com/jsyn/unitgen/SquareOscillatorBL.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 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/main/java/com/jsyn/unitgen/StereoStreamWriter.java b/src/main/java/com/jsyn/unitgen/StereoStreamWriter.java
new file mode 100644
index 0000000..b387836
--- /dev/null
+++ b/src/main/java/com/jsyn/unitgen/StereoStreamWriter.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;
+
+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.
+ *
+ * Note that you must call start() on this unit because it does not have an output for pulling data.
+ *
+ * @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/main/java/com/jsyn/unitgen/StochasticGrainScheduler.java b/src/main/java/com/jsyn/unitgen/StochasticGrainScheduler.java
new file mode 100644
index 0000000..1f79877
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/Subtract.java b/src/main/java/com/jsyn/unitgen/Subtract.java
new file mode 100644
index 0000000..d9ca035
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/TriangleOscillator.java b/src/main/java/com/jsyn/unitgen/TriangleOscillator.java
new file mode 100644
index 0000000..ada2d6e
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/TunableFilter.java b/src/main/java/com/jsyn/unitgen/TunableFilter.java
new file mode 100644
index 0000000..d2c9f66
--- /dev/null
+++ b/src/main/java/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
+ * Tolentino.
+ */
+public abstract class TunableFilter extends UnitFilter {
+
+ 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/main/java/com/jsyn/unitgen/TwoInDualOut.java b/src/main/java/com/jsyn/unitgen/TwoInDualOut.java
new file mode 100644
index 0000000..a8fea48
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/UnitBinaryOperator.java b/src/main/java/com/jsyn/unitgen/UnitBinaryOperator.java
new file mode 100644
index 0000000..c5675ff
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/UnitFilter.java b/src/main/java/com/jsyn/unitgen/UnitFilter.java
new file mode 100644
index 0000000..49976ba
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/UnitGate.java b/src/main/java/com/jsyn/unitgen/UnitGate.java
new file mode 100644
index 0000000..59144c2
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/UnitGenerator.java b/src/main/java/com/jsyn/unitgen/UnitGenerator.java
new file mode 100644
index 0000000..7aacaf1
--- /dev/null
+++ b/src/main/java/com/jsyn/unitgen/UnitGenerator.java
@@ -0,0 +1,357 @@
+/*
+ * 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 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;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Base class for all unit generators.
+ *
+ * @author Phil Burk (C) 2009 Mobileer Inc
+ */
+public abstract class UnitGenerator {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(UnitGenerator.class);
+
+ protected static final double VERY_SMALL_FLOAT = 1.0e-26;
+
+ // Some common port names.
+ public static final String PORT_NAME_INPUT = "Input";
+ public static final String PORT_NAME_OUTPUT = "Output";
+ public static final String PORT_NAME_PHASE = "Phase";
+ public static final String PORT_NAME_FREQUENCY = "Frequency";
+ public static final String PORT_NAME_FREQUENCY_SCALER = "FreqScaler";
+ public static final String PORT_NAME_AMPLITUDE = "Amplitude";
+ public static final String PORT_NAME_PAN = "Pan";
+ public static final String PORT_NAME_TIME = "Time";
+ public static final String PORT_NAME_CUTOFF = "Cutoff";
+ public static final String PORT_NAME_PRESSURE = "Pressure";
+ public static final String PORT_NAME_TIMBRE = "Timbre";
+
+ public static final double FALSE = 0.0;
+ public static final double TRUE = 1.0;
+ protected SynthesisEngine synthesisEngine;
+ private final LinkedHashMap<String, UnitPort> ports = new LinkedHashMap<String, UnitPort>();
+ private Circuit circuit;
+ private long lastFrameCount;
+ private boolean enabled = true;
+ private static int nextId;
+ private final int id = nextId++;
+
+ 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);
+ }
+
+ /**
+ * Case-insensitive search for a port by name.
+ * @param portName
+ * @return matching port or null
+ */
+ 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.debug("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 {
+ // Oddly 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#setupAutoDisable(UnitGenerator)
+ * @see #start()
+ */
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ if (!enabled) {
+ flattenOutputs();
+ }
+ }
+
+ /**
+ * Some units, for example LineOut and FixedRateMonoWriter, will only work if
+ * started explicitly. Other units will run when downstream units are started.
+ *
+ * @return true if you should call start() for this unit
+ */
+ public boolean isStartRequired() {
+ return false;
+ }
+
+ /**
+ * 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(boolean)
+ */
+ 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);
+ }
+
+ /**
+ * @deprecated ignored, frameRate comes from the SynthesisEngine
+ * @param rate
+ */
+ @Deprecated
+ public void setFrameRate(int 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);
+ // LOGGER.debug("setPort " + port );
+ if (port == null) {
+ LOGGER.warn("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/main/java/com/jsyn/unitgen/UnitOscillator.java b/src/main/java/com/jsyn/unitgen/UnitOscillator.java
new file mode 100644
index 0000000..5d4c6fa
--- /dev/null
+++ b/src/main/java/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 = 1.0;
+
+ /* Define Unit Ports used by connect() and set(). */
+ public UnitOscillator() {
+ addPort(frequency = new UnitInputPort(PORT_NAME_FREQUENCY));
+ frequency.setup(40.0, DEFAULT_FREQUENCY, 8000.0);
+ addPort(amplitude = new UnitInputPort(PORT_NAME_AMPLITUDE, DEFAULT_AMPLITUDE));
+ addPort(phase = new UnitVariablePort(PORT_NAME_PHASE));
+ addPort(output = new UnitOutputPort(PORT_NAME_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/main/java/com/jsyn/unitgen/UnitSink.java b/src/main/java/com/jsyn/unitgen/UnitSink.java
new file mode 100644
index 0000000..3e0f55e
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/UnitSource.java b/src/main/java/com/jsyn/unitgen/UnitSource.java
new file mode 100644
index 0000000..5ee8134
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/UnitStreamWriter.java b/src/main/java/com/jsyn/unitgen/UnitStreamWriter.java
new file mode 100644
index 0000000..0c5bd8b
--- /dev/null
+++ b/src/main/java/com/jsyn/unitgen/UnitStreamWriter.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;
+
+import com.jsyn.io.AudioOutputStream;
+import com.jsyn.ports.UnitInputPort;
+
+/**
+ * Base class for writing to an AudioOutputStream.
+ *
+ * Note that you must call start() on subclasses of this unit because it does not have an output for pulling data.
+ *
+ * @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;
+ }
+
+ /**
+ * This unit won't do anything unless you start() it.
+ */
+ @Override
+ public boolean isStartRequired() {
+ return true;
+ }
+
+ @Override
+ public UnitInputPort getInput() {
+ return input;
+ }
+}
diff --git a/src/main/java/com/jsyn/unitgen/UnitVoice.java b/src/main/java/com/jsyn/unitgen/UnitVoice.java
new file mode 100644
index 0000000..3f5e6ef
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/Unzipper.java b/src/main/java/com/jsyn/unitgen/Unzipper.java
new file mode 100644
index 0000000..c776ffb
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/VariableRateDataReader.java b/src/main/java/com/jsyn/unitgen/VariableRateDataReader.java
new file mode 100644
index 0000000..2ef163c
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/VariableRateMonoReader.java b/src/main/java/com/jsyn/unitgen/VariableRateMonoReader.java
new file mode 100644
index 0000000..52b7f1e
--- /dev/null
+++ b/src/main/java/com/jsyn/unitgen/VariableRateMonoReader.java
@@ -0,0 +1,115 @@
+/*
+ * 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}.
+ *
+ * <pre><code>
+ // Queue an envelope to the dataQueue port.
+ ampEnv.dataQueue.queue(ampEnvelope);
+</code></pre>
+ *
+ * @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/main/java/com/jsyn/unitgen/VariableRateStereoReader.java b/src/main/java/com/jsyn/unitgen/VariableRateStereoReader.java
new file mode 100644
index 0000000..0f9fce8
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/WhiteNoise.java b/src/main/java/com/jsyn/unitgen/WhiteNoise.java
new file mode 100644
index 0000000..b708e92
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/unitgen/ZeroCrossingCounter.java b/src/main/java/com/jsyn/unitgen/ZeroCrossingCounter.java
new file mode 100644
index 0000000..6cd36ea
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/util/AudioSampleLoader.java b/src/main/java/com/jsyn/util/AudioSampleLoader.java
new file mode 100644
index 0000000..b665933
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/util/AudioStreamReader.java b/src/main/java/com/jsyn/util/AudioStreamReader.java
new file mode 100644
index 0000000..5a725c3
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/util/AutoCorrelator.java b/src/main/java/com/jsyn/util/AutoCorrelator.java
new file mode 100644
index 0000000..944d515
--- /dev/null
+++ b/src/main/java/com/jsyn/util/AutoCorrelator.java
@@ -0,0 +1,290 @@
+/*
+ * 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) {
+ rawDeltaScan(last1, splitLast, splitLast, stride);
+ rawDeltaScan(last1 - splitLast, buffer.length - 1, count - splitLast, stride);
+ }
+
+ private void checkDeltaScan(int last1, int last2, int count, int stride) {
+ if (count > last2) {
+ // Use recursion with reverse indexes to handle a double split.
+ checkDeltaScan(last2, last1, last2, stride);
+ checkDeltaScan(buffer.length - 1, last1 - last2, count - last2, 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);
+
+ return (float) (2.0 * sumProducts / sumSquares) * correction;
+ }
+
+ // 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;
+ }
+
+ // Use quadratic fit to return offset between -0.5 and +0.5 from center.
+ protected static double interpolatePeak(double d1, double d2, double d3) {
+ return 0.5 * (d1 - d3) / (d1 - (2.0 * d2) + d3);
+ }
+
+ // 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) {
+ double average = (value + previousSample) * 0.5;
+ previousSample = value;
+
+ cursor += 1;
+ if (cursor == buffer.length) {
+ cursor = 0;
+ bufferValid = true;
+ }
+ buffer[cursor] = (float) average;
+
+ return incrementalAnalysis();
+ }
+
+ @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/main/java/com/jsyn/util/Instrument.java b/src/main/java/com/jsyn/util/Instrument.java
new file mode 100644
index 0000000..8a53304
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/util/InstrumentLibrary.java b/src/main/java/com/jsyn/util/InstrumentLibrary.java
new file mode 100644
index 0000000..65113dc
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/util/JavaSoundSampleLoader.java b/src/main/java/com/jsyn/util/JavaSoundSampleLoader.java
new file mode 100644
index 0000000..56a654e
--- /dev/null
+++ b/src/main/java/com/jsyn/util/JavaSoundSampleLoader.java
@@ -0,0 +1,149 @@
+/*
+ * 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();
+ 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/main/java/com/jsyn/util/JavaTools.java b/src/main/java/com/jsyn/util/JavaTools.java
new file mode 100644
index 0000000..570e4c4
--- /dev/null
+++ b/src/main/java/com/jsyn/util/JavaTools.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.util;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class JavaTools {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(JavaTools.class);
+
+ @SuppressWarnings("rawtypes")
+ public static Class loadClass(String className, boolean verbose) {
+ Class newClass = null;
+ try {
+ newClass = Class.forName(className);
+ } catch (Throwable e) {
+ if (verbose)
+ LOGGER.debug("Caught " + e);
+ }
+ if (newClass == null) {
+ try {
+ ClassLoader systemLoader = ClassLoader.getSystemClassLoader();
+ newClass = Class.forName(className, true, systemLoader);
+ } catch (Throwable e) {
+ if (verbose)
+ LOGGER.debug("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/main/java/com/jsyn/util/MultiChannelSynthesizer.java b/src/main/java/com/jsyn/util/MultiChannelSynthesizer.java
new file mode 100644
index 0000000..da7f6c7
--- /dev/null
+++ b/src/main/java/com/jsyn/util/MultiChannelSynthesizer.java
@@ -0,0 +1,404 @@
+/*
+ * Copyright 2016 Phil Burk, Mobileer Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.engine.SynthesisEngine;
+import com.jsyn.midi.MidiConstants;
+import com.jsyn.ports.UnitInputPort;
+import com.jsyn.ports.UnitOutputPort;
+import com.jsyn.unitgen.ExponentialRamp;
+import com.jsyn.unitgen.LinearRamp;
+import com.jsyn.unitgen.Multiply;
+import com.jsyn.unitgen.Pan;
+import com.jsyn.unitgen.PowerOfTwo;
+import com.jsyn.unitgen.SineOscillator;
+import com.jsyn.unitgen.TwoInDualOut;
+import com.jsyn.unitgen.UnitGenerator;
+import com.jsyn.unitgen.UnitOscillator;
+import com.jsyn.unitgen.UnitVoice;
+import com.softsynth.math.AudioMath;
+import com.softsynth.shared.time.TimeStamp;
+
+/**
+ * General purpose synthesizer with "channels"
+ * that could be used to implement a MIDI synthesizer.
+ *
+ * Each channel has:
+ * <pre><code>
+ * lfo -&gt; pitchToLinear -&gt; [VOICES] -&gt; volume* -&gt; panner
+ * bend --/
+ * </code></pre>
+ *
+ * Note: this class is experimental and subject to change.
+ *
+ * @author Phil Burk (C) 2016 Mobileer Inc
+ */
+public class MultiChannelSynthesizer {
+ private Synthesizer synth;
+ private TwoInDualOut outputUnit;
+ private ChannelContext[] channels;
+ private final static int MAX_VELOCITY = 127;
+ private double mMasterAmplitude = 0.25;
+
+ private class ChannelGroupContext {
+ private VoiceDescription voiceDescription;
+ private UnitVoice[] voices;
+ private VoiceAllocator allocator;
+
+ ChannelGroupContext(int numVoices, VoiceDescription voiceDescription) {
+ this.voiceDescription = voiceDescription;
+
+ voices = new UnitVoice[numVoices];
+ for (int i = 0; i < numVoices; i++) {
+ UnitVoice voice = voiceDescription.createUnitVoice();
+ UnitGenerator ugen = voice.getUnitGenerator();
+ synth.add(ugen);
+ voices[i] = voice;
+
+ }
+ allocator = new VoiceAllocator(voices);
+ }
+ }
+
+ private class ChannelContext {
+ private UnitOscillator lfo;
+ private PowerOfTwo pitchToLinear;
+ private LinearRamp timbreRamp;
+ private LinearRamp pressureRamp;
+ private ExponentialRamp volumeRamp;
+ private Multiply volumeMultiplier;
+ private Pan panner;
+ private double vibratoRate = 5.0;
+ private double bendRangeOctaves = 2.0 / 12.0;
+ private int presetIndex;
+ private ChannelGroupContext groupContext;
+ VoiceOperation voiceOperation = new VoiceOperation() {
+ @Override
+ public void operate (UnitVoice voice) {
+ voice.usePreset(presetIndex);
+ connectVoice(voice);
+ }
+ };
+
+ void setup(ChannelGroupContext groupContext) {
+ this.groupContext = groupContext;
+ synth.add(pitchToLinear = new PowerOfTwo());
+ synth.add(lfo = new SineOscillator()); // TODO use a MorphingOscillator or switch
+ // between S&H etc.
+ // Use a ramp to smooth out the timbre changes.
+ // This helps reduce pops from changing filter cutoff too abruptly.
+ synth.add(timbreRamp = new LinearRamp());
+ timbreRamp.time.set(0.02);
+ synth.add(pressureRamp = new LinearRamp());
+ pressureRamp.time.set(0.02);
+ synth.add(volumeRamp = new ExponentialRamp());
+ volumeRamp.input.set(1.0);
+ volumeRamp.time.set(0.02);
+ synth.add(volumeMultiplier = new Multiply());
+ synth.add(panner = new Pan());
+
+ pitchToLinear.input.setValueAdded(true); // so we can sum pitch bend
+ lfo.output.connect(pitchToLinear.input);
+ lfo.amplitude.set(0.0);
+ lfo.frequency.set(vibratoRate);
+
+ volumeRamp.output.connect(volumeMultiplier.inputB);
+ volumeMultiplier.output.connect(panner.input);
+ panner.output.connect(0, outputUnit.inputA, 0); // Use MultiPassthrough
+ panner.output.connect(1, outputUnit.inputB, 0);
+ }
+
+ private void connectVoice(UnitVoice voice) {
+ UnitGenerator ugen = voice.getUnitGenerator();
+ // Hook up some channel controllers to standard ports on the voice.
+ UnitInputPort freqMod = (UnitInputPort) ugen
+ .getPortByName(UnitGenerator.PORT_NAME_FREQUENCY_SCALER);
+ if (freqMod != null) {
+ freqMod.disconnectAll();
+ pitchToLinear.output.connect(freqMod);
+ }
+ UnitInputPort timbrePort = (UnitInputPort) ugen
+ .getPortByName(UnitGenerator.PORT_NAME_TIMBRE);
+ if (timbrePort != null) {
+ timbrePort.disconnectAll();
+ timbreRamp.output.connect(timbrePort);
+ timbreRamp.input.setup(timbrePort);
+ }
+ UnitInputPort pressurePort = (UnitInputPort) ugen
+ .getPortByName(UnitGenerator.PORT_NAME_PRESSURE);
+ if (pressurePort != null) {
+ pressurePort.disconnectAll();
+ pressureRamp.output.connect(pressurePort);
+ pressureRamp.input.setup(pressurePort);
+ }
+ voice.getOutput().disconnectAll();
+ voice.getOutput().connect(volumeMultiplier.inputA); // mono mix all the voices
+ }
+
+ void programChange(int program) {
+ int programWrapped = program % groupContext.voiceDescription.getPresetCount();
+ String name = groupContext.voiceDescription.getPresetNames()[programWrapped];
+ //LOGGER.debug("Preset[" + program + "] = " + name);
+ presetIndex = programWrapped;
+ }
+
+ void noteOff(int noteNumber, double amplitude) {
+ groupContext.allocator.noteOff(noteNumber, synth.createTimeStamp());
+ }
+
+ void noteOff(int noteNumber, double amplitude, TimeStamp timeStamp) {
+ groupContext.allocator.noteOff(noteNumber, timeStamp);
+ }
+
+ void noteOn(int noteNumber, double amplitude) {
+ noteOn(noteNumber, amplitude, synth.createTimeStamp());
+ }
+
+ void noteOn(int noteNumber, double amplitude, TimeStamp timeStamp) {
+ double frequency = AudioMath.pitchToFrequency(noteNumber);
+ //LOGGER.debug("noteOn(noteNumber) -> " + frequency + " Hz");
+ groupContext.allocator.noteOn(noteNumber, frequency, amplitude, voiceOperation, timeStamp);
+ }
+
+ public void setPitchBend(double offset) {
+ pitchToLinear.input.set(bendRangeOctaves * offset);
+ }
+
+ public void setBendRange(double semitones) {
+ bendRangeOctaves = semitones / 12.0;
+ }
+
+ public void setVibratoDepth(double semitones) {
+ lfo.amplitude.set(semitones);
+ }
+
+ public void setVolume(double volume) {
+ double min = SynthesisEngine.DB96;
+ double max = 1.0;
+ double ratio = max / min;
+ double value = min * Math.pow(ratio, volume);
+ volumeRamp.input.set(value);
+ }
+
+ public void setPan(double pan) {
+ panner.pan.set(pan);
+ }
+
+ /*
+ * @param timbre normalized 0 to 1
+ */
+ public void setTimbre(double timbre) {
+ double min = timbreRamp.input.getMinimum();
+ double max = timbreRamp.input.getMaximum();
+ double value = min + (timbre * (max - min));
+ timbreRamp.input.set(value);
+ }
+
+ /*
+ * @param pressure normalized 0 to 1
+ */
+ public void setPressure(double pressure) {
+ double min = pressureRamp.input.getMinimum();
+ double max = pressureRamp.input.getMaximum();
+ double ratio = max / min;
+ double value = min * Math.pow(ratio, pressure);
+ pressureRamp.input.set(value);
+ }
+ }
+
+ /**
+ * Construct a synthesizer with a maximum of 16 channels like MIDI.
+ */
+ public MultiChannelSynthesizer() {
+ this(MidiConstants.MAX_CHANNELS);
+ }
+
+
+ public MultiChannelSynthesizer(int maxChannels) {
+ channels = new ChannelContext[maxChannels];
+ for (int i = 0; i < channels.length; i++) {
+ channels[i] = new ChannelContext();
+ }
+ }
+
+ /**
+ * Specify a VoiceDescription to use with multiple channels.
+ *
+ * @param synth
+ * @param startChannel channel index is zero based
+ * @param numChannels
+ * @param voicesPerChannel
+ * @param voiceDescription
+ */
+ public void setup(Synthesizer synth, int startChannel, int numChannels, int voicesPerChannel,
+ VoiceDescription voiceDescription) {
+ this.synth = synth;
+ if (outputUnit == null) {
+ synth.add(outputUnit = new TwoInDualOut());
+ }
+ ChannelGroupContext groupContext = new ChannelGroupContext(voicesPerChannel,
+ voiceDescription);
+ for (int i = 0; i < numChannels; i++) {
+ channels[startChannel + i].setup(groupContext);
+ }
+ }
+
+ public void programChange(int channel, int program) {
+ ChannelContext channelContext = channels[channel];
+ channelContext.programChange(program);
+ }
+
+
+ /**
+ * Turn off a note.
+ * @param channel
+ * @param noteNumber
+ * @param velocity between 0 and 127, will be scaled by masterAmplitude
+ */
+ public void noteOff(int channel, int noteNumber, int velocity) {
+ double amplitude = velocity * (1.0 / MAX_VELOCITY);
+ noteOff(channel, noteNumber, amplitude);
+ }
+
+ /**
+ * Turn off a note.
+ * @param channel
+ * @param noteNumber
+ * @param amplitude between 0 and 1.0, will be scaled by masterAmplitude
+ */
+ public void noteOff(int channel, int noteNumber, double amplitude) {
+ ChannelContext channelContext = channels[channel];
+ channelContext.noteOff(noteNumber, amplitude * mMasterAmplitude);
+ }
+
+ /**
+ * Turn off a note.
+ * @param channel
+ * @param noteNumber
+ * @param amplitude between 0 and 1.0, will be scaled by masterAmplitude
+ */
+ public void noteOff(int channel, int noteNumber, double amplitude, TimeStamp timeStamp) {
+ ChannelContext channelContext = channels[channel];
+ channelContext.noteOff(noteNumber, amplitude * mMasterAmplitude, timeStamp);
+ }
+
+ /**
+ * Turn on a note.
+ * @param channel
+ * @param noteNumber
+ * @param velocity between 0 and 127, will be scaled by masterAmplitude
+ */
+ public void noteOn(int channel, int noteNumber, int velocity) {
+ double amplitude = velocity * (1.0 / MAX_VELOCITY);
+ noteOn(channel, noteNumber, amplitude);
+ }
+
+ /**
+ * Turn on a note.
+ * @param channel
+ * @param noteNumber
+ * @param amplitude between 0 and 1.0, will be scaled by masterAmplitude
+ */
+ public void noteOn(int channel, int noteNumber, double amplitude, TimeStamp timeStamp) {
+ ChannelContext channelContext = channels[channel];
+ channelContext.noteOn(noteNumber, amplitude * mMasterAmplitude, timeStamp);
+ }
+
+ /**
+ * Turn on a note.
+ * @param channel
+ * @param noteNumber
+ * @param amplitude between 0 and 1.0, will be scaled by masterAmplitude
+ */
+ public void noteOn(int channel, int noteNumber, double amplitude) {
+ ChannelContext channelContext = channels[channel];
+ channelContext.noteOn(noteNumber, amplitude * mMasterAmplitude);
+ }
+
+ /**
+ * Set a pitch offset that will be scaled by the range for the channel.
+ *
+ * @param channel
+ * @param offset ranges from -1.0 to +1.0
+ */
+ public void setPitchBend(int channel, double offset) {
+ //LOGGER.debug("setPitchBend[" + channel + "] = " + offset);
+ ChannelContext channelContext = channels[channel];
+ channelContext.setPitchBend(offset);
+ }
+
+ public void setBendRange(int channel, double semitones) {
+ ChannelContext channelContext = channels[channel];
+ channelContext.setBendRange(semitones);
+ }
+
+ public void setPressure(int channel, double pressure) {
+ ChannelContext channelContext = channels[channel];
+ channelContext.setPressure(pressure);
+ }
+
+ public void setVibratoDepth(int channel, double semitones) {
+ ChannelContext channelContext = channels[channel];
+ channelContext.setVibratoDepth(semitones);
+ }
+
+ public void setTimbre(int channel, double timbre) {
+ ChannelContext channelContext = channels[channel];
+ channelContext.setTimbre(timbre);
+ }
+
+ /**
+ * Set volume for entire channel.
+ *
+ * @param channel
+ * @param volume normalized between 0.0 and 1.0
+ */
+ public void setVolume(int channel, double volume) {
+ ChannelContext channelContext = channels[channel];
+ channelContext.setVolume(volume);
+ }
+
+ /**
+ * Pan from left to right.
+ *
+ * @param channel
+ * @param pan ranges from -1.0 to +1.0
+ */
+ public void setPan(int channel, double pan) {
+ ChannelContext channelContext = channels[channel];
+ channelContext.setPan(pan);
+ }
+
+ /**
+ * @return stereo output port
+ */
+ public UnitOutputPort getOutput() {
+ return outputUnit.output;
+ }
+
+ /**
+ * Set amplitude for a single voice when the velocity is 127.
+ * @param masterAmplitude
+ */
+ public void setMasterAmplitude(double masterAmplitude) {
+ mMasterAmplitude = masterAmplitude;
+ }
+ public double getMasterAmplitude() {
+ return mMasterAmplitude;
+ }
+}
diff --git a/src/main/java/com/jsyn/util/NumericOutput.java b/src/main/java/com/jsyn/util/NumericOutput.java
new file mode 100644
index 0000000..e30975f
--- /dev/null
+++ b/src/main/java/com/jsyn/util/NumericOutput.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.jsyn.util;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * 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 {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(NumericOutput.class);
+
+ 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) {
+ // LOGGER.debug("MASK before : ln = " + ln );
+ ln = ln & 0x00000000FFFFFFFFL;
+ // LOGGER.debug("MASK after : ln = " + ln );
+ } else if (ln < 0) {
+ ifNeg = true;
+ ln = -ln;
+ }
+ if (ln == 0) {
+ buf.append('0');
+ } else {
+ // LOGGER.debug(" 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) {
+ LOGGER.debug("Test " + n + ", 0x" + Integer.toHexString(n) + ", %"
+ + Integer.toBinaryString(n));
+ LOGGER.debug(" +,8,t,10 = " + integerToString(n, 8, true, 10));
+ LOGGER.debug(" +,8,f,10 = " + integerToString(n, 8, false, 10));
+ LOGGER.debug(" -,8,t,10 = " + integerToString(-n, 8, true, 10));
+ LOGGER.debug(" -,8,f,10 = " + integerToString(-n, 8, false, 10));
+ LOGGER.debug(" +,8,t,16 = " + integerToString(n, 8, true, 16));
+ LOGGER.debug(" +,8,f,16 = " + integerToString(n, 8, false, 16));
+ LOGGER.debug(" -,8,t,16 = " + integerToString(-n, 8, true, 16));
+ LOGGER.debug(" -,8,f,16 = " + integerToString(-n, 8, false, 16));
+ LOGGER.debug(" +,8,t, 2 = " + integerToString(n, 8, true, 2));
+ LOGGER.debug(" +,8,f, 2 = " + integerToString(n, 8, false, 2));
+ }
+
+ static void testDouble(double value) {
+ LOGGER.debug("Test " + value);
+ LOGGER.debug(" +,5,1 = " + doubleToString(value, 5, 1));
+ LOGGER.debug(" -,5,1 = " + doubleToString(-value, 5, 1));
+
+ LOGGER.debug(" +,14,3 = " + doubleToString(value, 14, 3));
+ LOGGER.debug(" -,14,3 = " + doubleToString(-value, 14, 3));
+
+ LOGGER.debug(" +,6,2,true = " + doubleToString(value, 6, 2, true));
+ LOGGER.debug(" -,6,2,true = " + doubleToString(-value, 6, 2, true));
+ }
+
+ public static void main(String argv[]) {
+ LOGGER.debug("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/main/java/com/jsyn/util/PolyphonicInstrument.java b/src/main/java/com/jsyn/util/PolyphonicInstrument.java
new file mode 100644
index 0000000..08460d0
--- /dev/null
+++ b/src/main/java/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);
+ exportAllInputPorts();
+ }
+
+ /**
+ * 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;
+ }
+
+ @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/main/java/com/jsyn/util/PseudoRandom.java b/src/main/java/com/jsyn/util/PseudoRandom.java
new file mode 100644
index 0000000..e92b669
--- /dev/null
+++ b/src/main/java/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
+ * Tolentino.
+ */
+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;
+ }
+
+ 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() {
+ // Use values for 64-bit sequence from MMIX by Donald Knuth.
+ seed = (seed * 6364136223846793005L) + 1442695040888963407L;
+ return (int) (seed >> 32); // The higher bits have a longer sequence.
+ }
+
+ public int choose(int range) {
+ long positiveInt = nextRandomInteger() & 0x7FFFFFFF;
+ long temp = positiveInt * range;
+ return (int) (temp >> 31);
+ }
+}
diff --git a/src/main/java/com/jsyn/util/RecursiveSequenceGenerator.java b/src/main/java/com/jsyn/util/RecursiveSequenceGenerator.java
new file mode 100644
index 0000000..0d6e451
--- /dev/null
+++ b/src/main/java/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();
+ // LOGGER.debug("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--;
+ // LOGGER.debug("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;
+
+ // LOGGER.debug("nextVal = " + nextVal );
+
+ return playIt ? nextVal : -1;
+ }
+
+ public Random getRandom() {
+ return random;
+ }
+
+ public void setRandom(Random random) {
+ this.random = random;
+ }
+
+}
diff --git a/src/main/java/com/jsyn/util/SampleLoader.java b/src/main/java/com/jsyn/util/SampleLoader.java
new file mode 100644
index 0000000..170b4cb
--- /dev/null
+++ b/src/main/java/com/jsyn/util/SampleLoader.java
@@ -0,0 +1,230 @@
+/*
+ * 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/main/java/com/jsyn/util/SignalCorrelator.java b/src/main/java/com/jsyn/util/SignalCorrelator.java
new file mode 100644
index 0000000..ebdd46b
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/util/StreamingThread.java b/src/main/java/com/jsyn/util/StreamingThread.java
new file mode 100644
index 0000000..682f476
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/util/TransportListener.java b/src/main/java/com/jsyn/util/TransportListener.java
new file mode 100644
index 0000000..3c8b048
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/util/TransportModel.java b/src/main/java/com/jsyn/util/TransportModel.java
new file mode 100644
index 0000000..bcc75be
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/util/VoiceAllocator.java b/src/main/java/com/jsyn/util/VoiceAllocator.java
new file mode 100644
index 0000000..f20f7a5
--- /dev/null
+++ b/src/main/java/com/jsyn/util/VoiceAllocator.java
@@ -0,0 +1,258 @@
+/*
+ * 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 static final int UNASSIGNED_PRESET = -1;
+ private int mPresetIndex = UNASSIGNED_PRESET;
+
+ /**
+ * 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 = UNASSIGNED_PRESET;
+ 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 (voiceTracker.presetIndex != mPresetIndex) {
+ voiceTracker.voice.usePreset(mPresetIndex);
+ voiceTracker.presetIndex = mPresetIndex;
+ }
+ voiceTracker.voice.noteOn(frequency, amplitude, getSynthesizer().createTimeStamp());
+ }
+ });
+ }
+
+ /**
+ * 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.
+ * Apply an operation to the voice.
+ */
+ public void noteOn(final int tag,
+ final double frequency,
+ final double amplitude,
+ final VoiceOperation operation,
+ TimeStamp timeStamp) {
+ getSynthesizer().scheduleCommand(timeStamp, new ScheduledCommand() {
+ @Override
+ public void run() {
+ VoiceTracker voiceTracker = allocateTracker(tag);
+ operation.operate(voiceTracker.voice);
+ 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() {
+ mPresetIndex = presetIndex;
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/com/jsyn/util/VoiceDescription.java b/src/main/java/com/jsyn/util/VoiceDescription.java
new file mode 100644
index 0000000..b7be044
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/util/VoiceOperation.java b/src/main/java/com/jsyn/util/VoiceOperation.java
new file mode 100644
index 0000000..cd3b48e
--- /dev/null
+++ b/src/main/java/com/jsyn/util/VoiceOperation.java
@@ -0,0 +1,7 @@
+package com.jsyn.util;
+
+import com.jsyn.unitgen.UnitVoice;
+
+public interface VoiceOperation {
+ public void operate(UnitVoice voice);
+}
diff --git a/src/main/java/com/jsyn/util/WaveFileWriter.java b/src/main/java/com/jsyn/util/WaveFileWriter.java
new file mode 100644
index 0000000..32e9995
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/util/WaveRecorder.java b/src/main/java/com/jsyn/util/WaveRecorder.java
new file mode 100644
index 0000000..8008d1d
--- /dev/null
+++ b/src/main/java/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 ignored) {
+ }
+ 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/main/java/com/jsyn/util/soundfile/AIFFFileParser.java b/src/main/java/com/jsyn/util/soundfile/AIFFFileParser.java
new file mode 100644
index 0000000..89b443c
--- /dev/null
+++ b/src/main/java/com/jsyn/util/soundfile/AIFFFileParser.java
@@ -0,0 +1,232 @@
+/*
+ * 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;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class AIFFFileParser extends AudioFileParser {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(AIFFFileParser.class);
+
+ 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);
+ // LOGGER.debug( "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 */
+ @SuppressWarnings("unused")
+ 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;
+ // LOGGER.debug("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();
+ // LOGGER.debug( "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) {
+ LOGGER.debug("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) {
+ LOGGER.debug("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/main/java/com/jsyn/util/soundfile/AudioFileParser.java b/src/main/java/com/jsyn/util/soundfile/AudioFileParser.java
new file mode 100644
index 0000000..e7bb066
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/util/soundfile/ChunkHandler.java b/src/main/java/com/jsyn/util/soundfile/ChunkHandler.java
new file mode 100644
index 0000000..6dfe26d
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/util/soundfile/CustomSampleLoader.java b/src/main/java/com/jsyn/util/soundfile/CustomSampleLoader.java
new file mode 100644
index 0000000..14efde9
--- /dev/null
+++ b/src/main/java/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/main/java/com/jsyn/util/soundfile/IFFParser.java b/src/main/java/com/jsyn/util/soundfile/IFFParser.java
new file mode 100644
index 0000000..9bb4ec3
--- /dev/null
+++ b/src/main/java/com/jsyn/util/soundfile/IFFParser.java
@@ -0,0 +1,313 @@
+/*
+ * 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 org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+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 static final Logger LOGGER = LoggerFactory.getLogger(IFFParser.class);
+
+ 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;
+ // LOGGER.debug("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)
+ LOGGER.debug("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) {
+ LOGGER.debug("IFF: parseForm >>>>>>>>>>>>>>>>>> BEGIN");
+ }
+ while (numBytes > 8) {
+ int ckid = readIntBig();
+ int size = readChunkSize();
+ numBytes -= 8;
+ if (debug) {
+ LOGGER.debug("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) {
+ LOGGER.debug("parseForm: numBytes left in form = " + numBytes);
+ }
+ }
+ if (debug) {
+ LOGGER.debug("IFF: parseForm <<<<<<<<<<<<<<<<<<<< END");
+ }
+
+ if (numBytes > 0) {
+ LOGGER.debug("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)
+ LOGGER.debug("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) {
+ LOGGER.debug("parseChunk: endOffset = " + endOffset);
+ LOGGER.debug("parseChunk: numRead = " + numRead);
+ }
+ if ((numBytes & 1) == 1)
+ numBytes++; // even-up
+ if (numRead < numBytes)
+ skip(numBytes - numRead);
+ }
+
+ public void readHead() throws IOException {
+ if (debug)
+ LOGGER.debug("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/main/java/com/jsyn/util/soundfile/WAVEFileParser.java b/src/main/java/com/jsyn/util/soundfile/WAVEFileParser.java
new file mode 100644
index 0000000..a083961
--- /dev/null
+++ b/src/main/java/com/jsyn/util/soundfile/WAVEFileParser.java
@@ -0,0 +1,338 @@
+/*
+ * 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;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+class WAVEFileParser extends AudioFileParser implements ChunkHandler {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(WAVEFileParser.class);
+
+ 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) {
+ LOGGER.debug("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) {
+ LOGGER.debug("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) {
+ LOGGER.debug("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) {
+ LOGGER.debug("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) {
+ LOGGER.debug("WAV: format = 0x" + Integer.toHexString(format));
+ LOGGER.debug("WAV: bitsPerSample = " + bitsPerSample);
+ LOGGER.debug("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) {
+ LOGGER.debug("WAV: extraSize = " + extraSize);
+ LOGGER.debug("WAV: validBitsPerSample = " + validBitsPerSample);
+ LOGGER.debug("WAV: channelMask = " + channelMask);
+ System.out.print("guid = {");
+ for (int i = 0; i < guid.length; i++) {
+ System.out.print(guid[i] + ", ");
+ }
+ LOGGER.debug("}");
+ }
+ 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");
+ }
+ return (samplesPerBlock * byteOffset) / (samplesPerFrame * blockAlign);
+ }
+
+ 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/main/java/com/softsynth/math/AudioMath.java b/src/main/java/com/softsynth/math/AudioMath.java
new file mode 100644
index 0000000..06eb45b
--- /dev/null
+++ b/src/main/java/com/softsynth/math/AudioMath.java
@@ -0,0 +1,82 @@
+/*
+ * 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;
+ private static double mConcertAFrequency = CONCERT_A_FREQUENCY;
+
+ /**
+ * Convert amplitude to decibels. 1.0 is zero dB. 0.5 is -6.02 dB.
+ */
+ public static double amplitudeToDecibels(double amplitude) {
+ return Math.log(amplitude) * a2dScalar;
+ }
+
+ /**
+ * Convert decibels to amplitude. Zero dB is 1.0 and -6.02 dB is 0.5.
+ */
+ public static double decibelsToAmplitude(double decibels) {
+ return Math.pow(10.0, decibels / 20.0);
+ }
+
+ /**
+ * 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 / mConcertAFrequency) / 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 mConcertAFrequency * Math.pow(2.0, ((pitch - CONCERT_A_PITCH) * (1.0 / 12.0)));
+ }
+
+ /**
+ * This can be used to globally adjust the tuning in JSyn from Concert A at 440.0 Hz to
+ * a slightly different frequency. Some orchestras use a higher frequency, eg. 441.0.
+ * This value will be used by pitchToFrequency() and frequencyToPitch().
+ *
+ * @param concertAFrequency
+ */
+ public static void setConcertAFrequency(double concertAFrequency) {
+ mConcertAFrequency = concertAFrequency;
+ }
+
+ public static double getConcertAFrequency() {
+ return mConcertAFrequency;
+ }
+
+ /** Convert a delta value in semitones to a frequency multiplier.
+ * @param semitones
+ * @return scaler For example 2.0 for an input of 12.0 semitones.
+ */
+ public static double semitonesToFrequencyScaler(double semitones) {
+ return Math.pow(2.0, semitones / 12.0);
+ }
+}
diff --git a/src/main/java/com/softsynth/math/ChebyshevPolynomial.java b/src/main/java/com/softsynth/math/ChebyshevPolynomial.java
new file mode 100644
index 0000000..bc0e854
--- /dev/null
+++ b/src/main/java/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/main/java/com/softsynth/math/FourierMath.java b/src/main/java/com/softsynth/math/FourierMath.java
new file mode 100644
index 0000000..d133d7f
--- /dev/null
+++ b/src/main/java/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 = (sign > 0) ? (2.0 / n) : (0.5);
+
+ 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]; // cosine
+ 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 = (sign > 0) ? (2.0f / n) : (0.5f);
+
+ 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]; // cosine
+ 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/main/java/com/softsynth/math/JustRatio.java b/src/main/java/com/softsynth/math/JustRatio.java
new file mode 100644
index 0000000..f4070b4
--- /dev/null
+++ b/src/main/java/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/main/java/com/softsynth/math/Polynomial.java b/src/main/java/com/softsynth/math/Polynomial.java
new file mode 100644
index 0000000..6c6f96a
--- /dev/null
+++ b/src/main/java/com/softsynth/math/Polynomial.java
@@ -0,0 +1,259 @@
+/*
+ * 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 org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+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 static final Logger LOGGER = LoggerFactory.getLogger(Polynomial.class);
+
+ private final Vector terms;
+
+ // TODO: Does this need to exist?
+ static 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();
+ LOGGER.debug("p1=" + p1);
+ Polynomial p2 = new Polynomial(3);
+ LOGGER.debug("p2=" + p2);
+ Polynomial p3 = new Polynomial(2, 3);
+ LOGGER.debug("p3=" + p3);
+ Polynomial p4 = new Polynomial(1, 2, 3);
+ LOGGER.debug("p4=" + p4);
+ LOGGER.debug("p4*5=" + Polynomial.mult(5.0, p4));
+
+ LOGGER.debug("{}", p4.evaluate(10));
+
+ LOGGER.debug("{}", Polynomial.plus(p4, p1));
+ LOGGER.debug("{}", Polynomial.minus(p4, p3));
+ p4.setTerm(12.2, 5);
+ LOGGER.debug("{}", p4);
+ p4.addTerm(0.8, 5);
+ LOGGER.debug("{}", p4);
+ p4.addTerm(0.8, 7);
+ LOGGER.debug("{}", p4);
+ LOGGER.debug("{}", Polynomial.mult(p3, p2));
+ LOGGER.debug("{}", Polynomial.mult(p3, p3));
+ LOGGER.debug("{}", 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);
+ LOGGER.debug(t2 + "\n" + t3 + "\n" + t4);
+ // com.softsynth.jmsl.util
+
+ }
+}
diff --git a/src/main/java/com/softsynth/math/PolynomialTableData.java b/src/main/java/com/softsynth/math/PolynomialTableData.java
new file mode 100644
index 0000000..8110151
--- /dev/null
+++ b/src/main/java/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);
+ // LOGGER.debug("x = " + x + ", p(x) = " + data[i] );
+ }
+
+ }
+
+ public static void main(String[] args) {
+ PolynomialTableData chebData = new PolynomialTableData(ChebyshevPolynomial.T(2), 8);
+ }
+
+}
diff --git a/src/main/java/com/softsynth/math/PrimeFactors.java b/src/main/java/com/softsynth/math/PrimeFactors.java
new file mode 100644
index 0000000..06c0d55
--- /dev/null
+++ b/src/main/java/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.
+ *
+ *
+ * @param n Warning: Do not exceed getPrimeCount()-1.
+ * @return Nth prime number, the 0th prime is 2
+ */
+ public static int getPrime(int n) {
+ return primes[n];
+ }
+
+ /**
+ * @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/main/java/com/softsynth/shared/time/ScheduledCommand.java b/src/main/java/com/softsynth/shared/time/ScheduledCommand.java
new file mode 100644
index 0000000..5b600a7
--- /dev/null
+++ b/src/main/java/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/main/java/com/softsynth/shared/time/ScheduledQueue.java b/src/main/java/com/softsynth/shared/time/ScheduledQueue.java
new file mode 100644
index 0000000..367e4f8
--- /dev/null
+++ b/src/main/java/com/softsynth/shared/time/ScheduledQueue.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.softsynth.shared.time;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+/**
+ * Store objects in time sorted order.
+ */
+public class ScheduledQueue<T> {
+ private final 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 synchronized void clear() {
+ timeNodes.clear();
+ }
+
+ public TimeStamp getNextTime() {
+ return timeNodes.firstKey();
+ }
+
+}
diff --git a/src/main/java/com/softsynth/shared/time/TimeStamp.java b/src/main/java/com/softsynth/shared/time/TimeStamp.java
new file mode 100644
index 0000000..6d243ed
--- /dev/null
+++ b/src/main/java/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 &lt; 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);
+ }
+
+}