001/**
002 * Copyright (c) 2011, The University of Southampton and the individual contributors.
003 * All rights reserved.
004 *
005 * Redistribution and use in source and binary forms, with or without modification,
006 * are permitted provided that the following conditions are met:
007 *
008 *   *  Redistributions of source code must retain the above copyright notice,
009 *      this list of conditions and the following disclaimer.
010 *
011 *   *  Redistributions in binary form must reproduce the above copyright notice,
012 *      this list of conditions and the following disclaimer in the documentation
013 *      and/or other materials provided with the distribution.
014 *
015 *   *  Neither the name of the University of Southampton nor the names of its
016 *      contributors may be used to endorse or promote products derived from this
017 *      software without specific prior written permission.
018 *
019 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
020 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
021 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
022 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
023 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
024 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
025 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
026 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
027 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
028 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
029 */
030/**
031 *
032 */
033package org.openimaj.audio.generation;
034
035import java.util.ArrayList;
036import java.util.List;
037
038import org.openimaj.audio.AudioFormat;
039import org.openimaj.audio.AudioStream;
040import org.openimaj.audio.Instrument;
041import org.openimaj.audio.SampleChunk;
042import org.openimaj.audio.generation.Oscillator.SineOscillator;
043import org.openimaj.audio.samples.SampleBuffer;
044import org.openimaj.audio.util.WesternScaleNote;
045
046/**
047 *      Really really basic synthesizer. Useful for doing tests by running the
048 *      synth as an audio source through filters or whatever.
049 *
050 *  @author David Dupplaw (dpd@ecs.soton.ac.uk)
051 *
052 *      @created 2 May 2012
053 */
054public class Synthesizer extends AudioStream implements Instrument
055{
056        /**
057         *      Interface for options.
058         *
059         *      @author David Dupplaw (dpd@ecs.soton.ac.uk)
060         *  @created 19 Feb 2013
061         *      @version $Author$, $Revision$, $Date$
062         */
063        public static interface OscillatorOptions
064        {
065        }
066
067        /**
068         *      Options class for FM synthesis. Note that the carrier options are set by the
069         *      FM synth and will be overridden.  The modulator options can be controlled
070         *      by the user.
071         *
072         *      @author David Dupplaw (dpd@ecs.soton.ac.uk)
073         *  @created 19 Feb 2013
074         *      @version $Author$, $Revision$, $Date$
075         */
076        public static class FMOptions implements OscillatorOptions
077        {
078                /** The carrier signal */
079                public Synthesizer carrier;
080
081                /** The modulator signal */
082                public Synthesizer modulator;
083
084                /** The modulator amplitude */
085                public double modulatorAmplitude = 1000d / Integer.MAX_VALUE;
086        }
087
088        /** The current time position of the synth */
089        private double currentTime = 0;
090
091        /** This is a cached version of the current time * 1000 as it's used lots of places */
092        private double currentTimeMS = 0;
093
094        /** The oscillator used to generate the wave */
095        private Oscillator oscillator = new SineOscillator();
096
097        /** Default sample chunk length is 1024 bytes */
098        private final int sampleChunkLength = 1024;
099
100        /** Default frequency is the standard A4 (440Hz) tuning pitch */
101        private double frequency = 440;
102
103        /** The gain of the synth */
104        private int gain = Integer.MAX_VALUE;
105
106        /** The time the last note on was detected */
107        private double noteOnTime = 0;
108
109        /** Whether to generate a note or not */
110        private boolean noteOn = true;
111
112        /** Determines whether a note off has been detected. */
113        private boolean noteOff = false;
114
115        /** The time the note off was detected */
116        private double noteOffTime = 0;
117
118        /** Envelope parameter - attack */
119        private long attack = 0;
120
121        /** Envelope parameter - decay */
122        private long decay = 0;
123
124        /** Envelope parameter - sustain */
125        private float sustain = 1f;
126
127        /** Envelope parameter - release */
128        private long release = 0;
129
130        /** The current value on the envelope */
131        private float currentADSRValue = 0;
132
133        /** The phase the envelope is in */
134        private char envelopePhase = 'N';
135
136        /** Listeners for synthesizer events */
137        private final List<SynthesizerListener> listeners =
138                        new ArrayList<SynthesizerListener>();
139
140        /**
141         *
142         */
143        public Synthesizer()
144    {
145                this.setFormat( new AudioFormat( 16, 44.1, 1 ) );
146    }
147
148        /**
149         *      {@inheritDoc}
150         *      @see org.openimaj.audio.AudioStream#nextSampleChunk()
151         */
152        @Override
153    public SampleChunk nextSampleChunk()
154    {
155                final Oscillator o = this.oscillator;
156                if( !this.noteOn )
157                        return null;
158//                      o = Oscillator.NONE;
159
160            final SampleChunk x = o.getSampleChunk( this.sampleChunkLength,
161                        this.currentTime, this.frequency, this.gain, this.format );
162
163            this.applyADSREnvelope( x.getSampleBuffer() );
164
165            this.currentTime += x.getSampleBuffer().size() /
166                (this.format.getSampleRateKHz()*1000d);
167            this.currentTimeMS = this.currentTime * 1000d;
168
169            return x;
170    }
171
172        /**
173         *      Set the frequency at which the synth will generate tones.
174         *  @param f The frequency
175         */
176        public void setFrequency( final double f )
177        {
178                this.frequency = f;
179        }
180
181        /**
182         *      Set the gain at which the synth will generate tones
183         *      @param gain The gain
184         */
185        public void setGain( final int gain )
186        {
187                this.gain = gain;
188        }
189
190        /**
191         *      Set the type of oscillator used to generate tones.
192         *  @param t The type of oscillator.
193         */
194        public void setOscillatorType( final Oscillator t )
195        {
196                this.oscillator = t;
197        }
198
199        /**
200         *      Returns the oscillator in use.
201         *      @return the oscillator instance.
202         */
203        public Oscillator getOscillator()
204        {
205                return this.oscillator;
206        }
207
208        /**
209         *      {@inheritDoc}
210         *      @see org.openimaj.audio.AudioStream#reset()
211         */
212        @Override
213    public void reset()
214    {
215                this.currentTime = 0;
216    }
217
218        /**
219         *      {@inheritDoc}
220         *      @see org.openimaj.audio.AudioStream#getLength()
221         */
222        @Override
223        public long getLength()
224        {
225                return -1;
226        }
227
228        /**
229         *      Note on.
230         */
231        public void noteOn()
232        {
233                this.noteOn = true;
234                this.noteOff = false;
235                this.noteOnTime = this.currentTimeMS;
236        }
237
238        /**
239         *      Note off
240         */
241        public void noteOff()
242        {
243                this.noteOff  = true;
244                this.noteOffTime = this.currentTimeMS;
245        }
246
247        /**
248         *      Returns whether the synth is playing a note or not.
249         *      @return TRUE if the synth is currently playing a note.
250         */
251        public boolean isNoteOn()
252        {
253                return this.noteOn;
254        }
255
256        /**
257         *      Applies the ADSR gain envelope to the samples generated by the
258         *      generator.
259         *      @param sb The sample buffer to affect
260         */
261        public void applyADSREnvelope( final SampleBuffer sb )
262        {
263                // The current time in the envelope at the start of processing this buffer
264                double currentms = this.currentTimeMS - this.noteOnTime;
265
266                // The time of each sample in ms
267                final double sampleTime = 1d / sb.getFormat().getSampleRateKHz();
268
269                // Loop through the sample buffer
270                for( int i = 0; i < sb.size(); i++ )
271                {
272                        // If the note is set to off and the ADSR value dips to zero,
273                        // then we can say the note is now definitively off.
274                        if( this.noteOff && this.currentADSRValue == 0 && this.envelopePhase == 'R' )
275                        {
276                                this.noteOn = false;
277                                this.fireSynthQuiet();
278                                this.noteOff = false;
279                        }
280
281                        // Outside the attack phase?
282                        if( currentms > this.attack )
283                        {
284                                // Outside the decay phase?
285                                if( currentms > this.decay+this.attack )
286                                {
287                                        if( !this.noteOff )
288                                        {
289                                                // In the sustain phase.
290                                                this.currentADSRValue = this.sustain;
291                                                this.envelopePhase = 'S';
292                                        }
293                                        else
294                                        {
295                                                // in the release phase.
296                                                this.currentADSRValue = this.sustain * Math.max( 0f,
297                                                        (float)(1d-((currentms - this.noteOffTime) /
298                                                                        this.release)) );
299                                                this.envelopePhase = 'R';
300                                        }
301                                }
302                                else
303                                {
304                                        // In the decay phase
305                                        this.currentADSRValue = Math.max( this.sustain, 1f - (float)
306                                                ((1f - this.sustain) *
307                                                                (currentms-this.attack) / this.decay ) );
308                                        this.envelopePhase = 'D';
309                                }
310                        }
311                        else
312                        {
313                                // In the attack phase
314                                this.currentADSRValue = Math.min( 1f,
315                                                (float)(currentms/this.attack) );
316                                this.envelopePhase = 'A';
317                        }
318
319                        sb.set( i, sb.get(i) * this.currentADSRValue );
320
321                        currentms += sampleTime;
322
323//                      System.out.println( currentms +" : "+this.currentADSRValue+" ("+this.envelopePhase+")" );
324                }
325
326//              System.out.println( this.envelopePhase +" : "+this.currentADSRValue );
327        }
328
329        /**
330         *      Returns the phase of the ADSR envelope
331         *      @return the envelope phase.
332         */
333        public char getEnvelopePhase()
334        {
335                return this.envelopePhase;
336        }
337
338        /**
339         *      Get the ADSR attack time in milliseconds
340         *      @return the attack time in milliseconds
341         */
342        public long getAttack()
343        {
344                return this.attack;
345        }
346
347        /**
348         *      Set the ADSR attack time in milliseconds
349         *      @param attack the attack time in milliseconds
350         */
351        public void setAttack( final long attack )
352        {
353                this.attack = attack;
354        }
355
356        /**
357         *      Get the ADSR decay time in milliseconds
358         *      @return the decay time in milliseconds
359         */
360        public long getDecay()
361        {
362                return this.decay;
363        }
364
365        /**
366         *      Set the ADSR decay time in milliseconds
367         *      @param decay the decay time in milliseconds
368         */
369        public void setDecay( final long decay )
370        {
371                this.decay = decay;
372        }
373
374        /**
375         *      Get the ADSR sustain gain
376         *      @return the sustain gain
377         */
378        public float getSustain()
379        {
380                return this.sustain;
381        }
382
383        /**
384         *      Set the ADSR sustain gain
385         *      @param sustain the sustain gain
386         */
387        public void setSustain( final float sustain )
388        {
389                this.sustain = sustain;
390        }
391
392        /**
393         *      Get the release time in milliseconds
394         *      @return the release time in milliseconds
395         */
396        public long getRelease()
397        {
398                return this.release;
399        }
400
401        /**
402         *      Set the ADSR release time in milliseconds
403         *      @param release the release time in milliseconds
404         */
405        public void setRelease( final long release )
406        {
407                this.release = release;
408        }
409
410        /**
411         *      Add a synth listener to this synth.
412         *      @param sl The synth listener
413         */
414        public void addSynthesizerListener( final SynthesizerListener sl )
415        {
416                this.listeners.add( sl );
417        }
418
419        /**
420         *      remove the synth listener from this synth.
421         *      @param sl The synth listener to remove
422         */
423        public void removeSynthesizerListener( final SynthesizerListener sl )
424        {
425                this.listeners.remove( sl );
426        }
427
428        /**
429         *      Fired when the synth finished the release phase.
430         */
431        protected void fireSynthQuiet()
432        {
433                final ArrayList<SynthesizerListener> l = new ArrayList<SynthesizerListener>( this.listeners );
434                for( final SynthesizerListener sl : l )
435                        sl.synthQuiet();
436        }
437
438        /**
439         *      {@inheritDoc}
440         *      @see org.openimaj.audio.Instrument#noteOn(int, double)
441         */
442        @Override
443        public void noteOn( final int noteNumber, final double velocity )
444        {
445                System.out.println( "Note on "+noteNumber );
446                this.setGain( (int)(velocity * Integer.MAX_VALUE) );
447                this.setFrequency( WesternScaleNote.noteToFrequency(noteNumber) );
448                this.noteOn();
449        }
450
451        /**
452         *      {@inheritDoc}
453         *      @see org.openimaj.audio.Instrument#noteOff(int)
454         */
455        @Override
456        public void noteOff( final int noteNumber )
457        {
458                System.out.println( "Note off "+noteNumber );
459                this.noteOff();
460        }
461}