Polyphonic Nanosynth v1.0

Home Forums Products NS1nanosynth Polyphonic Nanosynth v1.0

This topic contains 3 replies, has 3 voices, and was last updated by  liamfpower 5 days, 23 hours ago.

Viewing 4 posts - 1 through 4 (of 4 total)
  • Author
    Posts
  • #3650

    liamfpower
    Participant

    Hi guys,

    Been a while since posting, but I have successfully adapted a Mozzi Polyphonic sketch for the Nano!
    To use:

    Upload the sketch
    Connect C1 -> A0 for wave shape control
    D9 (Mozzi output) -> Filter input
    D13 (Gate Signal) -> ADSR Gate

    Now you can play up to six notes simultaneously over usb-midi and have a NS1 Polysynth!

    Definitely not a perfect sketch so be nice!
    Would appreciate any additions!

    Liam

    
    
    #include <MIDI.h>
    #include <MozziGuts.h>
    #include <Oscil.h> // oscillator template
    #include <Line.h> // for envelope
    #include <mozzi_midi.h>
    #include <ADSR.h>
    #include <mozzi_fixmath.h>
    #include <tables/sin2048_int8.h> // sine table for oscillator
    #include <tables/saw2048_int8.h> // saw table for oscillator
    #include <tables/triangle2048_int8.h> // triangle table for oscillator
    #include <tables/square_analogue512_int8.h> // square table for oscillator
    
    //Oscillator Tables used for Low Frequency Oscillator (LFO)
    #include <tables/sin512_int8.h>
    
    // NANOSYNTH POLYPHONY SKETCH L.P V 1.0 060817
    
    // TO DO: 
    // FIX NOTE OFF BEHAVIOUR
    // ADD LFO/CV IN FOR 6 OSCILLATORS
    // USE DAC OUT INSTEAD OF MOZZI OUT FOR CLEANER SIGNAL + FM
    // ADD UNISON/STACKING MODE (MORE OSCILLATORS)
    
    // USAGE
    // POLYPHONIC SIGNAL OUT OF PIN 9
    // GATE SIGNAL OUT OF PIN 13
    // C1 -> A0 TO CONTROL WAVEFORM
    // WAVES: SINE, SAW, TRIANGLE, SQUARE
    // JITTERFREQ ADDS A RANDOM AMOUNT OF DETURNING TO OSCILLATORS ON NOTE ON FOR MORE "ANALOGUE" BEHAVIOUR
    // FEEL FREE TO REMOVE THIS FEATURE IF YOU WANT MORE STABLE TUNING
    
    // GATE OUT
    #define LED 13
    
    // set other constants
    #define CONTROL_RATE 128 // low to save processor
    
    #define ATTACK 50 // long enough for control rate to catch it
    #define DECAY 50
    #define SUSTAIN 600000 // Sustain 60 seconds unless a noteOff comes.
    #define RELEASE 1000
    // higher resolution for smoother envelopes
    
    #define ATTACK_LEVEL 255
    #define DECAY_LEVEL 255
    
    // declare with or without a wavetable, and use setTable() later
    Oscil <2048, AUDIO_RATE> oscil1; 
    Oscil <2048, AUDIO_RATE> oscil2; 
    Oscil <2048, AUDIO_RATE> oscil3; 
    Oscil <2048, AUDIO_RATE> oscil4; 
    Oscil <2048, AUDIO_RATE> oscil5; 
    Oscil <2048, AUDIO_RATE> oscil6; 
    
    //LFO
    Oscil <512, AUDIO_RATE> lfo(SIN512_DATA);
    
    // envelope generators
    // requires latest Mozzi (April 2014), enables envelope.next() at control rate, using latest version of Mozzi
    // use: ADSR <unsigned int CONTROL_UPDATE_RATE, unsigned int LERP_RATE> envName;
    ADSR <CONTROL_RATE, CONTROL_RATE> envelope1;
    ADSR <CONTROL_RATE, CONTROL_RATE> envelope2;
    ADSR <CONTROL_RATE, CONTROL_RATE> envelope3;
    ADSR <CONTROL_RATE, CONTROL_RATE> envelope4;
    ADSR <CONTROL_RATE, CONTROL_RATE> envelope5;
    ADSR <CONTROL_RATE, CONTROL_RATE> envelope6;
    
    // notes
    byte note=0;
    byte note1=0;
    byte note2=0;
    byte note3=0;
    byte note4=0;
    byte note5=0;
    byte note6=0;
    const byte NOTEON = 0x09;
    const byte NOTEOFF = 0x08;
    
    // LFO (NOT IMPLEMENTED YET)
    
    float lfovalue[1] = { 0 };
    unsigned int lfo_speed = 0;
    unsigned int lfo_depth = 0;
    byte lfo_gain = 0;
    int depth = 0;
    
    // gains to carry control rate envelope levels from updateControl() to updateAudio()
    byte gain1,gain2,gain3,gain4,gain5,gain6 = 0;
    
    // wave type control
    boolean waveLatchOn=false; // acts as a semaphore to handle button push/release actions, to avoid looping
    byte waveNumber=2; // defines selected wave type
    byte notecounter = 0;
    //ADSR control
    int sensorPin = A0;
    int decayValue = 50;
    byte noteonnumber = 0;
    byte noteoffumber = 0;
    int mozziraw = 0;
    int lastmozziraw = 0;
    int vibrato = 0;
    int mode = 2;
    int detune = 7;
    int jitterfreq = 0;
    int lastnote = 0;
    bool voice[6] {0, 0, 0, 0, 0, 0};
    
    void setup() {
    
     Serial.begin(9600);
      
      pinMode(LED, OUTPUT); // set led to indicate playing note(s)
    
      envelope1.setADLevels(ATTACK_LEVEL,DECAY_LEVEL);
      envelope1.setTimes(ATTACK,DECAY,SUSTAIN,RELEASE); 
      envelope2.setADLevels(ATTACK_LEVEL,DECAY_LEVEL);
      envelope2.setTimes(ATTACK,DECAY,SUSTAIN,RELEASE); 
      envelope3.setADLevels(ATTACK_LEVEL,DECAY_LEVEL);
      envelope3.setTimes(ATTACK,DECAY,SUSTAIN,RELEASE); 
      envelope4.setADLevels(ATTACK_LEVEL,DECAY_LEVEL);
      envelope4.setTimes(ATTACK,DECAY,SUSTAIN,RELEASE);  
      envelope5.setADLevels(ATTACK_LEVEL,DECAY_LEVEL);
      envelope5.setTimes(ATTACK,DECAY,SUSTAIN,RELEASE); 
      envelope6.setADLevels(ATTACK_LEVEL,DECAY_LEVEL);
      envelope6.setTimes(ATTACK,DECAY,SUSTAIN,RELEASE);  
    
      setWave(waveNumber);
    
      startMozzi(CONTROL_RATE); 
      
    
    }
    
    void setWave(int waveNumber) { // good practice to use local parameters to avoid global confusion
    
      switch (waveNumber) {
      case 5:
    
        oscil1.setTable(SIN2048_DATA);
        oscil2.setTable(SIN2048_DATA);
        oscil3.setTable(SIN2048_DATA);
        oscil4.setTable(SIN2048_DATA);
        oscil5.setTable(SIN2048_DATA);
        oscil6.setTable(SIN2048_DATA);
    
        break;
      case 2:
      
        oscil1.setTable(TRIANGLE2048_DATA);
        oscil2.setTable(TRIANGLE2048_DATA);
        oscil3.setTable(TRIANGLE2048_DATA);
        oscil4.setTable(TRIANGLE2048_DATA);
        oscil5.setTable(TRIANGLE2048_DATA);
        oscil6.setTable(TRIANGLE2048_DATA);
        break;
      case 3:
        
        oscil1.setTable(SAW2048_DATA);
        oscil2.setTable(SAW2048_DATA);
        oscil3.setTable(SAW2048_DATA);
        oscil4.setTable(SAW2048_DATA);
        oscil5.setTable(SAW2048_DATA);
        oscil6.setTable(SAW2048_DATA);
        break;
        
      case 4:
        
        oscil1.setTable(SQUARE_ANALOGUE512_DATA);
        oscil2.setTable(SQUARE_ANALOGUE512_DATA);
        oscil3.setTable(SQUARE_ANALOGUE512_DATA);
        oscil4.setTable(SQUARE_ANALOGUE512_DATA);
        oscil5.setTable(SQUARE_ANALOGUE512_DATA);
        oscil6.setTable(SQUARE_ANALOGUE512_DATA);
        break;
    
     
      
      }
    
    }
    
    void updateControl(){
    
    midiprocessing();
    
    mozziraw = mozziAnalogRead(A1);
    
    //vibrato = mozziAnalogRead(A0);
    //depth = mozziAnalogRead(A2);
    //lfo_speed=map(vibrato, 0,1023,0,1400);
    //lfo_depth=map(depth, 0, 1023, 1, 256); 
    //lfo.setFreq((int)lfo_speed);
    //
    //
    //  lfo_gain = (int)((long)(lfo.next() + 128) * lfo_depth) >> 8;
    
    // SET WAVESHAPE
    if (mozziraw != lastmozziraw)
    {
     waveNumber = map(mozziraw, 0, 1023, 0, 4);
     
      setWave(waveNumber);
      lastmozziraw = mozziraw;
    
    }
     
      envelope1.update();
      envelope2.update();
      envelope3.update();
      envelope4.update();
      envelope5.update();
      envelope6.update();
    
      gain1 = envelope1.next() >> 3;
      gain2 = envelope2.next() >> 3;
      gain3 = envelope3.next() >> 3;
      gain4 = envelope4.next() >> 3;
      gain5 = envelope5.next() >> 3;
      gain6 = envelope6.next() >> 3;
      
    
     
      
     
     
    }
    // GENERATE AUDIO AND MULTIPLY BY ENVELOPES
    
    int updateAudio(){
    
     
      return ((long)+ gain1 * oscil1.next() + 
        (int)gain2 * oscil2.next() + 
        (int)gain3 * oscil3.next() + 
        (int)gain4 * oscil4.next() +
        (int)gain5 * oscil5.next() + 
        (int)gain6 * oscil6.next()) >> 6;
        
    }
    
    // REQUIRED FOR MOZZI
    void loop() {
      audioHook(); // required here
    } 
    
    // NOTE ON ROUTING
    void  USBNoteon(byte note)
    {
    
    if((note1==0) && (note != lastnote)){
        note1=note;
        
        oscil1.setFreq_Q16n16(Q16n16_mtof(Q8n0_to_Q16n16(note)) + jitterfreq );
        envelope1.noteOn();
        voice[0] = 1;
        lastnote=note;
       
      }
      else if((note2==0) && (note != lastnote)){
        note2=note;
        oscil2.setFreq_Q16n16(Q16n16_mtof(Q8n0_to_Q16n16(note)) + jitterfreq );
        envelope2.noteOn();
         voice[1] = 1;
         lastnote=note;
          
      }
      else if((note3==0) && (note != lastnote)){
        note3=note;
        oscil3.setFreq_Q16n16(Q16n16_mtof(Q8n0_to_Q16n16(note)) + jitterfreq );
        envelope3.noteOn();
         voice[2] = 1;
         lastnote=note;
         
      }
      else if((note4==0) && (note != lastnote)){
        note4=note;
        oscil4.setFreq_Q16n16(Q16n16_mtof(Q8n0_to_Q16n16(note)) + jitterfreq );
        envelope4.noteOn();
         voice[3] = 1;
         lastnote=note;
         
      }
      else if((note5==0) && (note != lastnote)){
        note5=note;
        oscil5.setFreq_Q16n16(Q16n16_mtof(Q8n0_to_Q16n16(note)) + jitterfreq );
        envelope5.noteOn();
         voice[4] = 1;
         lastnote=note;
          
      }
      else if((note6==0) && (note != lastnote)){
        note6=note;
        oscil6.setFreq_Q16n16(Q16n16_mtof(Q8n0_to_Q16n16(note)) + jitterfreq );
        envelope6.noteOn();
         voice[5] = 1;
         lastnote=note;
         
     
      }
    
     // IF NO NOTES PLAYING, TURN OFF ENVELOPES
    
      if(note1+note2+note3+note4+note5+note6==0){
       digitalWrite(LED,LOW); // no notes playing
      allnotesoff();
      }
    
    }
    
    // NOTE OFF ROUTING
    void  USBNoteoff(byte note)
    {
    
    if(note==note1){
        envelope1.noteOff();
        note1=0;
      }
      else if(note==note2){
        envelope2.noteOff();
        note2=0;
      }
      else if(note==note3){
        envelope3.noteOff();
        note3=0;
      }
      else if(note==note4){
        envelope4.noteOff();
        note4=0;
      }
      else if(note==note5){
        envelope5.noteOff();
        note5=0;
      }
      else if(note==note6){
        envelope6.noteOff();
        note6=0;
      }
    if (notecounter == 0) {  
             
     digitalWrite(LED,LOW); // no notes playing
    allnotesoff();
      
       } 
    
      
    }
    // TURN ALL NOTES OFF
    void allnotesoff()
    {
      
     envelope1.noteOff();
         envelope2.noteOff();
           envelope3.noteOff();
             envelope4.noteOff();
               envelope5.noteOff();
                 envelope6.noteOff();
    }
    
    void midiprocessing()
    {
       while(MIDIUSB.available() > 0) {
        MIDIEvent e = MIDIUSB.read();
    
    // IF NOTE ON WITH VELOCITY GREATER THAN ZERO
        if((e.type == NOTEON) && (e.m3 > 0)){
          jitterfreq = random(0, 7);
         digitalWrite(LED, HIGH);
          note = e.m2;
            notecounter++;
            
            if (notecounter > 0)
            {
              
          USBNoteon(note);
      
            }
    
          }
    
          // IF NOTE ON W/ ZERO VELOCITY
        if((e.type == NOTEON) && (e.m3 == 0)){
          note = e.m2;
          USBNoteoff(note);
          notecounter--;
      Serial.println(notecounter);
         
         
          }
        // IF USB NOTE OFF 
        if(e.type == NOTEOFF){
          notecounter--;
    note = e.m2;
    USBNoteoff(note);  
        }
    }
    }
    
    #3652

    donturner
    Participant

    Firstly, awesome!!!! Congratulations on creating something pretty amazing!

    Just a couple of issues…

    1) If you press the same note twice in a row, the second note doesn’t sound

    2) The wave change input is on pin A1, not A0, and the mapping from analog input value 0-1023 to 0-4 was slightly wrong since your setWave method is expecting a value from 2-5.

    I modified it slightly to fix it:

    void setWave(int waveNumber) { // good practice to use local parameters to avoid global confusion
    
      switch (waveNumber) {
      case 0:
    
        oscil1.setTable(SIN2048_DATA);
        oscil2.setTable(SIN2048_DATA);
        oscil3.setTable(SIN2048_DATA);
        oscil4.setTable(SIN2048_DATA);
        oscil5.setTable(SIN2048_DATA);
        oscil6.setTable(SIN2048_DATA);
    
        break;
      case 1:
      
        oscil1.setTable(TRIANGLE2048_DATA);
        oscil2.setTable(TRIANGLE2048_DATA);
        oscil3.setTable(TRIANGLE2048_DATA);
        oscil4.setTable(TRIANGLE2048_DATA);
        oscil5.setTable(TRIANGLE2048_DATA);
        oscil6.setTable(TRIANGLE2048_DATA);
        break;
      case 2:
        
        oscil1.setTable(SAW2048_DATA);
        oscil2.setTable(SAW2048_DATA);
        oscil3.setTable(SAW2048_DATA);
        oscil4.setTable(SAW2048_DATA);
        oscil5.setTable(SAW2048_DATA);
        oscil6.setTable(SAW2048_DATA);
        break;
        
      case 3:
      case 4:
        
        oscil1.setTable(SQUARE_ANALOGUE512_DATA);
        oscil2.setTable(SQUARE_ANALOGUE512_DATA);
        oscil3.setTable(SQUARE_ANALOGUE512_DATA);
        oscil4.setTable(SQUARE_ANALOGUE512_DATA);
        oscil5.setTable(SQUARE_ANALOGUE512_DATA);
        oscil6.setTable(SQUARE_ANALOGUE512_DATA);
        break;
    
     
      
      }
    
    }

    Might be worth getting this code on github to make it easier to collaborate if you’re interested.

    #3659

    Niklas
    Participant

    Good work! But I need to ask something on a code specific manner.

    You have:

    Oscil <2048, AUDIO_RATE> oscil1; 
    ...
    Oscil <2048, AUDIO_RATE> oscil6;
    ...
    ADSR <CONTROL_RATE, CONTROL_RATE> envelope1;
    ...
    ADSR <CONTROL_RATE, CONTROL_RATE> envelope6;
    
    // notes
    byte note=0;
    ....
    byte note6=0;

    That’s a lot of VARIABLE_1_of_1 to VARIABLE_1_of_6, VARIABLE_2_of_1 to VARIABLE_2_of_6, etc.

    If one would declare a struct and array:

    struct Voice{
      Oscil <2048, AUDIO_RATE> oscil;
      ADSR <CONTROL_RATE, CONTROL_RATE> envelope;
      byte note = 0;
    };
    
    Voice voices[6];
    

    You could easily reduce the code size with for loops, for instance:

    // NOTE OFF ROUTING
    void  USBNoteoff(byte note)
    {
      for(uint8_t i = 0 ; i < 5 ; ++i )
      {
        Voice v = voices[i];
        if(note==v.note){
            v.envelope1.noteOff();
            v.note=0;
            break;
          }
      }
      if (notecounter == 0) {  
        digitalWrite(LED,LOW); // no notes playing
        allnotesoff();
      } 
    }

    And that makes also the code more flexible.

    I have seen it in some other arduino code also and I ask myself why?
    It is just one more reference/indexing, comparing and increment per voice.
    I don’t think that could cause any big overhead or…?

    #3691

    liamfpower
    Participant

    Hi Niklas,

    I had the same thought! I lifted parts of this code from a polyphonic Mozzi polysynth sketch I found here, so the Oscillators were already instantiated in that clumsy but functional way. The fact that the sketch worked and didn’t overflow the memory led me to believe it was the right way to do it, and I wasn’t really sure how to declare Oscillators in an array using the MOZZi Framework.

    In future I would definitely like to process the envelopes, oscillators and notes held/cycles in an array to make adding and processing voices easier and possibly make the code more efficient. Ideally I could rebuild it from the ground up using the onboard NS1 DAC for stronger output (which could also be used for FM and sync.

    I have been reading up on Direct Digital Synthesis and trying to make a phase accumulator read through a wavetable but as a musician and amateur coder it is pretty heavy going as far as mathematics. See this link for the article I’ve been reading

    I’ll definitely have a go at integrating some for loops into the code in future though.

    Liam

Viewing 4 posts - 1 through 4 (of 4 total)

You must be logged in to reply to this topic.