Page 1 of 2

Quad-Klanger released: A real-time Bezier sound synthesizer

Posted: Fri Jul 27, 2012 10:27 pm
by doctornash
Quad-Klanger is a real-time monophonic synthesizer playable from a virtual or hardware midi keyboard, featuring a novel sound production technique - Bezier synthesis. Now regarding the name, I thought the German word for sound (klang) was perfect to describe the type of stuff it is good at producing (clangorous percussion, industrial noises, bell-like tones, unusual basses etc), and the 'quad' because the x and y co-ordinates of the two points that warp the waveform along the x and y axes, can all be modulated independently.

For the whole project, just unzip the contents of this file into a directory
http://www.mediafire.com/?v1r8kietyv6rmi7
and launch Quad-Klanger.pb (it makes use of curvegadget.pbi and portaudio.pb, which are in the zip). For just playing the exe, you only need to put Quad-Klanger101.exe, portaudio_x86.dll, portaudio_x86.lib from the zip in a directory, and launch the exe from there.

Thanks to Infratec for the midi device switching and midi note event detection/frequency conversion procedures, and for optimizing parts of the code for performance.

Here is a screenshot
Image

The Help file is built into the app, but I reproduce it here:

Code: Select all

The X1, X2 sliders 'pull' the waveform along the X axis
The Y1, Y2 sliders pull' the waveform along the Y axis

You can make each of the X1, X2, Y1, Y2 points 'wobble' as the Note plays by modulating each of them independently, allowing a wide variety of harmonically rich sounds to be produced

Let us take point X1 for example:

The point will oscillate between where the X1 slider is currently set, to a level determined by X1Mod%. If X1Mod% = +80, the point will oscillate between where the X1 slider is currently set, to 80% of the way up to the top of the slider. If X1Mod% = -30, the point will oscillate between where the X1 slider is currently set, to 30% of the way to the bottom of the slider. The point will oscillate at a frequency determined by CoarseX1ModF and FineX1ModF. If Coarse X1ModF = 2 and FineX1ModF = .25, the point will oscillate at 2.25 times the frequency of the Note being played on the keyboard. If Coarse X1ModF = 10 and FineX1ModF = 0, the point will oscillate at 10 times the frequency of the Note being played on the keyboard. If Coarse X1ModF = 0 and FineX1ModF = .05, the point will oscillate at 0.05 times the frequency of the Note being played on the keyboard. Integer multiples of the Note Frequency set by CoarseX1ModF result in harmonic overtones being produced, whilst the fractional multiplier set by FineX1ModF results in inharmonic overtones.

You can make the point oscillation increase or decrease in amplitude over time as the Note plays, by moving the 7 segments of the wobble amplitude curve for point X1 into your desired envelope shape, and the period over which that envelope applies is determined by the Duration slider. The x1 setting for the Duration slider constrains this period to a maximum of a couple of seconds, whilst the x10 setting allows the period to be many seconds.

For example, if you draw the wobble amplitude curve as a straight line starting at lower left and ending at top right of the envelope window, and you leave the Duration slider in its default position, then when you press a Note, the point oscillation will occur between where the X1 slider is currently set, and a level which linearly moves over a couple of seconds from X1 to the level determined by X1Mod%. This will change the harmonic content of the sound as the Note plays, giving a kind of 'filter sweeping' effect.

The sound produced by this engine is then passed through a master low pass filter, the frequency of which is set by the F slider and the Resonance by the Res slider, giving you even greater sound shaping versatility, and then through the level ADSR, which you set with the Amplitude EG sliders.

The Vol slider adjusts the overall level of the sound

The MIDI devices available on your system which Quad-Klanger can use to receive Midi note events from your virtual or external hardware keyboard, are shown in the Midi devices drop-down. Select the device you wish to use, and ensure your keyboard transmits through the same device. Quad-Klanger features a form of velocity-sensitivity so that if your keyboard is velocity sensitive, the sound will play softer if you press the keys lightly, and louder if you press the keys harder
And here is a pic of the midi-device-switch being tested, to toggle play between a virtual keyboard you can see on the screen, and a little m-audio oxygen 8 controller (the edge of which is in the photo) plugged into the usb port
Image
I got the virtual keyboard from here:
http://www.granucon.com/vmk.html
and the virtual midi port for the virtual keyboard (loopbe1) from here:
http://nerds.de/en/loopbe1.html

Re: Quad-Klanger released: A real-time Bezier sound synthesi

Posted: Sat Jul 28, 2012 1:00 am
by electrochrisso
8), Well worth a look, Thanks for sharing. :)

Re: Quad-Klanger released: A real-time Bezier sound synthesi

Posted: Sun Jul 29, 2012 3:02 pm
by karmacomposer
Can pure basic be used to create virtual instruments or only stand alone apps?

Mike

Re: Quad-Klanger released: A real-time Bezier sound synthesi

Posted: Sun Jul 29, 2012 11:28 pm
by Zach
I would assume that as long as you have the ability to interface with the API your are targetting without any major headaches, I don't see why PB couldn't be used to make VST's or VSTi's, etc. Since those are packaged as DLL's and we certainly have the ability to compile those..

But I don't know what all goes into the process of creating those kind of Instrument plugins, so there may be things I don't know about, which have to be considered.

But on the surface, I would hazzard a guess this should be possible.

Re: Quad-Klanger released: A real-time Bezier sound synthesi

Posted: Mon Jul 30, 2012 10:29 pm
by doctornash
having fun pumping out sounds with it, like this :mrgreen:
http://soundcloud.com/doctornash/quad-klangerassorted

Re: Quad-Klanger released: A real-time Bezier sound synthesi

Posted: Tue Jul 31, 2012 12:30 am
by Zach
Pretty cool :mrgreen:

Re: Quad-Klanger released: A real-time Bezier sound synthesi

Posted: Tue Jul 31, 2012 10:29 am
by doctornash
user feedback is coming in...and the most commonly reported issue appears to be the sharp 'clicking' that occurs when a key is pressed, just prior to the sound playing. if anyone can help isolate the cause of this (and better still, help in remediating it :) ) I'd greatly appreciate it.

Why does this phenomenon not occur with other stand-alone soft-synths (wonder what are they doing that's different)? Yes this is a monophonic synth so the old key can only be replaced by the new one (such that the old key can't go to the end of its release state), but it seems to occur intermittently even when a key is pressed for the first time on the keyboard, or even when a key is pressed after the previous key has gone to the end of the release (0 amplitude)

Re: Quad-Klanger released: A real-time Bezier sound synthesi

Posted: Tue Jul 31, 2012 12:24 pm
by Zach
Maybe its a sound buffer, or latency issue? Or some kind of artifact?

I have have "clicking" issues with other VsT's in the past.

Re: Quad-Klanger released: A real-time Bezier sound synthesi

Posted: Wed Aug 01, 2012 5:04 am
by doctornash
Thanks for the suggestion. Tried playing around with the suggestedLatency value (currently set to 150/1000 but varied it from 20 to 300), and framesperbuffer value in pa_openstream (even using paFramesPerBufferUnspecified as the value to let portaudio work out optimal frames per buffer itself). Made no difference to the ‘clicking’ noise when key is pressed.

The frustrating thing is that everything after that ‘noise impulse’ works like a charm (ie sound processes and plays smoothly as expected). I think this is the only aspect preventing the app from being used ‘seriously’, as any keyboard sequence played with it has to be ‘cleaned up’ in an editor to reduce the ‘clicking’ amplitude, before it can be used in song production.

Now, something like Propellerhead Reason is not a vst, yet it’s monosynths don’t have a discernible clicking on keypress when played with a midi keyboard – so it certainly appears it can be done (though I have no idea what trickery they used). Here’s the app code from the zip file. ANY help in solving this would be truly appreciated, because then we would all have a PB template for ANY kind of real-time synth implementation (ie one could then just tack on any sort of sound generation engine, not just a Bezier-based one as happens to be the case here):

Code: Select all

;Quad-Klanger v101 a monophonic bezier-curve based sound synthesizer
;by doctornash
;midi procedures and some optimization by infratec


XIncludeFile "PortAudio.pb"
XIncludeFile "CurveGadget.pbi"

;for curvegadgets
Global Dim CurvePoint.POINT(1)
Global No2
Global No3
Global No4
Global No5
;end curvegadget

;initial points for single cycle of bezier waveform
Global ptA.f = 0.5
Global ptB.f = 0.06
Global ptC.f = 0.75
Global ptD.f = 0.95

#SampleRate = 44100

#PI2 = 6.28318530717958

Global BezierEnvelopeSamples.l
Global Pee.l
Global InitializeTheBezier.l=0

Global ModX1Freq.f
Global ModX2Freq.f
Global ModY1Freq.f
Global ModY2Freq.f

Global X1HOOKA.f
Global X2HOOKA.f
Global Y1HOOKA.f
Global Y2HOOKA.f

;for triangle wave modulation method of bezier points
Global NewX1Level.f
Global X1Sign.l
Global TheX1Level.f
Global UX1.f 
Global VX1.f
Global FinalCurveGadgetValX1.f

Global NewY1Level.f
Global Y1Sign.l
Global TheY1Level.f
Global UY1.f 
Global VY1.f
Global FinalCurveGadgetValY1.f

Global NewX2Level.f
Global X2Sign.l
Global TheX2Level.f
Global UX2.f 
Global VX2.f
Global FinalCurveGadgetValX2.f

Global NewY2Level.f
Global Y2Sign.l
Global TheY2Level.f
Global UY2.f 
Global VY2.f
Global FinalCurveGadgetValY2.f

Global TheVolume.f

Global Kata.s


Enumeration
  #WIN_MAIN
  #PlayPauseButton
  #MIDIDevice
  #Trackbar_FilterFreq
  #Trackbar_FilterReso
  #Trackbar_Attack
  #Trackbar_Decay
  #Trackbar_Sustain
  #Trackbar_Release
  #Text_FilterFreq
  #Text_FilterReso
  #Text_Attack
  #Text_Sustain
  #Text_Decay
  #Text_Release
  
  ;for the bezier:
  #GADGET_Canvas2
  #GADGET_TrackA
  #GADGET_TrackB
  #GADGET_TrackC
  #GADGET_TrackD
  #GADGET_TrackVolume
  #GADGET_TrackDuration
  #GADGET_WaveGrapher
  
  #GADGET_MODAMOUNTX1
  #GADGET_MODFREQCOARSEX1
  #GADGET_MODFREQFINEX1
  #GADGET_MODAMOUNTY1
  #GADGET_MODFREQCOARSEY1
  #GADGET_MODFREQFINEY1
  #GADGET_MODAMOUNTX2
  #GADGET_MODFREQCOARSEX2
  #GADGET_MODFREQFINEX2
  #GADGET_MODAMOUNTY2
  #GADGET_MODFREQCOARSEY2
  #GADGET_MODFREQFINEY2
  
  #GADGET_TextX1
  #GADGET_TextY1
  #GADGET_TextX2
  #GADGET_TextY2
  #GADGET_TextX1ModPercentage
  #GADGET_TextY1ModPercentage
  #GADGET_TextX2ModPercentage
  #GADGET_TextY2ModPercentage
  #GADGET_TextX1CoarseModFreq
  #GADGET_TextX1FineModFreq
  #GADGET_TextY1CoarseModFreq
  #GADGET_TextY1FineModFreq
  #GADGET_TextX2CoarseModFreq
  #GADGET_TextX2FineModFreq
  #GADGET_TextY2CoarseModFreq
  #GADGET_TextY2FineModFreq
  #GADGET_TextMasterVolume
  #GADGET_TextDuration
  #GADGET_ModDurationIncrement
  #GADGET_FrameAmpEG
  #GADGET_FrameFilter
EndEnumeration

Enumeration
  #Off
  #On
EndEnumeration


Global State.i
Global MaxAmplitude.d

Global SampleCounter.l

Global TheDuration.f=2000
Global ModAmountX1.f
Global ModAmountX2.f
Global ModAmountY1.f
Global ModAmountY2.f

;midi stuff
Global Chromatic$
Chromatic$="C C#D EbE F F#G AbA BbB C "
Global NoteVal.l
Global Freq.d
Global NoteCheck.i
Global foo.l
Global p.d
Global i.d

;ADSR stuff
Global Attack.d
Global Decay.d
Global Sustain.d
Global Release.d

Global AttackSamples.d
Global DecaySamples.d
Global SustainLevel.d
Global ReleaseSamples.d

Global ADSRLevel.d
Global UptoV.d ;the ADSR level we have reached when Release strikes
Global UptoP.l ;samplecount we have reached when the Release strikes


NoteCheck = 0

;filter stuff
Global cutoffo.d
Global t1o.d
Global t2o.d
Global reso.d
Global rawresolution.l

cutoffo = 1500
rawresolution = 50

Declare.f MoogFilter16(Uo.f)
Declare.f CubicBezier_ForDrawing(xval.f, aval.f, bval.f, cval.f, dval.f)
Declare.f CubicBezier(SampleNum.l, xval.f, aval.f, bval.f, cval.f, dval.f)
Declare.f slopeFromT(tee.f, Ai.f, Bi.f, Ci.f)
Declare.f xFromT(tee.f, Ai.f, Bi.f, Ci.f, Di.f)
Declare.f yFromT(tee.f, Ei.f, Fi.f, Gi.f, Hi.f)
Declare DrawTheBezier()
Declare.f CalculateTheWaveform(SamplePoint.l)
Declare ShowAboutWindow()


Procedure.i MIDI_GetInputDevices()
 
  Protected mic.MIDIINCAPS, Num.i, i.i
 
  Num = midiInGetNumDevs_()
  For i = 0 To Num - 1
    If midiInGetDevCaps_(i, @mic, SizeOf(MIDIINCAPS)) = 0
      AddGadgetItem(#MIDIDevice, -1, PeekS(@mic\szPname))
    EndIf
  Next i
 
  ProcedureReturn Num
 
EndProcedure


Procedure.s MIDI_Note(Note);returns note's name
   ProcedureReturn Trim(Mid(Chromatic$, (Note % 12) * 2 + 1, 2)) + Str(Note / 12)
EndProcedure


Procedure MidiInCallback(hMidiIn.i, wMsg.i, dwInstance.l, dwParam1.l, dwParam2.l)
 
  Protected Note.i, Velocity.i, Status.i
  Static KeyCounter.i
  
  Select wMsg    ; process MIDI in events
    Case #MIM_CLOSE
      KeyCounter = 0
      State = #Off
      kata = "Off"
    Case #MM_MIM_DATA
      Status = dwParam1 & $FF
      If Status = $90
        Note = (dwParam1 >> 8) & $FF
        Velocity = (dwParam1 >> 16) & $FF
        If Velocity
          MaxAmplitude = Velocity / 127.0
          Freq = Pow(2, (Note - 69) / 12) * 440
          ModX1Freq = (ValF(Str(GetGadgetState(#GADGET_MODFREQCOARSEX1)) + "." + Str(GetGadgetState(#GADGET_MODFREQFINEX1))))*Freq
          ModX2Freq = (ValF(Str(GetGadgetState(#GADGET_MODFREQCOARSEX2)) + "." + Str(GetGadgetState(#GADGET_MODFREQFINEX2))))*Freq
          ModY1Freq = (ValF(Str(GetGadgetState(#GADGET_MODFREQCOARSEY1)) + "." + Str(GetGadgetState(#GADGET_MODFREQFINEY1))))*Freq
          ModY2Freq = (ValF(Str(GetGadgetState(#GADGET_MODFREQCOARSEY2)) + "." + Str(GetGadgetState(#GADGET_MODFREQFINEY2))))*Freq
          If KeyCounter = 0
            State = #On
            kata = "On"
          EndIf
          ;If IsStatusBar(0) : StatusBarText(0, 1, MIDI_Note(Note) + " " + Str(Velocity), #PB_StatusBar_Center) : EndIf
          KeyCounter + 1
        Else
          If KeyCounter : KeyCounter - 1 : EndIf
          If KeyCounter = 0
          State = #Off
          kata = "Off"
          InitializeTheBezier = 0
          EndIf
        EndIf
      EndIf 
      
      If Status = $80
        If KeyCounter : KeyCounter - 1 : EndIf
        If KeyCounter = 0
          State = #Off 
          kata = "Off"
          InitializeTheBezier = 0
        EndIf
      EndIf
      
      If IsStatusBar(0) : StatusBarText(0, 1, kata, #PB_StatusBar_Center) : EndIf

  EndSelect
EndProcedure
;end midi stuff


Procedure Error(err)
  MessageRequester("PortAudio", PeekS(Pa_GetErrorText(err)))
  End
EndProcedure


 Procedure.f CalculateTheWaveform(SamplePoint.l)
   
  Protected N.l
  Protected NumSamplePoints.l
  Protected RawBezierVal.f
  
If Freq>0  
  NumSamplePoints = #SampleRate/Freq
  
    Pee = Pee+1
    If Pee = NumSamplePoints
      Pee = 0
    EndIf
        RawBezierVal = (200 - (CubicBezier(SamplePoint, Pee/NumSamplePoints, ptA, ptB, ptC, ptD)))/200
        If RawBezierVal = 0.5
          RawBezierVal = 0
        Else
          ;increasing the amplitude of the overall sound to give it an acceptable volume
          RawBezierVal = ((RawBezierVal-0.5)*TheVolume/100)
        EndIf  
EndIf       
ProcedureReturn RawBezierVal
     
EndProcedure  


Procedure NewX1ModMethod(SampleVal.l)
;even though this is called NewX1Method, it is for the modulation of ALL points!  
  
  Protected MuX1 
  Protected MuY1
  Protected MuX2
  Protected MuY2
  
  If SampleVal<=BezierEnvelopeSamples-1 
  MuX1 = VX1*CurveGadgetGetAttribute(No2, #CurveGadget_Y_Value_Scaled, SampleVal) / 100
  MuY1 = VY1*CurveGadgetGetAttribute(No3, #CurveGadget_Y_Value_Scaled, SampleVal) / 100
  MUX2 = VX2*CurveGadgetGetAttribute(No4, #CurveGadget_Y_Value_Scaled, SampleVal) / 100
  MuY2 = VY2*CurveGadgetGetAttribute(No5, #CurveGadget_Y_Value_Scaled, SampleVal) / 100
  Else
  MuX1 = VX1*FinalCurveGadgetValX1
  MuY1 = VY1*FinalCurveGadgetValY1 
  MuX2 = VX2*FinalCurveGadgetValX2 
  MuY2 = VY2*FinalCurveGadgetValY2 
  EndIf  
    
  
If ModX1Freq>0  
  If ModAmountX1>0   
    NewX1Level = NewX1Level+ X1Sign*((MuX1*2)/(#SampleRate/ModX1Freq))
    If NewX1Level>TheX1Level+MuX1
      NewX1Level = TheX1Level+MuX1
      X1Sign = -1
    ElseIf NewX1Level<TheX1Level
      NewX1Level = TheX1Level
      X1Sign = 1
    EndIf 
  Else
    NewX1Level = NewX1Level+ X1Sign*((MuX1*2)/(#SampleRate/ModX1Freq))
    If NewX1Level<TheX1Level-MuX1
      NewX1Level = TheX1Level-MuX1
      X1Sign = 1
    ElseIf NewX1Level>TheX1Level
      NewX1Level = TheX1Level
      X1Sign = -1
    EndIf 
  EndIf   
EndIf  

If ModY1Freq>0
  If ModAmountY1>0   
    NewY1Level = NewY1Level+ Y1Sign*((MuY1*2)/(#SampleRate/ModY1Freq))
    If NewY1Level>TheY1Level+MuY1
      NewY1Level = TheY1Level+MuY1
      Y1Sign = -1
    ElseIf NewY1Level<TheY1Level
      NewY1Level = TheY1Level
      Y1Sign = 1
    EndIf 
  Else
    NewY1Level = NewY1Level+ Y1Sign*((MuY1*2)/(#SampleRate/ModY1Freq))
    If NewY1Level<TheY1Level-MuY1
      NewY1Level = TheY1Level-MuY1
      Y1Sign = 1
    ElseIf NewY1Level>TheY1Level
      NewY1Level = TheY1Level
      Y1Sign = -1
    EndIf 
  EndIf   
EndIf  

If ModX2Freq>0
  If ModAmountX2>0   
    NewX2Level = NewX2Level+ X2Sign*((MuX2*2)/(#SampleRate/ModX2Freq))
    If NewX2Level>TheX2Level+MuX2
      NewX2Level = TheX2Level+MuX2
      X2Sign = -1
    ElseIf NewX2Level<TheX2Level
      NewX2Level = TheX2Level
      X2Sign = 1
    EndIf 
  Else
    NewX2Level = NewX2Level+ X2Sign*((MuX2*2)/(#SampleRate/ModX2Freq))
    If NewX2Level<TheX2Level-MuX2
      NewX2Level = TheX2Level-MuX2
      X2Sign = 1
    ElseIf NewX2Level>TheX2Level
      NewX2Level = TheX2Level
      X2Sign = -1
    EndIf 
  EndIf 
EndIf  

If ModY2Freq>0
  If ModAmountY2>0   
    NewY2Level = NewY2Level+ Y2Sign*((MuY2*2)/(#SampleRate/ModY2Freq))
    If NewY2Level>TheY2Level+MuY2
      NewY2Level = TheY2Level+MuY2
      Y2Sign = -1
    ElseIf NewY2Level<TheY2Level
      NewY2Level = TheY2Level
      Y2Sign = 1
    EndIf 
  Else
    NewY2Level = NewY2Level+ Y2Sign*((MuY2*2)/(#SampleRate/ModY2Freq))
    If NewY2Level<TheY2Level-MuY2
      NewY2Level = TheY2Level-MuY2
      Y2Sign = 1
    ElseIf NewY2Level>TheY2Level
      NewY2Level = TheY2Level
      Y2Sign = -1
    EndIf 
  EndIf 
EndIf  
  
  X1HOOKA = NewX1Level/100
  Y1HOOKA = NewY1Level/100
  X2HOOKA = NewX2Level/100
  Y2HOOKA = (NewY2Level+50)/100
     
EndProcedure  


Procedure DrawTheBezier()
 ;this is for graphing the single cycle wave only
  Protected N.i
  Dim BezierDrawArr(399)
  For N = 0 To 399
    BezierDrawArr(N) = CubicBezier_ForDrawing(N/400, ptA, ptB, ptC, ptD)
  Next N
     
  StartDrawing(CanvasOutput(#GADGET_Canvas2))
  Box(0, 0, 400, 200, $000000)
  For N = 0 To 398
    LineXY(N, BezierDrawArr(N), N+1, BezierDrawArr(N+1), $FFFFFF)
  Next N 
  StopDrawing()  

EndProcedure 


Procedure.f CubicBezier_ForDrawing(xval.f, aval.f, bval.f, cval.f, dval.f)
;this is for graphing the single cycle wave only
Protected A.f = 0
Protected B.f = 0
Protected C.f = 0
Protected D.f = 0
Protected E.f = 0
Protected F.f = 0
Protected G.f = 0
Protected H.f = 0
 
Protected y0a.f
Protected x0a.f
Protected y1a.f
Protected x1a.f
Protected y2a.f
Protected x2a.f
Protected y3a.f
Protected x3a.f
Protected currentt.f
Protected nRefinementIterations.l
Protected i.l = 0
Protected currentx.f
Protected currentslope.f
Protected CB.f

y0a = 0.5
x0a = 0
y1a = bval
x1a = aval
y2a = dval
x2a = cval
y3a = 0.5
x3a = 1

A = x3a - 3*x2a + 3*x1a - x0a
B = 3*x2a - 6*x1a + 3*x0a
C = 3*x1a - 3*x0a
D = x0a
E = y3a - 3*y2a + 3*y1a - y0a
F = 3*y2a - 6*y1a + 3*y0a       
G = 3*y1a - 3*y0a         
H = y0a

currentt = xval
nRefinementIterations = 5

For i = 0 To nRefinementIterations-1
currentx = xFromT (currentt, A,B,C,D)
currentslope = slopeFromT (currentt, A,B,C)
currentt = currentt - ((currentx - xval)*(currentslope))
Next i

If currentt>1
currentt = 1
ElseIf currentt<0
currentt=0
EndIf

CB = yFromT(currentt, E,F,G,H)

cb = cb*200
If cb>200
cb = 200
EndIf
cb = 200-cb

ProcedureReturn CB

EndProcedure

Procedure.f CubicBezier(SampleNum.l, xval.f, aval.f, bval.f, cval.f, dval.f)
;this is the Bezier calculator used in the sound production 
Protected A.f = 0
Protected B.f = 0
Protected C.f = 0
Protected D.f = 0
Protected E.f = 0
Protected F.f = 0
Protected G.f = 0
Protected H.f = 0
 
Protected y0a.f
Protected x0a.f
Protected y1a.f
Protected x1a.f
Protected y2a.f
Protected x2a.f
Protected y3a.f
Protected x3a.f
Protected currentt.f
Protected nRefinementIterations.l
Protected i.l = 0
Protected currentx.f
Protected currentslope.f
Protected CB.f

y0a = 0.5
x0a = 0
y1a = bval
x1a = aval
y2a = dval
x2a = cval
y3a = 0.5
x3a = 1

NewX1ModMethod(SampleNum)
x1a = X1HOOKA
y1a = Y1HOOKA
x2a = X2HOOKA
y2a = Y2HOOKA

A = x3a - 3*x2a + 3*x1a - x0a
B = 3*x2a - 6*x1a + 3*x0a
C = 3*x1a - 3*x0a
D = x0a
E = y3a - 3*y2a + 3*y1a - y0a
F = 3*y2a - 6*y1a + 3*y0a       
G = 3*y1a - 3*y0a         
H = y0a

currentt = xval
nRefinementIterations = 5

For i = 0 To nRefinementIterations-1
currentx = xFromT (currentt, A,B,C,D)
currentslope = slopeFromT (currentt, A,B,C)
currentt = currentt - ((currentx - xval)*(currentslope))
Next i

If currentt>1
currentt = 1
ElseIf currentt<0
currentt=0
EndIf

CB = yFromT(currentt, E,F,G,H)

cb = cb*200
If cb>200
cb = 200
EndIf
cb = 200-cb

ProcedureReturn CB

EndProcedure


Procedure.f slopeFromT(tee.f, Ai.f, Bi.f, Ci.f)
Protected dtdx.f
dtdx = 1/(3*Ai*tee*tee + 2*Bi*tee + Ci)
ProcedureReturn dtdx
EndProcedure


Procedure.f xFromT(tee.f, Ai.f, Bi.f, Ci.f, Di.f)
Protected ex.f 
ex = Ai*(tee*tee*tee) + Bi*(tee*tee) + Ci*tee + Di
ProcedureReturn ex
EndProcedure

Procedure.f yFromT(tee.f, Ei.f, Fi.f, Gi.f, Hi.f)
Protected yi.f
yi = Ei*(tee*tee*tee) + Fi*(tee*tee) + Gi*tee + Hi
ProcedureReturn yi
EndProcedure

Global Stream

Procedure.f MoogFilter16(Uo.f)
 ;well, a low pass filter, anyway :-) 
  Static.d Po, Fo, Ko, ro, Xo, t1o, t2o
  
  Static.d Y1o, y2o, y3o, y4o
  Static.d oldxo, oldy1o, oldy2o, oldy3o
  
  Static.i OldCutoffo
  Static.l Oldrawresolution
  
  If OldCutoffo <> cutoffo
    Fo = 2 * cutoffo / #SampleRate
    Po = Fo * (1.8 - 0.8 * Fo)
    Ko = Po + Po - 1
    t1o = (1 - Po) * 1.386249
    t2o = 12 + t1o * t1o
    OldCutoffo = cutoffo
  EndIf
  
  If Oldrawresolution <> rawresolution
    ro = (rawresolution / 100) * (t2o + 6 * t1o) / (t2o - 6 * t1o)
    Oldrawresolution = rawresolution
  EndIf
  
  Xo = Uo - ro * y4o
  
  ;Four cascaded onepole filters (bilinear transform)
  Y1o = (Xo + oldxo) * Po - Ko * Y1o
  y2o = (Y1o + oldy1o) * Po - Ko * y2o
  y3o = (y2o + oldy2o) * Po - Ko * y3o
  y4o = (y3o + oldy3o) * Po - Ko * y4o
  
  ;Clipper band limited sigmoid
  y4o = y4o - (y4o * y4o * y4o) / 6
  
  oldxo = Xo
  oldy1o = Y1o
  oldy2o = y2o
  oldy3o = y3o
  
  ProcedureReturn y4o
 
EndProcedure

Procedure LevelADSR(SampleTracker.l)
  
If SampleTracker>2147483640
SampleCounter = 0
UptoV = 0
EndIf  
 
If State = #Off
    If (ReleaseSamples+UptoP-SampleTracker)>=0
      ADSRLevel = (UptoV*(ReleaseSamples+UptoP-SampleTracker))/ReleaseSamples  
    Else
      ADSRLevel = 0
    EndIf  
  If IsStatusBar(0)
  EndIf
EndIf
 
 If State = #On
    If InitializeTheBezier = 0
      SampleCounter = 0
      InitializeTheBezier = 1
      Pee = -1
      BezierEnvelopeSamples = (TheDuration/1000)*#SampleRate
      CurveGadgetSetAttribute(No2, #CurveGadget_X_Maximum, BezierEnvelopeSamples-1)
      CurveGadgetSetAttribute(No2, #CurveGadget_Y_Maximum, 100)
 
      CurveGadgetSetAttribute(No3, #CurveGadget_X_Maximum, BezierEnvelopeSamples-1)
      CurveGadgetSetAttribute(No3, #CurveGadget_Y_Maximum, 100)
 
      CurveGadgetSetAttribute(No4, #CurveGadget_X_Maximum, BezierEnvelopeSamples-1)
      CurveGadgetSetAttribute(No4, #CurveGadget_Y_Maximum, 100)
 
      CurveGadgetSetAttribute(No5, #CurveGadget_X_Maximum, BezierEnvelopeSamples-1)
      CurveGadgetSetAttribute(No5, #CurveGadget_Y_Maximum, 100)
      
      ModAmountX1 = GetGadgetState(#GADGET_MODAMOUNTX1)
      ModAmountX2 = GetGadgetState(#GADGET_MODAMOUNTX2)
      ModAmountY1 = GetGadgetState(#GADGET_MODAMOUNTY1)
      ModAmountY2 = GetGadgetState(#GADGET_MODAMOUNTY2)
      
      TheX1Level = GetGadgetState(#GADGET_TrackA)
      NewX1Level = TheX1Level 
      UX1 = ModAmountX1/100
      If ModAmountX1>0
      X1Sign = 1
      VX1 = UX1*(99-TheX1Level)
      Else
      X1Sign = -1
      VX1 = -1*UX1*(TheX1Level-1)
      EndIf
      FinalCurveGadgetValX1 = CurveGadgetGetAttribute(No2, #CurveGadget_Y_Value_Scaled, BezierEnvelopeSamples-1)/100      
      
      TheY1Level = GetGadgetState(#GADGET_TrackB)
      NewY1Level = TheY1Level
      UY1 = ModAmountY1/100
      If ModAmountY1>0
      Y1Sign = 1 
      VY1 = UY1*(49-TheY1Level)
      Else
      Y1Sign = -1
      VY1 = -1*UY1*(TheY1Level-1)
      EndIf
      FinalCurveGadgetValY1 = CurveGadgetGetAttribute(No3, #CurveGadget_Y_Value_Scaled, BezierEnvelopeSamples-1)/100
      
      
      TheX2Level = GetGadgetState(#GADGET_TrackC)
      NewX2Level = TheX2Level
      UX2 = ModAmountX2/100
      If ModAmountX2>0
      X2Sign = 1 
      VX2 = UX2*(99-TheX2Level)
      Else
      X2Sign = -1
      VX2 = -1*UX2*(TheX2Level-1)
      EndIf
      FinalCurveGadgetValX2 = CurveGadgetGetAttribute(No4, #CurveGadget_Y_Value_Scaled, BezierEnvelopeSamples-1)/100      
      
      TheY2Level = GetGadgetState(#GADGET_TrackD)-50
      NewY2Level = TheY2Level
      UY2 = ModAmountY2/100
      If ModAmountY2>0
      Y2Sign = 1 
      VY2 = UY2*(49-TheY2Level)
      Else
      Y2Sign = -1
      VY2 = -1*UY2*(TheY2Level-1)
      EndIf
      FinalCurveGadgetValY2 = CurveGadgetGetAttribute(No5, #CurveGadget_Y_Value_Scaled, BezierEnvelopeSamples-1) / 100
      
      AttackSamples = Attack*#SampleRate
      DecaySamples = Decay*#SampleRate
      SustainLevel = Sustain
      ReleaseSamples = Release*#SampleRate
    EndIf
    ;let us calculate the actual ADSR level now shall we
    If SampleTracker<=AttackSamples
      ADSRLevel = SampleTracker/AttackSamples
    ElseIf SampleTracker>AttackSamples And SampleTracker<=(AttackSamples+DecaySamples)
      ADSRLevel = SustainLevel + (((AttackSamples+DecaySamples-SampleTracker)*(1-SustainLevel))/DecaySamples)
    ElseIf SampleTracker>(AttackSamples+DecaySamples)
      ADSRLevel = SustainLevel
    EndIf
    UptoV = ADSRLevel
    UptoP = SampleTracker
EndIf   

EndProcedure


ProcedureC PaStreamCallback(*in, *output.Float, frameCount, *timeInfo.PaStreamCallbackTimeInfo, statusFlags, *userData)
  
  While frameCount
   LevelADSR(SampleCounter)
   *output\f = MoogFilter16(CalculateTheWaveform(SampleCounter))*ADSRLevel*MaxAmplitude
   SampleCounter = SampleCounter+1  
   *output + 4
   frameCount - 1
Wend
 
EndProcedure


Define err.i

err = Pa_Initialize()
If err <> #paNoError : Error(err) : EndIf

Define op.PaStreamParameters
op\channelCount = 1
op\device = Pa_GetDefaultOutputDevice()
op\sampleFormat = #paFloat32
op\suggestedLatency = 150/1000


err = Pa_OpenStream(@Stream, #Null, @op, #SampleRate, #SampleRate / 100, #paNoFlag, @PaStreamCallback(), 0)
If err <> #paNoError : Error(err) : EndIf

Define Indev.i
InDev = 0

Define hMi.i
hMi = 0

If OpenWindow(#WIN_MAIN, 0, 0, 535, 590, "Quad-Klanger", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
  
    If CreateMenu(0, WindowID(#WIN_MAIN))    ; menu creation starts....
      MenuTitle("?")
      MenuItem(1, "About")
      MenuItem(2, "Help")
    EndIf
  
  Frame3DGadget(#GADGET_FrameAmpEG, 400,  230, 120, 130, "Amplitude EG")
  Frame3DGadget(#GADGET_FrameFilter, 400,  370, 120, 130, "Filter")

 
  ComboBoxGadget(#MIDIDevice, 390, 520, 140, 20)
  If MIDI_GetInputDevices()
    SetGadgetState(#MIDIDevice, 0)
    InDev = 0
  Else
    InDev = -1
  EndIf
  
  No2 = CurveGadget(#PB_Any, 220, 240, 160, 70, 8, $ffffff, $000000)
  CurveGadgetSetAttribute(No2, #CurveGadget_PointRadius, 3)
  CurveGadgetSetAttribute(No2, #CurveGadget_CatchRadius, 10)
  CurveGadgetSetAttribute(No2, #CurveGadget_X_Maximum, 380)
  CurveGadgetSetAttribute(No2, #CurveGadget_Y_Maximum, 10000)
  
  No3 = CurveGadget(#PB_Any, 220, 320, 160, 70, 8, $ffffff, $000000)
  CurveGadgetSetAttribute(No3, #CurveGadget_PointRadius, 3)
  CurveGadgetSetAttribute(No3, #CurveGadget_CatchRadius, 10)
  CurveGadgetSetAttribute(No3, #CurveGadget_X_Maximum, 380)
  CurveGadgetSetAttribute(No3, #CurveGadget_Y_Maximum, 10000)
  
  No4 = CurveGadget(#PB_Any, 220, 400, 160, 70, 8, $ffffff, $000000)
  CurveGadgetSetAttribute(No4, #CurveGadget_PointRadius, 3)
  CurveGadgetSetAttribute(No4, #CurveGadget_CatchRadius, 10)
  CurveGadgetSetAttribute(No4, #CurveGadget_X_Maximum, 380)
  CurveGadgetSetAttribute(No4, #CurveGadget_Y_Maximum, 10000)
  
  No5 = CurveGadget(#PB_Any, 220, 480, 160, 70, 8, $ffffff, $000000)
  CurveGadgetSetAttribute(No5, #CurveGadget_PointRadius, 3)
  CurveGadgetSetAttribute(No5, #CurveGadget_CatchRadius, 10)
  CurveGadgetSetAttribute(No5, #CurveGadget_X_Maximum, 380)
  CurveGadgetSetAttribute(No5, #CurveGadget_Y_Maximum, 10000)
  
  CanvasGadget(#GADGET_Canvas2, 10, 10, 400, 200, #PB_Canvas_ClipMouse)
  StartDrawing(CanvasOutput(#GADGET_Canvas2))
  Box(0, 0, 400, 200, $000000)
  StopDrawing()
 
  TrackBarGadget(#Trackbar_FilterFreq, 420, 400, 20, 80, 20, 5000, #PB_TrackBar_Vertical)
  TrackBarGadget(#Trackbar_FilterReso, 460, 400, 20, 80, 0, 100, #PB_TrackBar_Vertical)
  SetGadgetState(#Trackbar_FilterFreq, 1500)
  SetGadgetState(#Trackbar_FilterReso, 50)
 
  TrackBarGadget(#Trackbar_Attack, 420, 260, 20, 80, 1, 2000, #PB_TrackBar_Vertical)
  TrackBarGadget(#Trackbar_Decay, 440, 260, 20, 80, 1, 5000, #PB_TrackBar_Vertical)
  TrackBarGadget(#Trackbar_Sustain, 460, 260, 20, 80, 0, 100, #PB_TrackBar_Vertical)
  SetGadgetState(#Trackbar_Sustain, 50)
  Sustain = 0.5
  TrackBarGadget(#Trackbar_Release, 480, 260, 20, 80, 1, 5000, #PB_TrackBar_Vertical)
 
  TextGadget(#Text_Attack, 425, 247, 10, 20, "A")
  TextGadget(#Text_Decay, 445, 247, 10, 20, "D")
  TextGadget(#Text_Sustain, 465, 247, 10, 20, "S")
  TextGadget(#Text_Release, 485, 247, 10, 20, "R")
  TextGadget(#Text_FilterFreq, 425, 387, 10, 20, "F")
  TextGadget(#Text_FilterReso, 455, 387, 20, 20, "Res")
    
  TrackBarGadget(#GADGET_TrackVolume, 420, 140, 20, 80, 0, 1500, #PB_TrackBar_Vertical)
  SetGadgetState(#GADGET_TrackVolume,400)
  TheVolume = 400
 
  TrackBarGadget(#GADGET_TrackDuration, 460, 140, 20, 80, 1000, 10000, #PB_TrackBar_Vertical)
  SetGadgetState(#GADGET_TrackDuration,2000)
  TheDuration = 2000
  
  SpinGadget(#GADGET_ModDurationIncrement, 480, 170, 40, 25, 1, 2)
  SetGadgetState (#GADGET_ModDurationIncrement, 2) 
  SetGadgetText(#GADGET_ModDurationIncrement, "x10")   ; set initial value
 
  TrackBarGadget(#GADGET_TrackA, 420, 30, 20, 80, 1, 99, #PB_TrackBar_Vertical)
  SetGadgetState(#GADGET_TrackA, 50)
  TrackBarGadget(#GADGET_TrackB, 440, 30, 20, 80, 0, 49, #PB_TrackBar_Vertical)
  SetGadgetState(#GADGET_TrackB, 5)
  TrackBarGadget(#GADGET_TrackC, 460, 30, 20, 80, 1, 99, #PB_TrackBar_Vertical)
  SetGadgetState(#GADGET_TrackC, 75)
  TrackBarGadget(#GADGET_TrackD, 480, 30, 20, 80, 50, 99, #PB_TrackBar_Vertical)
  SetGadgetState(#GADGET_TrackD, 95)
  
  SpinGadget(#GADGET_MODAMOUNTX1, 10, 260, 50, 25, -100, 100)
  SetGadgetState (#GADGET_MODAMOUNTX1, 0) : SetGadgetText(#GADGET_MODAMOUNTX1, "0")   ; set initial value
  SpinGadget(#GADGET_MODFREQCOARSEX1, 80, 260, 50, 25, 0, 15)
  SetGadgetState (#GADGET_MODFREQCOARSEX1, 0) : SetGadgetText(#GADGET_MODFREQCOARSEX1, "0")   ; set initial value
  SpinGadget(#GADGET_MODFREQFINEX1, 150, 260, 50, 25, 0, 99)
  SetGadgetState (#GADGET_MODFREQFINEX1, 0) : SetGadgetText(#GADGET_MODFREQFINEX1, ".00")   ; set initial value
  
  SpinGadget(#GADGET_MODAMOUNTY1, 10, 340, 50, 25, -100, 100)
  SetGadgetState (#GADGET_MODAMOUNTY1, 0) : SetGadgetText(#GADGET_MODAMOUNTY1, "0")   ; set initial value
  SpinGadget(#GADGET_MODFREQCOARSEY1, 80, 340, 50, 25, 0, 15)
  SetGadgetState (#GADGET_MODFREQCOARSEY1, 0) : SetGadgetText(#GADGET_MODFREQCOARSEY1, "0")   ; set initial value
  SpinGadget(#GADGET_MODFREQFINEY1, 150, 340, 50, 25, 0, 99)
  SetGadgetState (#GADGET_MODFREQFINEY1, 0) : SetGadgetText(#GADGET_MODFREQFINEY1, ".00")   ; set initial value
  
  SpinGadget(#GADGET_MODAMOUNTX2, 10, 420, 50, 25, -100, 100)
  SetGadgetState (#GADGET_MODAMOUNTX2, 0) : SetGadgetText(#GADGET_MODAMOUNTX2, "0")   ; set initial value
  SpinGadget(#GADGET_MODFREQCOARSEX2, 80, 420, 50, 25, 0, 15)
  SetGadgetState (#GADGET_MODFREQCOARSEX2, 0) : SetGadgetText(#GADGET_MODFREQCOARSEX2, "0")   ; set initial value
  SpinGadget(#GADGET_MODFREQFINEX2, 150, 420, 50, 25, 0, 99)
  SetGadgetState (#GADGET_MODFREQFINEX2, 0) : SetGadgetText(#GADGET_MODFREQFINEX2, ".00")   ; set initial value
  
  SpinGadget(#GADGET_MODAMOUNTY2, 10, 500, 50, 25, -100, 100)
  SetGadgetState (#GADGET_MODAMOUNTY2, 0) : SetGadgetText(#GADGET_MODAMOUNTY2, "0")   ; set initial value
  SpinGadget(#GADGET_MODFREQCOARSEY2, 80, 500, 50, 25, 0, 15)
  SetGadgetState (#GADGET_MODFREQCOARSEY2, 0) : SetGadgetText(#GADGET_MODFREQCOARSEY2, "0")   ; set initial value
  SpinGadget(#GADGET_MODFREQFINEY2, 150, 500, 50, 25, 0, 99)
  SetGadgetState (#GADGET_MODFREQFINEY2, 0) : SetGadgetText(#GADGET_MODFREQFINEY2, ".00")   ; set initial value
  
  TextGadget(#GADGET_TextX1, 420, 10, 20, 20, "X1")
  TextGadget(#GADGET_TextY1, 440, 10, 20, 20, "Y1")
  TextGadget(#GADGET_TextX2, 460, 10, 20, 20, "X2")
  TextGadget(#GADGET_TextY2, 480, 10, 20, 20, "Y2")
  TextGadget(#GADGET_TextMasterVolume, 420, 120, 30, 20, "Vol")
  TextGadget(#GADGET_TextDuration, 450, 120, 50, 20, "Duration")
  
  TextGadget(#GADGET_TextX1ModPercentage, 10, 240, 50, 20, "X1Mod%")
  TextGadget(#GADGET_TextX1CoarseModFreq, 70, 240, 80, 20, "CoarseX1ModF")
  TextGadget(#GADGET_TextX1FineModFreq, 150, 240, 70, 20, "FineX1ModF")
  
  TextGadget(#GADGET_TextY1ModPercentage, 10, 320, 50, 20, "Y1Mod%")
  TextGadget(#GADGET_TextY1CoarseModFreq, 70, 320, 80, 20, "CoarseY1ModF")
  TextGadget(#GADGET_TextY1FineModFreq, 150, 320, 70, 20, "FineY1ModF")
  
  TextGadget(#GADGET_TextX2ModPercentage, 10, 400, 50, 20, "X2Mod%")
  TextGadget(#GADGET_TextX2CoarseModFreq, 70, 400, 80, 20, "CoarseX2ModF")
  TextGadget(#GADGET_TextX2FineModFreq, 150, 400, 70, 20, "FineX2ModF")
  
  TextGadget(#GADGET_TextY2ModPercentage, 10, 480, 50, 20, "Y2Mod%")
  TextGadget(#GADGET_TextY2CoarseModFreq, 70, 480, 80, 20, "CoarseY2ModF")
  TextGadget(#GADGET_TextY2FineModFreq, 150, 480, 70, 20, "FineY2ModF")
  
  DrawTheBezier()
  
  If CreateStatusBar(0, WindowID(#WIN_MAIN))
    AddStatusBarField(100)
    AddStatusBarField(100)
    AddStatusBarField(30)
  EndIf
 
  AddKeyboardShortcut(#WIN_MAIN, #PB_Shortcut_Escape, 111)


  ;MIDI STUFF for NOTE ON/OFF DISPLAY
  If midiInOpen_(@hMi, InDev, @MidiInCallback(), 0, #CALLBACK_FUNCTION) = #MMSYSERR_NOERROR
    If midiInStart_(hMi) = #MMSYSERR_NOERROR
      If IsStatusBar(0) : StatusBarText(0, 0, "MIDI IN", #PB_StatusBar_Center) : EndIf
    Else
      If IsStatusBar(0) : StatusBarText(0, 0, "No MIDI IN", #PB_StatusBar_Center) : EndIf
    EndIf
  Else
    If IsStatusBar(0) : StatusBarText(0, 0, "No MIDI IN", #PB_StatusBar_Center) : EndIf
  EndIf
 
  ;END MIDI STUFF
 
 
  State = #Off
  Pa_StartStream(Stream)
 
  Define Quit.i
  Quit = #False
 
  Define Event.i
  Repeat
    Event = WaitWindowEvent()
    Select Event
      Case #PB_Event_Menu
        
        Select EventMenu()
        Case 1
        MessageRequester("About", "Quad-Klanger v1.01 " + Chr(169) + " Jim Singh" + Chr(13) + "Bezier Sound Synthesizer")
        Case 2
        ShowAboutWindow()
        EndSelect
       
        If EventMenu() = 111 : Quit = #True : EndIf
       
      Case #PB_Event_Gadget
        Select EventGadget()
           
          Case #MIDIDevice
            midiInStop_(hMi)
            midiInClose_(hMi)
            InDev = GetGadgetState(#MIDIDevice)
            midiInOpen_(@hMi, InDev, @MidiInCallback(), 0, #CALLBACK_FUNCTION)
            If midiInStart_(hMi) = #MMSYSERR_NOERROR
              If IsStatusBar(0) : StatusBarText(0, 0, "MIDI IN", #PB_StatusBar_Center) : EndIf
            Else
              If IsStatusBar(0) : StatusBarText(0, 0, "No MIDI IN", #PB_StatusBar_Center) : EndIf
            EndIf
           
          Case #Trackbar_FilterFreq
            cutoffo = GetGadgetState(#Trackbar_FilterFreq)
           
          Case #Trackbar_FilterReso
            rawresolution = GetGadgetState(#Trackbar_FilterReso)
           
          Case #Trackbar_Attack
            Attack = GetGadgetState(#Trackbar_Attack) / 1000
           
          Case #Trackbar_Decay
            Decay = GetGadgetState(#Trackbar_Decay) / 1000
           
          Case #Trackbar_Sustain
            Sustain = GetGadgetState(#Trackbar_Sustain) / 100
           
          Case #Trackbar_Release
            Release = GetGadgetState(#Trackbar_Release) / 1000
                        
              Case #GADGET_TrackVolume                
                TheVolume = GetGadgetState(#GADGET_TrackVolume)
                
              Case #GADGET_TrackDuration
                TheDuration = GetGadgetState(#GADGET_TrackDuration)
                
              Case #GADGET_TrackA
                ptA = GetGadgetState(#GADGET_TrackA)/100
                DrawTheBezier()
              Case #GADGET_TrackB
                ptB = GetGadgetState(#GADGET_TrackB)/100 
                DrawTheBezier()
              Case #GADGET_TrackC
                ptC = GetGadgetState(#GADGET_TrackC)/100
                DrawTheBezier()
              Case #GADGET_TrackD
                ptD = GetGadgetState(#GADGET_TrackD)/100
                DrawTheBezier()
              Case #GADGET_MODAMOUNTX1
                SetGadgetText(#GADGET_MODAMOUNTX1, Str(GetGadgetState(#GADGET_MODAMOUNTX1)))
              Case #GADGET_MODFREQCOARSEX1
                SetGadgetText(#GADGET_MODFREQCOARSEX1, Str(GetGadgetState(#GADGET_MODFREQCOARSEX1)))
              Case #GADGET_MODFREQFINEX1
                If GetGadgetState(#GADGET_MODFREQFINEX1) < 10
                SetGadgetText(#GADGET_MODFREQFINEX1, "." + "0" + Str(GetGadgetState(#GADGET_MODFREQFINEX1))) 
                Else  
                SetGadgetText(#GADGET_MODFREQFINEX1, "." + Str(GetGadgetState(#GADGET_MODFREQFINEX1)))
                EndIf  
                
              Case #GADGET_MODAMOUNTY1
                SetGadgetText(#GADGET_MODAMOUNTY1, Str(GetGadgetState(#GADGET_MODAMOUNTY1)))
              Case #GADGET_MODFREQCOARSEY1
                SetGadgetText(#GADGET_MODFREQCOARSEY1, Str(GetGadgetState(#GADGET_MODFREQCOARSEY1)))
              Case #GADGET_MODFREQFINEY1
                If GetGadgetState(#GADGET_MODFREQFINEY1)<10
                SetGadgetText(#GADGET_MODFREQFINEY1, "." + "0" + Str(GetGadgetState(#GADGET_MODFREQFINEY1)))  
                Else
                SetGadgetText(#GADGET_MODFREQFINEY1, "." + Str(GetGadgetState(#GADGET_MODFREQFINEY1)))
                EndIf
              
              Case #GADGET_MODAMOUNTX2
                SetGadgetText(#GADGET_MODAMOUNTX2, Str(GetGadgetState(#GADGET_MODAMOUNTX2)))
              Case #GADGET_MODFREQCOARSEX2
                SetGadgetText(#GADGET_MODFREQCOARSEX2, Str(GetGadgetState(#GADGET_MODFREQCOARSEX2)))
              Case #GADGET_MODFREQFINEX2
                If GetGadgetState(#GADGET_MODFREQFINEX2)<10
                SetGadgetText(#GADGET_MODFREQFINEX2, "." + "0" + Str(GetGadgetState(#GADGET_MODFREQFINEX2)))  
                Else
                SetGadgetText(#GADGET_MODFREQFINEX2, "." + Str(GetGadgetState(#GADGET_MODFREQFINEX2)))
                EndIf
              Case #GADGET_MODAMOUNTY2
                SetGadgetText(#GADGET_MODAMOUNTY2, Str(GetGadgetState(#GADGET_MODAMOUNTY2)))
              Case #GADGET_MODFREQCOARSEY2
                SetGadgetText(#GADGET_MODFREQCOARSEY2, Str(GetGadgetState(#GADGET_MODFREQCOARSEY2)))
              Case #GADGET_MODFREQFINEY2
                If GetGadgetState(#GADGET_MODFREQFINEY2)<10
                SetGadgetText(#GADGET_MODFREQFINEY2, "." + "0" + Str(GetGadgetState(#GADGET_MODFREQFINEY2)))  
                Else
                SetGadgetText(#GADGET_MODFREQFINEY2, "." + Str(GetGadgetState(#GADGET_MODFREQFINEY2)))
                EndIf
              Case #GADGET_ModDurationIncrement
                If GetGadgetState(#GADGET_ModDurationIncrement) = 1
                  SetGadgetText(#GADGET_ModDurationIncrement, "x1")
                  TrackBarGadget(#GADGET_TrackDuration, 460, 140, 20, 80, 100, 1000, #PB_TrackBar_Vertical)
                  SetGadgetState(#GADGET_TrackDuration,400)
                  TheDuration = 400
                ElseIf GetGadgetState(#GADGET_ModDurationIncrement) = 2
                  SetGadgetText(#GADGET_ModDurationIncrement, "x10")
                  TrackBarGadget(#GADGET_TrackDuration, 460, 140, 20, 80, 1000, 10000, #PB_TrackBar_Vertical)
                  SetGadgetState(#GADGET_TrackDuration,2000)
                  TheDuration = 2000
                EndIf  
                
              Case No2
                If CurveGadgetEvent(No2) = 1
                  If CurveGadgetGetState(No2, CurvePoint())
                  EndIf
                EndIf
                
              Case No3
                If CurveGadgetEvent(No3) = 1
                  If CurveGadgetGetState(No3, CurvePoint())
                  EndIf
                EndIf 
                
              Case No4
                If CurveGadgetEvent(No4) = 1
                  If CurveGadgetGetState(No4, CurvePoint())
                  EndIf
                EndIf
                
              Case No5
                If CurveGadgetEvent(No5) = 1
                  If CurveGadgetGetState(No5, CurvePoint())
                  EndIf
                EndIf   
        EndSelect
       
      Case #PB_Event_CloseWindow
        Quit = #True
    EndSelect
  Until Quit
 
EndIf


Procedure ShowAboutWindow()
  
  Protected AboutWindow, myEditorGadget, CloseButton 
  Protected Text$
  Protected Quit
  
  AboutWindow = OpenWindow(#PB_Any, #PB_Ignore, #PB_Ignore, 620, 373, "Quad-Klanger Help", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
  
 If AboutWindow
    
myEditorGadget = EditorGadget(#PB_Any, 5, 5, 610, 360, #PB_Editor_ReadOnly)
    
Text$ = Chr($d) + "The Quad-Klanger main window displays a single cycle of the basic waveform." + Chr($d)
Text$ + Chr($d) + "The X1, X2 sliders 'pull' the waveform along the X axis."
Text$ + Chr($d) + "The Y1, Y2 sliders pull' the waveform along the Y axis." + Chr($d)
Text$ + Chr($d) + "You can make each of the X1, X2, Y1, Y2 points 'wobble' as the Note plays by modulating each of them "
Text$ + Chr($d) + "independently, allowing a wide variety of harmonically rich sounds to be produced." + Chr($d)
Text$ + Chr($d) + "Let us take point X1 for example:" + Chr($d)
Text$ + Chr($d) + "The point will oscillate between where the X1 slider is currently set, to a level determined by X1Mod%."
Text$ + Chr($d) + "If X1Mod% = +80, the point will oscillate between where the X1 slider is currently set, to 80% of the way up"  
Text$ + Chr($d) + "to the top of the slider. If X1Mod% = -30, the point will oscillate between where the X1 slider is currently set,"
Text$ + Chr($d) + "to 30% of the way to the bottom of the slider. The point will oscillate at a frequency determined by"
Text$ + Chr($d) + "CoarseX1ModF and FineX1ModF. If Coarse X1ModF = 2 and FineX1ModF = .25, the point will oscillate at" 
Text$ + Chr($d) + "2.25 times the frequency of the Note being played on the keyboard. If Coarse X1ModF = 10 and" 
Text$ + Chr($d) + "FineX1ModF = 0, the point will oscillate at 10 times the frequency of the Note being played on the keyboard." 
Text$ + Chr($d) + "If Coarse X1ModF = 0 and FineX1ModF = .05, the point will oscillate at 0.05 times the frequency of the Note" 
Text$ + Chr($d) + "being played on the keyboard. Integer multiples of the Note Frequency set by CoarseX1ModF result in harmonic" 
Text$ + Chr($d) + "overtones being produced, whilst the fractional multiplier set by FineX1ModF results in inharmonic overtones." + Chr($d)
Text$ + Chr($d) + "You can make the point oscillation increase or decrease in amplitude over time as the Note plays, by moving "
Text$ + Chr($d) + "the 7 segments of the wobble amplitude curve for point X1 into your desired envelope shape, and the period "
Text$ + Chr($d) + "over which that envelope applies is determined by the Duration slider. The x1 setting for the Duration slider "
Text$ + Chr($d) + "constrains this period to a maximum of a couple of seconds, whilst the x10 setting allows the period to be "
Text$ + Chr($d) + "many seconds." + Chr($d)
Text$ + Chr($d) + "For example, if you draw the wobble amplitude curve as a straight line starting at lower left and ending at top " 
Text$ + Chr($d) + "right of the envelope window, and you leave the Duration slider in its default position, then when you press a "
Text$ + Chr($d) + "Note, the point oscillation will occur between where the X1 slider is currently set, and a level which linearly " 
Text$ + Chr($d) + "moves over a couple of seconds from X1 to the level determined by X1Mod%. This will change the harmonic " 
Text$ + Chr($d) + "content of the sound as the Note plays, giving a kind of 'filter sweeping' effect." + Chr($d)
Text$ + Chr($d) + "The sound produced by this engine is then passed through a master low pass filter, the frequency of which is set "
Text$ + Chr($d) + "by the F slider and the Resonance by the Res slider, giving you even greater sound shaping versatility, and then "
Text$ + Chr($d) + "through the level ADSR, which you set with the Amplitude EG sliders." + Chr($d)
Text$ + Chr($d) + "The Vol slider adjusts the overall level of the sound." + Chr($d)
Text$ + Chr($d) + "The MIDI devices available on your system which Quad-Klanger can use to receive Midi note events from your "
Text$ + Chr($d) + "virtual or external hardware keyboard, are shown in the Midi devices drop-down. Select the device you wish to use, "  
Text$ + Chr($d) + "and ensure your keyboard transmits through the same device. Quad-Klanger features a form of velocity-sensitivity "
Text$ + Chr($d) + "so that if your keyboard is velocity sensitive, the sound will play softer if you press the keys lightly, and louder "
Text$ + Chr($d) + "if you press the keys harder." 

SetGadgetText(myEditorGadget, Text$)
    
    Repeat 
      
      Select WaitWindowEvent()
          
        Case #PB_Event_Gadget :      If EventGadget()=CloseButton : Break : EndIf
        Case #PB_Event_CloseWindow : If EventWindow()=AboutWindow : Break : EndIf 
          
      EndSelect
      
    ForEver
    
    CloseWindow(AboutWindow)
    
 EndIf
  
EndProcedure


Pa_AbortStream(Stream)
While Pa_IsStreamActive(Stream) : WaitWindowEvent(1) : Wend
Pa_Terminate()

Re: Quad-Klanger released: A real-time Bezier sound synthesi

Posted: Wed Aug 01, 2012 11:17 pm
by IdeasVacuum
Perhaps the click does occur in the other apps but they have a filter to trap it.

Re: Quad-Klanger released: A real-time Bezier sound synthesi

Posted: Fri Aug 03, 2012 4:50 pm
by Zach
Could try asking the developers of some of these Workstations to see what they think too.

I wouldn't know anything about this, other than user-experience.

Re: Quad-Klanger released: A real-time Bezier sound synthesi

Posted: Sun Aug 05, 2012 8:37 am
by doctornash
IdeasVacuum, your ideas are hardly vacuous :) I implemented a couple of routines which seem go a long way towards eliminating the clicking, and I made use of your filtering idea in the approach.

We needed to solve the following problems:

a) Even when previous sound had already decayed to 0, press of a new note resulted in a click before the new sound played
b) If a new note was pressed before previous sound had decayed, a click resulted

To address these issues, this is what the routines do: Let's say that a current sound has amplitude = X when a new note is pressed. The first routine linearly decays the sound from amplitude X to amplitude 0 over 60 samples, and Low Pass filters the sound over that decay period. The second routine then over the next 100 samples exponentially increases the sound amplitude from 0 to whatever the set Attack level is at 100 samples, from which point the normal ADSR behaviour takes over.

Re the 60 and 100 sample period, I needed to make the period short enough (no more than a few milliseconds) so that a delay between a keypress and onset of the Attack would not be perceived, yet long enough for the sound to be force decayed to 0 and made to rise from 0 without a sharp discontinuity being perceived.
Image
Here's the v1.02 code with these routines added:

Code: Select all

;Quad-Klanger v102 a monophonic bezier-curve based sound synthesizer
;by doctornash
;midi procedures and some optimization by infratec


XIncludeFile "PortAudio.pb"
XIncludeFile "CurveGadget.pbi"

;for curvegadgets
Global Dim CurvePoint.POINT(1)
Global No2
Global No3
Global No4
Global No5
;end curvegadget

;initial points for single cycle of bezier waveform
Global ptA.f = 0.5
Global ptB.f = 0.06
Global ptC.f = 0.75
Global ptD.f = 0.95

#SampleRate = 44100

#PI2 = 6.28318530717958

Global BezierEnvelopeSamples.l
Global Pee.l
Global InitializeTheBezier.l=0

Global ModX1Freq.f
Global ModX2Freq.f
Global ModY1Freq.f
Global ModY2Freq.f

Global X1HOOKA.f
Global X2HOOKA.f
Global Y1HOOKA.f
Global Y2HOOKA.f

;for triangle wave modulation method of bezier points
Global NewX1Level.f
Global X1Sign.l
Global TheX1Level.f
Global UX1.f 
Global VX1.f
Global FinalCurveGadgetValX1.f

Global NewY1Level.f
Global Y1Sign.l
Global TheY1Level.f
Global UY1.f 
Global VY1.f
Global FinalCurveGadgetValY1.f

Global NewX2Level.f
Global X2Sign.l
Global TheX2Level.f
Global UX2.f 
Global VX2.f
Global FinalCurveGadgetValX2.f

Global NewY2Level.f
Global Y2Sign.l
Global TheY2Level.f
Global UY2.f 
Global VY2.f
Global FinalCurveGadgetValY2.f

Global TheVolume.f

Global Kata.s


Enumeration
  #WIN_MAIN
  #PlayPauseButton
  #MIDIDevice
  #Trackbar_FilterFreq
  #Trackbar_FilterReso
  #Trackbar_Attack
  #Trackbar_Decay
  #Trackbar_Sustain
  #Trackbar_Release
  #Text_FilterFreq
  #Text_FilterReso
  #Text_Attack
  #Text_Sustain
  #Text_Decay
  #Text_Release
  
  ;for the bezier:
  #GADGET_Canvas2
  #GADGET_TrackA
  #GADGET_TrackB
  #GADGET_TrackC
  #GADGET_TrackD
  #GADGET_TrackVolume
  #GADGET_TrackDuration
  #GADGET_WaveGrapher
  
  #GADGET_MODAMOUNTX1
  #GADGET_MODFREQCOARSEX1
  #GADGET_MODFREQFINEX1
  #GADGET_MODAMOUNTY1
  #GADGET_MODFREQCOARSEY1
  #GADGET_MODFREQFINEY1
  #GADGET_MODAMOUNTX2
  #GADGET_MODFREQCOARSEX2
  #GADGET_MODFREQFINEX2
  #GADGET_MODAMOUNTY2
  #GADGET_MODFREQCOARSEY2
  #GADGET_MODFREQFINEY2
  
  #GADGET_TextX1
  #GADGET_TextY1
  #GADGET_TextX2
  #GADGET_TextY2
  #GADGET_TextX1ModPercentage
  #GADGET_TextY1ModPercentage
  #GADGET_TextX2ModPercentage
  #GADGET_TextY2ModPercentage
  #GADGET_TextX1CoarseModFreq
  #GADGET_TextX1FineModFreq
  #GADGET_TextY1CoarseModFreq
  #GADGET_TextY1FineModFreq
  #GADGET_TextX2CoarseModFreq
  #GADGET_TextX2FineModFreq
  #GADGET_TextY2CoarseModFreq
  #GADGET_TextY2FineModFreq
  #GADGET_TextMasterVolume
  #GADGET_TextDuration
  #GADGET_ModDurationIncrement
  #GADGET_FrameAmpEG
  #GADGET_FrameFilter
EndEnumeration

Enumeration
  #Off
  #On
EndEnumeration


Global State.i

Global SampleCounter.l

Global ForcedDecayCounter.l=0
Global Groucho.d=0
Global InitializeSampleCounter.l=0

Global TheDuration.f=2000
Global ModAmountX1.f
Global ModAmountX2.f
Global ModAmountY1.f
Global ModAmountY2.f

;midi stuff
Global Chromatic$
Chromatic$="C C#D EbE F F#G AbA BbB C "
Global NoteVal.l
Global Freq.d
Global NoteCheck.i
Global foo.l
Global p.d
Global i.d

;ADSR stuff
Global Attack.d
Global Decay.d
Global Sustain.d
Global Release.d

Global AttackSamples.d
Global DecaySamples.d
Global SustainLevel.d
Global ReleaseSamples.d

Global ADSRLevel.d
Global UptoV.d ;the ADSR level we have reached when Release strikes
Global UptoP.l ;samplecount we have reached when the Release strikes


NoteCheck = 0

;filter stuff
Global cutoffo.d
Global t1o.d
Global t2o.d
Global reso.d
Global rawresolution.l

cutoffo = 1500
rawresolution = 50

Declare.f MoogFilter16(Uo.f)
Declare.f CubicBezier_ForDrawing(xval.f, aval.f, bval.f, cval.f, dval.f)
Declare.f CubicBezier(SampleNum.l, xval.f, aval.f, bval.f, cval.f, dval.f)
Declare.f slopeFromT(tee.f, Ai.f, Bi.f, Ci.f)
Declare.f xFromT(tee.f, Ai.f, Bi.f, Ci.f, Di.f)
Declare.f yFromT(tee.f, Ei.f, Fi.f, Gi.f, Hi.f)
Declare DrawTheBezier()
Declare.f CalculateTheWaveform(SamplePoint.l)
Declare ShowAboutWindow()


Procedure.i MIDI_GetInputDevices()
 
  Protected mic.MIDIINCAPS, Num.i, i.i
 
  Num = midiInGetNumDevs_()
  For i = 0 To Num - 1
    If midiInGetDevCaps_(i, @mic, SizeOf(MIDIINCAPS)) = 0
      AddGadgetItem(#MIDIDevice, -1, PeekS(@mic\szPname))
    EndIf
  Next i
 
  ProcedureReturn Num
 
EndProcedure


Procedure.s MIDI_Note(Note);returns note's name
   ProcedureReturn Trim(Mid(Chromatic$, (Note % 12) * 2 + 1, 2)) + Str(Note / 12)
EndProcedure


Procedure MidiInCallback(hMidiIn.i, wMsg.i, dwInstance.l, dwParam1.l, dwParam2.l)
 
  Protected Note.i, Velocity.i, Status.i
  Static KeyCounter.i
  
  Select wMsg    ; process MIDI in events
    Case #MIM_CLOSE
      KeyCounter = 0
      State = #Off
      kata = "Off"
    Case #MM_MIM_DATA
      Status = dwParam1 & $FF
      If Status = $90
        Note = (dwParam1 >> 8) & $FF
        Velocity = (dwParam1 >> 16) & $FF
        If Velocity
          Freq = Pow(2, (Note - 69) / 12) * 440
          ModX1Freq = (ValF(Str(GetGadgetState(#GADGET_MODFREQCOARSEX1)) + "." + Str(GetGadgetState(#GADGET_MODFREQFINEX1))))*Freq
          ModX2Freq = (ValF(Str(GetGadgetState(#GADGET_MODFREQCOARSEX2)) + "." + Str(GetGadgetState(#GADGET_MODFREQFINEX2))))*Freq
          ModY1Freq = (ValF(Str(GetGadgetState(#GADGET_MODFREQCOARSEY1)) + "." + Str(GetGadgetState(#GADGET_MODFREQFINEY1))))*Freq
          ModY2Freq = (ValF(Str(GetGadgetState(#GADGET_MODFREQCOARSEY2)) + "." + Str(GetGadgetState(#GADGET_MODFREQFINEY2))))*Freq
          If KeyCounter = 0
            State = #On
            kata = "On"
          EndIf
          ;If IsStatusBar(0) : StatusBarText(0, 1, MIDI_Note(Note) + " " + Str(Velocity), #PB_StatusBar_Center) : EndIf
          KeyCounter + 1
        Else
          If KeyCounter : KeyCounter - 1 : EndIf
          If KeyCounter = 0
          State = #Off
          kata = "Off"
          InitializeTheBezier = 0
          InitializeSampleCounter = 0
          EndIf
        EndIf
      EndIf 
      
      If Status = $80
        If KeyCounter : KeyCounter - 1 : EndIf
        If KeyCounter = 0
          State = #Off 
          kata = "Off"
          InitializeTheBezier = 0
          InitializeSampleCounter = 0
        EndIf
      EndIf
      
      If IsStatusBar(0) : StatusBarText(0, 1, kata, #PB_StatusBar_Center) : EndIf

  EndSelect
EndProcedure
;end midi stuff


Procedure Error(err)
  MessageRequester("PortAudio", PeekS(Pa_GetErrorText(err)))
  End
EndProcedure


 Procedure.f CalculateTheWaveform(SamplePoint.l)
   
  Protected N.l
  Protected NumSamplePoints.l
  Protected RawBezierVal.f
  
If Freq>0  
  NumSamplePoints = #SampleRate/Freq
    
    Pee = Pee+1
    If Pee = NumSamplePoints
      Pee = 0
    EndIf
        RawBezierVal = (200 - (CubicBezier(SamplePoint, Pee/NumSamplePoints, ptA, ptB, ptC, ptD)))/200
        If RawBezierVal = 0.5
          RawBezierVal = 0
        Else
          ;increasing the amplitude of the overall sound to give it an acceptable volume
          RawBezierVal = ((RawBezierVal-0.5)*TheVolume/100)
        EndIf  
EndIf       
ProcedureReturn RawBezierVal
     
EndProcedure  


Procedure NewX1ModMethod(SampleVal.l)
;even though this is called NewX1Method, it is for the modulation of ALL points!  
  
  Protected MuX1 
  Protected MuY1
  Protected MuX2
  Protected MuY2
  
  If SampleVal<=BezierEnvelopeSamples-1 
  MuX1 = VX1*CurveGadgetGetAttribute(No2, #CurveGadget_Y_Value_Scaled, SampleVal) / 100
  MuY1 = VY1*CurveGadgetGetAttribute(No3, #CurveGadget_Y_Value_Scaled, SampleVal) / 100
  MUX2 = VX2*CurveGadgetGetAttribute(No4, #CurveGadget_Y_Value_Scaled, SampleVal) / 100
  MuY2 = VY2*CurveGadgetGetAttribute(No5, #CurveGadget_Y_Value_Scaled, SampleVal) / 100
  Else
  MuX1 = VX1*FinalCurveGadgetValX1
  MuY1 = VY1*FinalCurveGadgetValY1 
  MuX2 = VX2*FinalCurveGadgetValX2 
  MuY2 = VY2*FinalCurveGadgetValY2 
  EndIf  
    
  
If ModX1Freq>0  
  If ModAmountX1>0   
    NewX1Level = NewX1Level+ X1Sign*((MuX1*2)/(#SampleRate/ModX1Freq))
    If NewX1Level>TheX1Level+MuX1
      NewX1Level = TheX1Level+MuX1
      X1Sign = -1
    ElseIf NewX1Level<TheX1Level
      NewX1Level = TheX1Level
      X1Sign = 1
    EndIf 
  Else
    NewX1Level = NewX1Level+ X1Sign*((MuX1*2)/(#SampleRate/ModX1Freq))
    If NewX1Level<TheX1Level-MuX1
      NewX1Level = TheX1Level-MuX1
      X1Sign = 1
    ElseIf NewX1Level>TheX1Level
      NewX1Level = TheX1Level
      X1Sign = -1
    EndIf 
  EndIf   
EndIf  

If ModY1Freq>0
  If ModAmountY1>0   
    NewY1Level = NewY1Level+ Y1Sign*((MuY1*2)/(#SampleRate/ModY1Freq))
    If NewY1Level>TheY1Level+MuY1
      NewY1Level = TheY1Level+MuY1
      Y1Sign = -1
    ElseIf NewY1Level<TheY1Level
      NewY1Level = TheY1Level
      Y1Sign = 1
    EndIf 
  Else
    NewY1Level = NewY1Level+ Y1Sign*((MuY1*2)/(#SampleRate/ModY1Freq))
    If NewY1Level<TheY1Level-MuY1
      NewY1Level = TheY1Level-MuY1
      Y1Sign = 1
    ElseIf NewY1Level>TheY1Level
      NewY1Level = TheY1Level
      Y1Sign = -1
    EndIf 
  EndIf   
EndIf  

If ModX2Freq>0
  If ModAmountX2>0   
    NewX2Level = NewX2Level+ X2Sign*((MuX2*2)/(#SampleRate/ModX2Freq))
    If NewX2Level>TheX2Level+MuX2
      NewX2Level = TheX2Level+MuX2
      X2Sign = -1
    ElseIf NewX2Level<TheX2Level
      NewX2Level = TheX2Level
      X2Sign = 1
    EndIf 
  Else
    NewX2Level = NewX2Level+ X2Sign*((MuX2*2)/(#SampleRate/ModX2Freq))
    If NewX2Level<TheX2Level-MuX2
      NewX2Level = TheX2Level-MuX2
      X2Sign = 1
    ElseIf NewX2Level>TheX2Level
      NewX2Level = TheX2Level
      X2Sign = -1
    EndIf 
  EndIf 
EndIf  

If ModY2Freq>0
  If ModAmountY2>0   
    NewY2Level = NewY2Level+ Y2Sign*((MuY2*2)/(#SampleRate/ModY2Freq))
    If NewY2Level>TheY2Level+MuY2
      NewY2Level = TheY2Level+MuY2
      Y2Sign = -1
    ElseIf NewY2Level<TheY2Level
      NewY2Level = TheY2Level
      Y2Sign = 1
    EndIf 
  Else
    NewY2Level = NewY2Level+ Y2Sign*((MuY2*2)/(#SampleRate/ModY2Freq))
    If NewY2Level<TheY2Level-MuY2
      NewY2Level = TheY2Level-MuY2
      Y2Sign = 1
    ElseIf NewY2Level>TheY2Level
      NewY2Level = TheY2Level
      Y2Sign = -1
    EndIf 
  EndIf 
EndIf  
  
  X1HOOKA = NewX1Level/100
  Y1HOOKA = NewY1Level/100
  X2HOOKA = NewX2Level/100
  Y2HOOKA = (NewY2Level+50)/100
     
EndProcedure  


Procedure DrawTheBezier()
 ;this is for graphing the single cycle wave only
  Protected N.i
  Dim BezierDrawArr(399)
  For N = 0 To 399
    BezierDrawArr(N) = CubicBezier_ForDrawing(N/400, ptA, ptB, ptC, ptD)
  Next N
     
  StartDrawing(CanvasOutput(#GADGET_Canvas2))
  Box(0, 0, 400, 200, $000000)
  For N = 0 To 398
    LineXY(N, BezierDrawArr(N), N+1, BezierDrawArr(N+1), $FFFFFF)
  Next N 
  StopDrawing()  

EndProcedure 


Procedure.f CubicBezier_ForDrawing(xval.f, aval.f, bval.f, cval.f, dval.f)
;this is for graphing the single cycle wave only
Protected A.f = 0
Protected B.f = 0
Protected C.f = 0
Protected D.f = 0
Protected E.f = 0
Protected F.f = 0
Protected G.f = 0
Protected H.f = 0
 
Protected y0a.f
Protected x0a.f
Protected y1a.f
Protected x1a.f
Protected y2a.f
Protected x2a.f
Protected y3a.f
Protected x3a.f
Protected currentt.f
Protected nRefinementIterations.l
Protected i.l = 0
Protected currentx.f
Protected currentslope.f
Protected CB.f

y0a = 0.5
x0a = 0
y1a = bval
x1a = aval
y2a = dval
x2a = cval
y3a = 0.5
x3a = 1

A = x3a - 3*x2a + 3*x1a - x0a
B = 3*x2a - 6*x1a + 3*x0a
C = 3*x1a - 3*x0a
D = x0a
E = y3a - 3*y2a + 3*y1a - y0a
F = 3*y2a - 6*y1a + 3*y0a       
G = 3*y1a - 3*y0a         
H = y0a

currentt = xval
nRefinementIterations = 5

For i = 0 To nRefinementIterations-1
currentx = xFromT (currentt, A,B,C,D)
currentslope = slopeFromT (currentt, A,B,C)
currentt = currentt - ((currentx - xval)*(currentslope))
Next i

If currentt>1
currentt = 1
ElseIf currentt<0
currentt=0
EndIf

CB = yFromT(currentt, E,F,G,H)

cb = cb*200
If cb>200
cb = 200
EndIf
cb = 200-cb

ProcedureReturn CB

EndProcedure

Procedure.f CubicBezier(SampleNum.l, xval.f, aval.f, bval.f, cval.f, dval.f)
;this is the Bezier calculator used in the sound production 
Protected A.f = 0
Protected B.f = 0
Protected C.f = 0
Protected D.f = 0
Protected E.f = 0
Protected F.f = 0
Protected G.f = 0
Protected H.f = 0
 
Protected y0a.f
Protected x0a.f
Protected y1a.f
Protected x1a.f
Protected y2a.f
Protected x2a.f
Protected y3a.f
Protected x3a.f
Protected currentt.f
Protected nRefinementIterations.l
Protected i.l = 0
Protected currentx.f
Protected currentslope.f
Protected CB.f

y0a = 0.5
x0a = 0
y1a = bval
x1a = aval
y2a = dval
x2a = cval
y3a = 0.5
x3a = 1

NewX1ModMethod(SampleNum)
x1a = X1HOOKA
y1a = Y1HOOKA
x2a = X2HOOKA
y2a = Y2HOOKA

A = x3a - 3*x2a + 3*x1a - x0a
B = 3*x2a - 6*x1a + 3*x0a
C = 3*x1a - 3*x0a
D = x0a
E = y3a - 3*y2a + 3*y1a - y0a
F = 3*y2a - 6*y1a + 3*y0a       
G = 3*y1a - 3*y0a         
H = y0a

currentt = xval
nRefinementIterations = 5

For i = 0 To nRefinementIterations-1
currentx = xFromT (currentt, A,B,C,D)
currentslope = slopeFromT (currentt, A,B,C)
currentt = currentt - ((currentx - xval)*(currentslope))
Next i

If currentt>1
currentt = 1
ElseIf currentt<0
currentt=0
EndIf

CB = yFromT(currentt, E,F,G,H)

cb = cb*200
If cb>200
cb = 200
EndIf
cb = 200-cb

ProcedureReturn CB

EndProcedure


Procedure.f slopeFromT(tee.f, Ai.f, Bi.f, Ci.f)
Protected dtdx.f
dtdx = 1/(3*Ai*tee*tee + 2*Bi*tee + Ci)
ProcedureReturn dtdx
EndProcedure


Procedure.f xFromT(tee.f, Ai.f, Bi.f, Ci.f, Di.f)
Protected ex.f 
ex = Ai*(tee*tee*tee) + Bi*(tee*tee) + Ci*tee + Di
ProcedureReturn ex
EndProcedure

Procedure.f yFromT(tee.f, Ei.f, Fi.f, Gi.f, Hi.f)
Protected yi.f
yi = Ei*(tee*tee*tee) + Fi*(tee*tee) + Gi*tee + Hi
ProcedureReturn yi
EndProcedure

Global Stream

Procedure.f MoogFilter16(Uo.f)
 ;well, a low pass filter, anyway :-) 
  Static.d Po, Fo, Ko, ro, Xo, t1o, t2o
  
  Static.d Y1o, y2o, y3o, y4o
  Static.d oldxo, oldy1o, oldy2o, oldy3o
  
  Static.i OldCutoffo
  Static.l Oldrawresolution
  
  If OldCutoffo <> cutoffo
    Fo = 2 * cutoffo / #SampleRate
    Po = Fo * (1.8 - 0.8 * Fo)
    Ko = Po + Po - 1
    t1o = (1 - Po) * 1.386249
    t2o = 12 + t1o * t1o
    OldCutoffo = cutoffo
  EndIf
  
  If Oldrawresolution <> rawresolution
    ro = (rawresolution / 100) * (t2o + 6 * t1o) / (t2o - 6 * t1o)
    Oldrawresolution = rawresolution
  EndIf
  
  Xo = Uo - ro * y4o
  
  ;Four cascaded onepole filters (bilinear transform)
  Y1o = (Xo + oldxo) * Po - Ko * Y1o
  y2o = (Y1o + oldy1o) * Po - Ko * y2o
  y3o = (y2o + oldy2o) * Po - Ko * y3o
  y4o = (y3o + oldy3o) * Po - Ko * y4o
  
  ;Clipper band limited sigmoid
  y4o = y4o - (y4o * y4o * y4o) / 6
  
  oldxo = Xo
  oldy1o = Y1o
  oldy2o = y2o
  oldy3o = y3o
  
  ProcedureReturn y4o
 
EndProcedure


Procedure LevelADSR(SampleTracker.l)
  
If SampleTracker>2147483640
SampleCounter = 0
UptoV = 0
EndIf  
 
If State = #Off
    If (ReleaseSamples+UptoP-SampleTracker)>=0
      ADSRLevel = (UptoV*(ReleaseSamples+UptoP-SampleTracker))/ReleaseSamples  
    Else
      ADSRLevel = 0
    EndIf 
    Groucho = ADSRLevel
  If IsStatusBar(0)
  EndIf
EndIf
 
 If State = #On
   If InitializeTheBezier = 0
     InitializeTheBezier = 1
     ForcedDecayCounter = -1
      Pee = -1
      BezierEnvelopeSamples = (TheDuration/1000)*#SampleRate
      CurveGadgetSetAttribute(No2, #CurveGadget_X_Maximum, BezierEnvelopeSamples-1)
      CurveGadgetSetAttribute(No2, #CurveGadget_Y_Maximum, 100)
 
      CurveGadgetSetAttribute(No3, #CurveGadget_X_Maximum, BezierEnvelopeSamples-1)
      CurveGadgetSetAttribute(No3, #CurveGadget_Y_Maximum, 100)
 
      CurveGadgetSetAttribute(No4, #CurveGadget_X_Maximum, BezierEnvelopeSamples-1)
      CurveGadgetSetAttribute(No4, #CurveGadget_Y_Maximum, 100)
 
      CurveGadgetSetAttribute(No5, #CurveGadget_X_Maximum, BezierEnvelopeSamples-1)
      CurveGadgetSetAttribute(No5, #CurveGadget_Y_Maximum, 100)
      
      ModAmountX1 = GetGadgetState(#GADGET_MODAMOUNTX1)
      ModAmountX2 = GetGadgetState(#GADGET_MODAMOUNTX2)
      ModAmountY1 = GetGadgetState(#GADGET_MODAMOUNTY1)
      ModAmountY2 = GetGadgetState(#GADGET_MODAMOUNTY2)
      
      TheX1Level = GetGadgetState(#GADGET_TrackA)
      NewX1Level = TheX1Level 
      UX1 = ModAmountX1/100
      If ModAmountX1>0
      X1Sign = 1
      VX1 = UX1*(99-TheX1Level)
      Else
      X1Sign = -1
      VX1 = -1*UX1*(TheX1Level-1)
      EndIf
      FinalCurveGadgetValX1 = CurveGadgetGetAttribute(No2, #CurveGadget_Y_Value_Scaled, BezierEnvelopeSamples-1)/100      
      
      TheY1Level = GetGadgetState(#GADGET_TrackB)
      NewY1Level = TheY1Level
      UY1 = ModAmountY1/100
      If ModAmountY1>0
      Y1Sign = 1 
      VY1 = UY1*(49-TheY1Level)
      Else
      Y1Sign = -1
      VY1 = -1*UY1*(TheY1Level-1)
      EndIf
      FinalCurveGadgetValY1 = CurveGadgetGetAttribute(No3, #CurveGadget_Y_Value_Scaled, BezierEnvelopeSamples-1)/100
      
      
      TheX2Level = GetGadgetState(#GADGET_TrackC)
      NewX2Level = TheX2Level
      UX2 = ModAmountX2/100
      If ModAmountX2>0
      X2Sign = 1 
      VX2 = UX2*(99-TheX2Level)
      Else
      X2Sign = -1
      VX2 = -1*UX2*(TheX2Level-1)
      EndIf
      FinalCurveGadgetValX2 = CurveGadgetGetAttribute(No4, #CurveGadget_Y_Value_Scaled, BezierEnvelopeSamples-1)/100      
      
      TheY2Level = GetGadgetState(#GADGET_TrackD)-50
      NewY2Level = TheY2Level
      UY2 = ModAmountY2/100
      If ModAmountY2>0
      Y2Sign = 1 
      VY2 = UY2*(49-TheY2Level)
      Else
      Y2Sign = -1
      VY2 = -1*UY2*(TheY2Level-1)
      EndIf
      FinalCurveGadgetValY2 = CurveGadgetGetAttribute(No5, #CurveGadget_Y_Value_Scaled, BezierEnvelopeSamples-1) / 100
      
      Attack = GetGadgetState(#Trackbar_Attack) / 1000
      Decay = GetGadgetState(#Trackbar_Decay) / 1000
      Sustain = GetGadgetState(#Trackbar_Sustain) / 100
      Release = GetGadgetState(#Trackbar_Release) / 1000
      
      AttackSamples = Attack*#SampleRate
      DecaySamples = Decay*#SampleRate
      SustainLevel = Sustain
      ReleaseSamples = Release*#SampleRate
            
    EndIf
    
    ForcedDecayCounter = ForcedDecayCounter+1
    If ForcedDecayCounter<=60
      ADSRLevel = (1-Pow((ForcedDecayCounter/60),1))*Groucho
      cutoffo = 250
    Else
      ;got to set the following only the FIRST TIME ForcedDecayCounter increments above 60 samples:
      If InitializeSampleCounter = 0
      InitializeSampleCounter = 1  
      SampleCounter = 0
      SampleTracker = 0
      cutoffo = GetGadgetState(#Trackbar_FilterFreq)
      EndIf
      ;let us calculate the actual ADSR level now shall we
          If SampleTracker<=AttackSamples
            ADSRLevel = SampleTracker/AttackSamples
          ElseIf SampleTracker>AttackSamples And SampleTracker<=(AttackSamples+DecaySamples)
            ADSRLevel = SustainLevel + (((AttackSamples+DecaySamples-SampleTracker)*(1-SustainLevel))/DecaySamples)
          ElseIf SampleTracker>(AttackSamples+DecaySamples)
            ADSRLevel = SustainLevel
          EndIf
    EndIf      
    UptoV = ADSRLevel
    UptoP = SampleTracker
    
EndIf   

EndProcedure


ProcedureC PaStreamCallback(*in, *output.Float, frameCount, *timeInfo.PaStreamCallbackTimeInfo, statusFlags, *userData)
  
  While frameCount
   LevelADSR(SampleCounter)
   If SampleCounter<100
     *output\f = MoogFilter16(CalculateTheWaveform(SampleCounter))*ADSRLevel*Pow((SampleCounter/100),5)
   Else
     *output\f = MoogFilter16(CalculateTheWaveform(SampleCounter))*ADSRLevel
   EndIf  
   SampleCounter = SampleCounter+1  
   *output + 4
   frameCount - 1
Wend
 
EndProcedure


Define err.i

err = Pa_Initialize()
If err <> #paNoError : Error(err) : EndIf

Define op.PaStreamParameters
op\channelCount = 1
op\device = Pa_GetDefaultOutputDevice()
op\sampleFormat = #paFloat32
op\suggestedLatency = 60/1000


;err = Pa_OpenStream(@Stream, #Null, @op, #SampleRate, #SampleRate / 100, #paNoFlag, @PaStreamCallback(), 0)
err = Pa_OpenStream(@Stream, #Null, @op, #SampleRate, #paframesperbufferunspecified, #paNoFlag, @PaStreamCallback(), 0)
If err <> #paNoError : Error(err) : EndIf

Define Indev.i
InDev = 0

Define hMi.i
hMi = 0

If OpenWindow(#WIN_MAIN, 0, 0, 535, 590, "Quad-Klanger", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
  
    If CreateMenu(0, WindowID(#WIN_MAIN))    ; menu creation starts....
      MenuTitle("?")
      MenuItem(1, "About")
      MenuItem(2, "Help")
    EndIf
  
  Frame3DGadget(#GADGET_FrameAmpEG, 400,  230, 120, 130, "Amplitude EG")
  Frame3DGadget(#GADGET_FrameFilter, 400,  370, 120, 130, "Filter")

 
  ComboBoxGadget(#MIDIDevice, 390, 520, 140, 20)
  If MIDI_GetInputDevices()
    SetGadgetState(#MIDIDevice, 0)
    InDev = 0
  Else
    InDev = -1
  EndIf
  
  No2 = CurveGadget(#PB_Any, 220, 240, 160, 70, 8, $ffffff, $000000)
  CurveGadgetSetAttribute(No2, #CurveGadget_PointRadius, 3)
  CurveGadgetSetAttribute(No2, #CurveGadget_CatchRadius, 10)
  CurveGadgetSetAttribute(No2, #CurveGadget_X_Maximum, 380)
  CurveGadgetSetAttribute(No2, #CurveGadget_Y_Maximum, 10000)
  
  No3 = CurveGadget(#PB_Any, 220, 320, 160, 70, 8, $ffffff, $000000)
  CurveGadgetSetAttribute(No3, #CurveGadget_PointRadius, 3)
  CurveGadgetSetAttribute(No3, #CurveGadget_CatchRadius, 10)
  CurveGadgetSetAttribute(No3, #CurveGadget_X_Maximum, 380)
  CurveGadgetSetAttribute(No3, #CurveGadget_Y_Maximum, 10000)
  
  No4 = CurveGadget(#PB_Any, 220, 400, 160, 70, 8, $ffffff, $000000)
  CurveGadgetSetAttribute(No4, #CurveGadget_PointRadius, 3)
  CurveGadgetSetAttribute(No4, #CurveGadget_CatchRadius, 10)
  CurveGadgetSetAttribute(No4, #CurveGadget_X_Maximum, 380)
  CurveGadgetSetAttribute(No4, #CurveGadget_Y_Maximum, 10000)
  
  No5 = CurveGadget(#PB_Any, 220, 480, 160, 70, 8, $ffffff, $000000)
  CurveGadgetSetAttribute(No5, #CurveGadget_PointRadius, 3)
  CurveGadgetSetAttribute(No5, #CurveGadget_CatchRadius, 10)
  CurveGadgetSetAttribute(No5, #CurveGadget_X_Maximum, 380)
  CurveGadgetSetAttribute(No5, #CurveGadget_Y_Maximum, 10000)
  
  CanvasGadget(#GADGET_Canvas2, 10, 10, 400, 200, #PB_Canvas_ClipMouse)
  StartDrawing(CanvasOutput(#GADGET_Canvas2))
  Box(0, 0, 400, 200, $000000)
  StopDrawing()
 
  TrackBarGadget(#Trackbar_FilterFreq, 420, 400, 20, 80, 20, 5000, #PB_TrackBar_Vertical)
  TrackBarGadget(#Trackbar_FilterReso, 460, 400, 20, 80, 0, 100, #PB_TrackBar_Vertical)
  SetGadgetState(#Trackbar_FilterFreq, 1500)
  SetGadgetState(#Trackbar_FilterReso, 50)
 
  TrackBarGadget(#Trackbar_Attack, 420, 260, 20, 80, 1, 2000, #PB_TrackBar_Vertical)
  TrackBarGadget(#Trackbar_Decay, 440, 260, 20, 80, 1, 5000, #PB_TrackBar_Vertical)
  TrackBarGadget(#Trackbar_Sustain, 460, 260, 20, 80, 0, 100, #PB_TrackBar_Vertical)
  SetGadgetState(#Trackbar_Sustain, 50)

  TrackBarGadget(#Trackbar_Release, 480, 260, 20, 80, 1, 5000, #PB_TrackBar_Vertical)
  SetGadgetState(#Trackbar_Attack, 1)
  SetGadgetState(#Trackbar_Decay, 1000)
  SetGadgetState(#Trackbar_Release, 1500)
 
  TextGadget(#Text_Attack, 425, 247, 10, 20, "A")
  TextGadget(#Text_Decay, 445, 247, 10, 20, "D")
  TextGadget(#Text_Sustain, 465, 247, 10, 20, "S")
  TextGadget(#Text_Release, 485, 247, 10, 20, "R")
  TextGadget(#Text_FilterFreq, 425, 387, 10, 20, "F")
  TextGadget(#Text_FilterReso, 455, 387, 20, 20, "Res")
    
  TrackBarGadget(#GADGET_TrackVolume, 420, 140, 20, 80, 0, 1500, #PB_TrackBar_Vertical)
  SetGadgetState(#GADGET_TrackVolume,400)
  TheVolume = 400
 
  TrackBarGadget(#GADGET_TrackDuration, 460, 140, 20, 80, 1000, 10000, #PB_TrackBar_Vertical)
  SetGadgetState(#GADGET_TrackDuration,2000)
  TheDuration = 2000
  
  SpinGadget(#GADGET_ModDurationIncrement, 480, 170, 40, 25, 1, 2)
  SetGadgetState (#GADGET_ModDurationIncrement, 2) 
  SetGadgetText(#GADGET_ModDurationIncrement, "x10")   ; set initial value
 
  TrackBarGadget(#GADGET_TrackA, 420, 30, 20, 80, 1, 99, #PB_TrackBar_Vertical)
  SetGadgetState(#GADGET_TrackA, 50)
  TrackBarGadget(#GADGET_TrackB, 440, 30, 20, 80, 0, 49, #PB_TrackBar_Vertical)
  SetGadgetState(#GADGET_TrackB, 5)
  TrackBarGadget(#GADGET_TrackC, 460, 30, 20, 80, 1, 99, #PB_TrackBar_Vertical)
  SetGadgetState(#GADGET_TrackC, 75)
  TrackBarGadget(#GADGET_TrackD, 480, 30, 20, 80, 50, 99, #PB_TrackBar_Vertical)
  SetGadgetState(#GADGET_TrackD, 95)
  
  SpinGadget(#GADGET_MODAMOUNTX1, 10, 260, 50, 25, -100, 100)
  SetGadgetState (#GADGET_MODAMOUNTX1, 0) : SetGadgetText(#GADGET_MODAMOUNTX1, "0")   ; set initial value
  SpinGadget(#GADGET_MODFREQCOARSEX1, 80, 260, 50, 25, 0, 15)
  SetGadgetState (#GADGET_MODFREQCOARSEX1, 0) : SetGadgetText(#GADGET_MODFREQCOARSEX1, "0")   ; set initial value
  SpinGadget(#GADGET_MODFREQFINEX1, 150, 260, 50, 25, 0, 99)
  SetGadgetState (#GADGET_MODFREQFINEX1, 0) : SetGadgetText(#GADGET_MODFREQFINEX1, ".00")   ; set initial value
  
  SpinGadget(#GADGET_MODAMOUNTY1, 10, 340, 50, 25, -100, 100)
  SetGadgetState (#GADGET_MODAMOUNTY1, 0) : SetGadgetText(#GADGET_MODAMOUNTY1, "0")   ; set initial value
  SpinGadget(#GADGET_MODFREQCOARSEY1, 80, 340, 50, 25, 0, 15)
  SetGadgetState (#GADGET_MODFREQCOARSEY1, 0) : SetGadgetText(#GADGET_MODFREQCOARSEY1, "0")   ; set initial value
  SpinGadget(#GADGET_MODFREQFINEY1, 150, 340, 50, 25, 0, 99)
  SetGadgetState (#GADGET_MODFREQFINEY1, 0) : SetGadgetText(#GADGET_MODFREQFINEY1, ".00")   ; set initial value
  
  SpinGadget(#GADGET_MODAMOUNTX2, 10, 420, 50, 25, -100, 100)
  SetGadgetState (#GADGET_MODAMOUNTX2, 0) : SetGadgetText(#GADGET_MODAMOUNTX2, "0")   ; set initial value
  SpinGadget(#GADGET_MODFREQCOARSEX2, 80, 420, 50, 25, 0, 15)
  SetGadgetState (#GADGET_MODFREQCOARSEX2, 0) : SetGadgetText(#GADGET_MODFREQCOARSEX2, "0")   ; set initial value
  SpinGadget(#GADGET_MODFREQFINEX2, 150, 420, 50, 25, 0, 99)
  SetGadgetState (#GADGET_MODFREQFINEX2, 0) : SetGadgetText(#GADGET_MODFREQFINEX2, ".00")   ; set initial value
  
  SpinGadget(#GADGET_MODAMOUNTY2, 10, 500, 50, 25, -100, 100)
  SetGadgetState (#GADGET_MODAMOUNTY2, 0) : SetGadgetText(#GADGET_MODAMOUNTY2, "0")   ; set initial value
  SpinGadget(#GADGET_MODFREQCOARSEY2, 80, 500, 50, 25, 0, 15)
  SetGadgetState (#GADGET_MODFREQCOARSEY2, 0) : SetGadgetText(#GADGET_MODFREQCOARSEY2, "0")   ; set initial value
  SpinGadget(#GADGET_MODFREQFINEY2, 150, 500, 50, 25, 0, 99)
  SetGadgetState (#GADGET_MODFREQFINEY2, 0) : SetGadgetText(#GADGET_MODFREQFINEY2, ".00")   ; set initial value
  
  TextGadget(#GADGET_TextX1, 420, 10, 20, 20, "X1")
  TextGadget(#GADGET_TextY1, 440, 10, 20, 20, "Y1")
  TextGadget(#GADGET_TextX2, 460, 10, 20, 20, "X2")
  TextGadget(#GADGET_TextY2, 480, 10, 20, 20, "Y2")
  TextGadget(#GADGET_TextMasterVolume, 420, 120, 30, 20, "Vol")
  TextGadget(#GADGET_TextDuration, 450, 120, 50, 20, "Duration")
  
  TextGadget(#GADGET_TextX1ModPercentage, 10, 240, 50, 20, "X1Mod%")
  TextGadget(#GADGET_TextX1CoarseModFreq, 70, 240, 80, 20, "CoarseX1ModF")
  TextGadget(#GADGET_TextX1FineModFreq, 150, 240, 70, 20, "FineX1ModF")
  
  TextGadget(#GADGET_TextY1ModPercentage, 10, 320, 50, 20, "Y1Mod%")
  TextGadget(#GADGET_TextY1CoarseModFreq, 70, 320, 80, 20, "CoarseY1ModF")
  TextGadget(#GADGET_TextY1FineModFreq, 150, 320, 70, 20, "FineY1ModF")
  
  TextGadget(#GADGET_TextX2ModPercentage, 10, 400, 50, 20, "X2Mod%")
  TextGadget(#GADGET_TextX2CoarseModFreq, 70, 400, 80, 20, "CoarseX2ModF")
  TextGadget(#GADGET_TextX2FineModFreq, 150, 400, 70, 20, "FineX2ModF")
  
  TextGadget(#GADGET_TextY2ModPercentage, 10, 480, 50, 20, "Y2Mod%")
  TextGadget(#GADGET_TextY2CoarseModFreq, 70, 480, 80, 20, "CoarseY2ModF")
  TextGadget(#GADGET_TextY2FineModFreq, 150, 480, 70, 20, "FineY2ModF")
  
  DrawTheBezier()
  
  If CreateStatusBar(0, WindowID(#WIN_MAIN))
    AddStatusBarField(100)
    AddStatusBarField(100)
    AddStatusBarField(30)
  EndIf
 
  AddKeyboardShortcut(#WIN_MAIN, #PB_Shortcut_Escape, 111)


  ;MIDI STUFF for NOTE ON/OFF DISPLAY
  If midiInOpen_(@hMi, InDev, @MidiInCallback(), 0, #CALLBACK_FUNCTION) = #MMSYSERR_NOERROR
    If midiInStart_(hMi) = #MMSYSERR_NOERROR
      If IsStatusBar(0) : StatusBarText(0, 0, "MIDI IN", #PB_StatusBar_Center) : EndIf
    Else
      If IsStatusBar(0) : StatusBarText(0, 0, "No MIDI IN", #PB_StatusBar_Center) : EndIf
    EndIf
  Else
    If IsStatusBar(0) : StatusBarText(0, 0, "No MIDI IN", #PB_StatusBar_Center) : EndIf
  EndIf
 
  ;END MIDI STUFF
 
 
  State = #Off
  Pa_StartStream(Stream)
 
  Define Quit.i
  Quit = #False
 
  Define Event.i
  Repeat
    Event = WaitWindowEvent()
    Select Event
      Case #PB_Event_Menu
        
        Select EventMenu()
        Case 1
        MessageRequester("About", "Quad-Klanger v1.02 " + Chr(169) + " Jim Singh" + Chr(13) + "Bezier Sound Synthesizer")
        Case 2
        ShowAboutWindow()
        EndSelect
       
        If EventMenu() = 111 : Quit = #True : EndIf
       
      Case #PB_Event_Gadget
        Select EventGadget()
           
          Case #MIDIDevice
            midiInStop_(hMi)
            midiInClose_(hMi)
            InDev = GetGadgetState(#MIDIDevice)
            midiInOpen_(@hMi, InDev, @MidiInCallback(), 0, #CALLBACK_FUNCTION)
            If midiInStart_(hMi) = #MMSYSERR_NOERROR
              If IsStatusBar(0) : StatusBarText(0, 0, "MIDI IN", #PB_StatusBar_Center) : EndIf
            Else
              If IsStatusBar(0) : StatusBarText(0, 0, "No MIDI IN", #PB_StatusBar_Center) : EndIf
            EndIf
           
          Case #Trackbar_FilterFreq
            cutoffo = GetGadgetState(#Trackbar_FilterFreq)
           
          Case #Trackbar_FilterReso
            rawresolution = GetGadgetState(#Trackbar_FilterReso)
           
          Case #Trackbar_Attack
            Attack = GetGadgetState(#Trackbar_Attack) / 1000
           
          Case #Trackbar_Decay
            Decay = GetGadgetState(#Trackbar_Decay) / 1000
           
          Case #Trackbar_Sustain
            Sustain = GetGadgetState(#Trackbar_Sustain) / 100
           
          Case #Trackbar_Release
            Release = GetGadgetState(#Trackbar_Release) / 1000
                        
              Case #GADGET_TrackVolume                
                TheVolume = GetGadgetState(#GADGET_TrackVolume)
                
              Case #GADGET_TrackDuration
                TheDuration = GetGadgetState(#GADGET_TrackDuration)
                
              Case #GADGET_TrackA
                ptA = GetGadgetState(#GADGET_TrackA)/100
                DrawTheBezier()
              Case #GADGET_TrackB
                ptB = GetGadgetState(#GADGET_TrackB)/100 
                DrawTheBezier()
              Case #GADGET_TrackC
                ptC = GetGadgetState(#GADGET_TrackC)/100
                DrawTheBezier()
              Case #GADGET_TrackD
                ptD = GetGadgetState(#GADGET_TrackD)/100
                DrawTheBezier()
              Case #GADGET_MODAMOUNTX1
                SetGadgetText(#GADGET_MODAMOUNTX1, Str(GetGadgetState(#GADGET_MODAMOUNTX1)))
              Case #GADGET_MODFREQCOARSEX1
                SetGadgetText(#GADGET_MODFREQCOARSEX1, Str(GetGadgetState(#GADGET_MODFREQCOARSEX1)))
              Case #GADGET_MODFREQFINEX1
                If GetGadgetState(#GADGET_MODFREQFINEX1) < 10
                SetGadgetText(#GADGET_MODFREQFINEX1, "." + "0" + Str(GetGadgetState(#GADGET_MODFREQFINEX1))) 
                Else  
                SetGadgetText(#GADGET_MODFREQFINEX1, "." + Str(GetGadgetState(#GADGET_MODFREQFINEX1)))
                EndIf  
                
              Case #GADGET_MODAMOUNTY1
                SetGadgetText(#GADGET_MODAMOUNTY1, Str(GetGadgetState(#GADGET_MODAMOUNTY1)))
              Case #GADGET_MODFREQCOARSEY1
                SetGadgetText(#GADGET_MODFREQCOARSEY1, Str(GetGadgetState(#GADGET_MODFREQCOARSEY1)))
              Case #GADGET_MODFREQFINEY1
                If GetGadgetState(#GADGET_MODFREQFINEY1)<10
                SetGadgetText(#GADGET_MODFREQFINEY1, "." + "0" + Str(GetGadgetState(#GADGET_MODFREQFINEY1)))  
                Else
                SetGadgetText(#GADGET_MODFREQFINEY1, "." + Str(GetGadgetState(#GADGET_MODFREQFINEY1)))
                EndIf
              
              Case #GADGET_MODAMOUNTX2
                SetGadgetText(#GADGET_MODAMOUNTX2, Str(GetGadgetState(#GADGET_MODAMOUNTX2)))
              Case #GADGET_MODFREQCOARSEX2
                SetGadgetText(#GADGET_MODFREQCOARSEX2, Str(GetGadgetState(#GADGET_MODFREQCOARSEX2)))
              Case #GADGET_MODFREQFINEX2
                If GetGadgetState(#GADGET_MODFREQFINEX2)<10
                SetGadgetText(#GADGET_MODFREQFINEX2, "." + "0" + Str(GetGadgetState(#GADGET_MODFREQFINEX2)))  
                Else
                SetGadgetText(#GADGET_MODFREQFINEX2, "." + Str(GetGadgetState(#GADGET_MODFREQFINEX2)))
                EndIf
              Case #GADGET_MODAMOUNTY2
                SetGadgetText(#GADGET_MODAMOUNTY2, Str(GetGadgetState(#GADGET_MODAMOUNTY2)))
              Case #GADGET_MODFREQCOARSEY2
                SetGadgetText(#GADGET_MODFREQCOARSEY2, Str(GetGadgetState(#GADGET_MODFREQCOARSEY2)))
              Case #GADGET_MODFREQFINEY2
                If GetGadgetState(#GADGET_MODFREQFINEY2)<10
                SetGadgetText(#GADGET_MODFREQFINEY2, "." + "0" + Str(GetGadgetState(#GADGET_MODFREQFINEY2)))  
                Else
                SetGadgetText(#GADGET_MODFREQFINEY2, "." + Str(GetGadgetState(#GADGET_MODFREQFINEY2)))
                EndIf
              Case #GADGET_ModDurationIncrement
                If GetGadgetState(#GADGET_ModDurationIncrement) = 1
                  SetGadgetText(#GADGET_ModDurationIncrement, "x1")
                  TrackBarGadget(#GADGET_TrackDuration, 460, 140, 20, 80, 100, 1000, #PB_TrackBar_Vertical)
                  SetGadgetState(#GADGET_TrackDuration,400)
                  TheDuration = 400
                ElseIf GetGadgetState(#GADGET_ModDurationIncrement) = 2
                  SetGadgetText(#GADGET_ModDurationIncrement, "x10")
                  TrackBarGadget(#GADGET_TrackDuration, 460, 140, 20, 80, 1000, 10000, #PB_TrackBar_Vertical)
                  SetGadgetState(#GADGET_TrackDuration,2000)
                  TheDuration = 2000
                EndIf  
                
              Case No2
                If CurveGadgetEvent(No2) = 1
                  If CurveGadgetGetState(No2, CurvePoint())
                  EndIf
                EndIf
                
              Case No3
                If CurveGadgetEvent(No3) = 1
                  If CurveGadgetGetState(No3, CurvePoint())
                  EndIf
                EndIf 
                
              Case No4
                If CurveGadgetEvent(No4) = 1
                  If CurveGadgetGetState(No4, CurvePoint())
                  EndIf
                EndIf
                
              Case No5
                If CurveGadgetEvent(No5) = 1
                  If CurveGadgetGetState(No5, CurvePoint())
                  EndIf
                EndIf   
        EndSelect
       
      Case #PB_Event_CloseWindow
        Quit = #True
    EndSelect
  Until Quit
 
EndIf


Procedure ShowAboutWindow()
  
  Protected AboutWindow, myEditorGadget, CloseButton 
  Protected Text$
  Protected Quit
  
  AboutWindow = OpenWindow(#PB_Any, #PB_Ignore, #PB_Ignore, 620, 373, "Quad-Klanger Help", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
  
 If AboutWindow
    
myEditorGadget = EditorGadget(#PB_Any, 5, 5, 610, 360, #PB_Editor_ReadOnly)
    
Text$ = Chr($d) + "The Quad-Klanger main window displays a single cycle of the basic waveform." + Chr($d)
Text$ + Chr($d) + "The X1, X2 sliders 'pull' the waveform along the X axis."
Text$ + Chr($d) + "The Y1, Y2 sliders pull' the waveform along the Y axis." + Chr($d)
Text$ + Chr($d) + "You can make each of the X1, X2, Y1, Y2 points 'wobble' as the Note plays by modulating each of them "
Text$ + Chr($d) + "independently, allowing a wide variety of harmonically rich sounds to be produced." + Chr($d)
Text$ + Chr($d) + "Let us take point X1 for example:" + Chr($d)
Text$ + Chr($d) + "The point will oscillate between where the X1 slider is currently set, to a level determined by X1Mod%."
Text$ + Chr($d) + "If X1Mod% = +80, the point will oscillate between where the X1 slider is currently set, to 80% of the way up"  
Text$ + Chr($d) + "to the top of the slider. If X1Mod% = -30, the point will oscillate between where the X1 slider is currently set,"
Text$ + Chr($d) + "to 30% of the way to the bottom of the slider. The point will oscillate at a frequency determined by"
Text$ + Chr($d) + "CoarseX1ModF and FineX1ModF. If Coarse X1ModF = 2 and FineX1ModF = .25, the point will oscillate at" 
Text$ + Chr($d) + "2.25 times the frequency of the Note being played on the keyboard. If Coarse X1ModF = 10 and" 
Text$ + Chr($d) + "FineX1ModF = 0, the point will oscillate at 10 times the frequency of the Note being played on the keyboard." 
Text$ + Chr($d) + "If Coarse X1ModF = 0 and FineX1ModF = .05, the point will oscillate at 0.05 times the frequency of the Note" 
Text$ + Chr($d) + "being played on the keyboard. Integer multiples of the Note Frequency set by CoarseX1ModF result in harmonic" 
Text$ + Chr($d) + "overtones being produced, whilst the fractional multiplier set by FineX1ModF results in inharmonic overtones." + Chr($d)
Text$ + Chr($d) + "You can make the point oscillation increase or decrease in amplitude over time as the Note plays, by moving "
Text$ + Chr($d) + "the 7 segments of the wobble amplitude curve for point X1 into your desired envelope shape, and the period "
Text$ + Chr($d) + "over which that envelope applies is determined by the Duration slider. The x1 setting for the Duration slider "
Text$ + Chr($d) + "constrains this period to a maximum of a couple of seconds, whilst the x10 setting allows the period to be "
Text$ + Chr($d) + "many seconds." + Chr($d)
Text$ + Chr($d) + "For example, if you draw the wobble amplitude curve as a straight line starting at lower left and ending at top " 
Text$ + Chr($d) + "right of the envelope window, and you leave the Duration slider in its default position, then when you press a "
Text$ + Chr($d) + "Note, the point oscillation will occur between where the X1 slider is currently set, and a level which linearly " 
Text$ + Chr($d) + "moves over a couple of seconds from X1 to the level determined by X1Mod%. This will change the harmonic " 
Text$ + Chr($d) + "content of the sound as the Note plays, giving a kind of 'filter sweeping' effect." + Chr($d)
Text$ + Chr($d) + "The sound produced by this engine is then passed through a master low pass filter, the frequency of which is set "
Text$ + Chr($d) + "by the F slider and the Resonance by the Res slider, giving you even greater sound shaping versatility, and then "
Text$ + Chr($d) + "through the level ADSR, which you set with the Amplitude EG sliders." + Chr($d)
Text$ + Chr($d) + "The Vol slider adjusts the overall level of the sound." + Chr($d)
Text$ + Chr($d) + "The MIDI devices available on your system which Quad-Klanger can use to receive Midi note events from your "
Text$ + Chr($d) + "virtual or external hardware keyboard, are shown in the Midi devices drop-down. Select the device you wish to use, "  
Text$ + Chr($d) + "and ensure your keyboard transmits through the same device." 

SetGadgetText(myEditorGadget, Text$)
    
    Repeat 
      
      Select WaitWindowEvent()
          
        Case #PB_Event_Gadget :      If EventGadget()=CloseButton : Break : EndIf
        Case #PB_Event_CloseWindow : If EventWindow()=AboutWindow : Break : EndIf 
          
      EndSelect
      
    ForEver
    
    CloseWindow(AboutWindow)
    
 EndIf
  
EndProcedure


Pa_AbortStream(Stream)
While Pa_IsStreamActive(Stream) : WaitWindowEvent(1) : Wend
Pa_Terminate()

Re: Quad-Klanger released: A real-time Bezier sound synthesi

Posted: Sun Aug 05, 2012 4:20 pm
by Zach
:shock: ... Cool story bro

(way over my head)

Re: Quad-Klanger released: A real-time Bezier sound synthesi

Posted: Sun Aug 26, 2012 7:35 am
by doctornash
YEEEHAAA! The clicking between key presses is now COMPLETELY eliminated :mrgreen: :mrgreen: :mrgreen:

I wasn't fully satisfied with the 'hack' in v102, so I went through the code carefully once again. Discovered there was a bug in the 'force-decay and smoothing-rise on keypress' routine of v102: at the start of the force-decay, the single-cycle waveform calculator was resetting the waveform (by forcing Pee = -1 in CalculateTheWaveform) causing a discontinuity in the waveform, and also the force-decay was decaying at the frequency of the NEW note, not the frequency of the OLD note. Fixed the first issue by shifting the placement of Pee = -1 in the LevelADSR routine, and the second issue by use of the new OldFreq variable. Doing so has allowed elimination of the need for the low pass filter in the force-decay because there is no clicking any more to filter out.

The duration of force decay can be adjusted by varying the value of ForcedDelayCounter (currently set to 1000) under CalculateTheWaveform and LevelADSR routines.

Further fine tuning of the response of the synth on your system when transitioning between notes can be achieved by manipulating the following parameters:
op\suggestedLatency (currently set to 75, you can change it some other value)
and
Use err = Pa_OpenStream(@Stream, #Null, @op, #SampleRate, #SampleRate / 100, #paNoFlag, @PaStreamCallback(), 0)
instead of
err = Pa_OpenStream(@Stream, #Null, @op, #SampleRate, #paframesperbufferunspecified, #paNoFlag, @PaStreamCallback(), 0)
and if you do so, you can adjust the value of 100 to something else in #SampleRate/100

In this new version (v104) I've also introduced the ability to save and load your sound patches. Next thing will be to allow user to select audio device for playback.
Here's the v104 update:
http://www.mediafire.com/?so1ab3w0709u20b

Just goes to show that where one puts one's Pee can make ALL the difference :oops:

Re: Quad-Klanger released: A real-time Bezier sound synthesi

Posted: Thu Aug 30, 2012 10:41 pm
by karmacomposer
This is just AMAZING work. I followed your posts on KVR (long time KVRian here) and bought PureBasic purely (lol) because of your progress!

You have shown me how to use MIDI, ASIO and general sound stuff through your code. However, how would one do this polyphonically? Also, there is a nagging crash when pressing a key on a midi keyboard and moving any one of the graphics 'dots' simultaneously.

I want to create something similar to my GrooveBloxLive and KlangDM, but in PureBasic (http://www.supersynths.com). I just need to see how to load in samples, play back multiple tracks, slicing and dicing the waveforms, etc. Then, I would like to see if I can wrap it as a vsti (big dreams here).

Other than that one crash bug, amazing work!

Mike