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}