Author Topic: Polyphonic Interrupter with STM32F103C8T6 (Bluepill)  (Read 898 times)

Offline Zipdox

  • High Voltage Technician
  • ***
  • Posts: 129
  • Karma: +1/-0
    • View Profile
    • Zipdox
Polyphonic Interrupter with STM32F103C8T6 (Bluepill)
« on: September 27, 2020, 11:43:14 PM »
I wrote a polyphonic interrupter for the Bluepill.


It uses the STM32duino core for programming.
Here's the code:

Code: (c) [Select]
#include <MIDI.h>

MIDI_CREATE_DEFAULT_INSTANCE();

#define sampling_rate 96000
#define max_playing_notes 8

float note_numerator = sampling_rate/440.0;
int notePeriod(float pitch){
    return note_numerator/pow(2, (pitch-69)/12.0);
}

typedef struct {
    byte pitch;
    int on_time;
    int off_time;
    int been_on;
} Note;

Note playing_notes[max_playing_notes];

void handleNoteOn(byte channel, byte pitch, byte velocity){
    for(int i = 0; i < max_playing_notes; i++){
        if(playing_notes[i].on_time == 0){
            int period = notePeriod(pitch);
            playing_notes[i] = {pitch, 10, period-10, 0};
            break;
        }
    }
    digitalWrite(LED_BUILTIN, LOW);
}

void handleNoteOff(byte channel, byte pitch, byte velocity){
    for(int i = 0; i < max_playing_notes; i++){
        if(playing_notes[i].pitch == pitch) playing_notes[i].on_time = 0;
    }
    digitalWrite(LED_BUILTIN, HIGH);
}

void sample(void){
    for(int i = 0; i < max_playing_notes; i++){
        if(playing_notes[i].on_time != 0) playing_notes[i].been_on++;
    }
}

void setup()
{
   
    pinMode(PB9, OUTPUT);
    digitalWrite(PB9, LOW);
    pinMode(LED_BUILTIN, OUTPUT);
    digitalWrite(LED_BUILTIN, HIGH);

    for(int i = 0; i<(sizeof(playing_notes)/sizeof(playing_notes[0])); i++){
        playing_notes[i] = {0,0,0,0};
    }

    MIDI.setHandleNoteOn(handleNoteOn);
    MIDI.setHandleNoteOff(handleNoteOff);
    MIDI.begin(MIDI_CHANNEL_OMNI);

    HardwareTimer *MyTim = new HardwareTimer(TIM2);
    MyTim->setOverflow(sampling_rate, HERTZ_FORMAT);
    MyTim->attachInterrupt(sample);
    MyTim->resume();
}

void loop()
{
    MIDI.read();
    bool play = false;
    for(int i = 0; i < max_playing_notes; i++){
        if(playing_notes[i].on_time == 0) continue;
        if(playing_notes[i].been_on > playing_notes[i].on_time) playing_notes[i].been_on = -playing_notes[i].off_time;
        if(playing_notes[i].been_on >= 0) play = true;
    }
    if(play){
        digitalWrite(PB9, HIGH);
    }else{
        digitalWrite(PB9, LOW);
    }
}
License: GPL-3

Offline TMaxElectronics

  • High Voltage Technician
  • ***
  • Posts: 111
  • Karma: +5/-0
    • View Profile
    • My random (and very empty) electronics blog
Re: Polyphonic Interrupter with STM32F103C8T6 (Bluepill)
« Reply #1 on: September 28, 2020, 01:14:46 AM »
Cool project :D quite a bit simpler that either my MidiStick or max's synthterrupter

a few things I noticed though:

It seems to me like you don't take velocity or volume into account, since that should be a simple addition I highly recommend it, especially if you have many voices. Sometimes a song gets unexpectedly intense with the amount of notes playing at a given time, and with your code you can't really do much about it once the code is uploaded. Since you don't have adsr all you need to add is maybe a macro for the maximum ontime and then use the MIDI_VOLUME command to scale a variable from 0 to the max from the macro, which is then used as the on-time setting for the notes.

It also doesn't seem like you have any duty limiting, which is a very risky thing with a 8-voice polyphonic interrupter. If you play many high notes (or even many low ones) the duty cycle could get very high, putting a lot of thermal stress onto your IGBTs. The way I'd recommend to do this is to just add all the on-times of active notes together whenever a note event (either on or off) occurs, and if that total on-time is too much you scale back all notes by the amount by which the total value is too large. My interrupter uses a different approach that is quite complex and not really needed here. Here is how the synthterrupter does it (older code, but the idea is still the same): https://github.com/MMMZZZZ/Syntherrupter/blob/80f99ee601b220f3aca8894f0c15684e727fd0e8/Syntherrupter_Tiva/MIDI.cpp#L920

And finally the last thing that I didn't find was a note holdoff,  which isn't really that important for a SSTC but very much so if you maybe want to upgrade to drsstc at some point. With a DRSSTC the current in the primary circuit continues to oscillate a bit after the interrupter pulse goes low as the energy is slowly put back into the bus caps. If however the interrupter is triggered again during this ring down (f.E. a note pulse occurs almost immediately after the end of another), the start "kick" or start oscillator (depending on the driver) would cause hard switching of the IGBTs, which if it occurs once might be fine, but if it happens too often it will probably kill the IGBTs. I believe that SSTCs don't have that problem, but I'm not 100% sure. If you want to implement this you could just add a variable that stores the system time of the last "output off", and whenever you switch it on again you check if the time is larger than a preset value.

Not so say of course that the interrupter is no good or anything like that, everybody starts somewhere and my intention is to tell you what I would have liked to read when starting work on my first one :D

Offline Zipdox

  • High Voltage Technician
  • ***
  • Posts: 129
  • Karma: +1/-0
    • View Profile
    • Zipdox
Re: Polyphonic Interrupter with STM32F103C8T6 (Bluepill)
« Reply #2 on: September 28, 2020, 01:27:07 AM »
This is still the first version. I just coded this quickly to see if the Bluepill is powerful enough for the job and I can say it very much is.

I still have to add pitchbend and velocity. I'll also try to figure something out to make lower notes louder because the low duty cycle makes them almost inaudible when higher notes play. Perhaps some kind of duty cycle sharing.

As for the note holdoff, I think that's a little too complicated for me. I'm still new to C.

Offline Max

  • High Voltage Technician
  • ***
  • Posts: 131
  • Karma: +10/-0
  • "With the first link, the chain is forged. [...]"
    • View Profile
    • My Youtube Channel
Re: Polyphonic Interrupter with STM32F103C8T6 (Bluepill)
« Reply #3 on: September 28, 2020, 04:44:56 PM »
Less than 80 lines of code - okay that's minimalistic! Now no one can say anymore that polyphony would be hard to do.  ;D Anyways, sounds already pretty good!

Note holdoff is really not that difficult at all. Add one more variable that you increment in your ISR just like the notes. It will be the holdoff. Then, in the if (been_on > ontime) section, you reset the holdoff to 0. Why? Well, if you enter that if, an ontime has ended and thus we want to wait for the holdoff time before the next ontime plays. Third change: replace the "if (play)" by "if (play && holdoff >= minHoldoff)". And you're done :)

This is a very simple approach but it has one disadvantage: the holdoff does not increase the time "between" ontimes but effectively omits a part of the next ontime. If two ontimes follow each other closely, the second one will be shortened by the holdoff time. This will probably be quite audible since 10-50us less ontime do make a big difference. On the other side, since there was an ontime just before this one it might not matter that much.
How to fix this? You could try the following. Only increment the been_on counters if the holdoff is above its threshold (and increment the holdoff otherwise). This does cause some jitter in the frequency of the notes but I'd guess this is less audible than the changing ontime. I'd definitely try both! ;)

Considering your issue with lower notes. In your video the very lowest note - even when playing alone is almost not audible at all (at 0:08 f.ex.). Are you 100% sure that it did really play at the same, constant ontime as the other notes? Your code looks like it should but the way it sounds would make me check it with a scope.
That aside, I thought about something similar for my Syntherrupter and came up with this: low notes play at constant duty, high notes at constant ontime, both values can be specified by the user. Crossover between modes happens at the frequency where both result in the same ontime. I explained it in details here: https://highvoltageforum.net/index.php?topic=1020.msg8564#msg8564
The code for this is not hard at all. You calculate the crossover frequency (or the MIDI pitch value corresponding to that frequency) once when the duty and/or ontime settings change. Then, in your handleNoteOn: if the note's frequency is above the crossover frequency, set ontime to the given value, otherwhise set the ontime to the given percentage of the notes period.
Here's the source code for the crossover frequency (line 433-445): https://github.com/MMMZZZZ/Syntherrupter/blob/d52c01bdb566ff2c13969cbc9f7b3510c9f22e47/Syntherrupter_Tiva/MIDI.cpp#L433
And here's the calculation of the ontime (line 691-699): https://github.com/MMMZZZZ/Syntherrupter/blob/d52c01bdb566ff2c13969cbc9f7b3510c9f22e47/Syntherrupter_Tiva/MIDI.cpp#L691


Kind regards,
Max

Offline Zipdox

  • High Voltage Technician
  • ***
  • Posts: 129
  • Karma: +1/-0
    • View Profile
    • Zipdox
Re: Polyphonic Interrupter with STM32F103C8T6 (Bluepill)
« Reply #4 on: September 28, 2020, 06:28:50 PM »
I cooked up some new code. It uses duty cycle per note instead of a fixed pulse width. It ranges from 0.5% to 2.5% duty cycle per note depending on velocity. This increases the quality of low notes drastically.
Code: (c) [Select]
// GPL-3.0
#include <MIDI.h>

MIDI_CREATE_DEFAULT_INSTANCE();

#define sampling_rate 96000
#define max_playing_notes 5
#define max_pitch 104

float note_numerator = sampling_rate/440.0;
int notePeriod(float pitch){
    while(pitch > max_pitch){
        pitch -= 12;
    }
    return note_numerator/pow(2, (pitch-69)/12.0);
}

typedef struct {
    byte channel;
    byte pitch;
    int on_time;
    int off_time;
    int been_on;
} Note;

Note playing_notes[max_playing_notes];

const float min_duty_cycle = 127/4.0;
const float duty_cycle_multiplier = 1/(min_duty_cycle+127)*0.025;
void handleNoteOn(byte channel, byte pitch, byte velocity){
    for(int i = 0; i < max_playing_notes; i++){
        if(playing_notes[i].on_time == 0){
            int period = notePeriod(pitch);
            float duty_cycle = (min_duty_cycle+velocity)*duty_cycle_multiplier;
            int on_time = duty_cycle*period;
            playing_notes[i] = {channel, pitch, on_time, period-on_time, 0};
            break;
        }
    }
}

void handleNoteOff(byte channel, byte pitch, byte velocity){
    for(int i = 0; i < max_playing_notes; i++){
        if(playing_notes[i].pitch == pitch) playing_notes[i].on_time = 0;
    }
}

void handlePitchBend(byte channel, int bend){
    for(int i = 0; i < max_playing_notes; i++){
        if(playing_notes[i].channel != channel) continue;
        int period = notePeriod(playing_notes[i].pitch + bend/4096.0);
        playing_notes[i].off_time = period-10;
    }
}

void sample(void){
    for(int i = 0; i < max_playing_notes; i++){
        if(playing_notes[i].on_time != 0) playing_notes[i].been_on++;
    }
}

void handleStop(){
    for(int i = 0; i < max_playing_notes; i++){
        playing_notes[i].on_time = 0;
    }
}

void setup(){
    pinMode(PB9, OUTPUT);
    digitalWrite(PB9, LOW);

    for(int i = 0; i<(sizeof(playing_notes)/sizeof(playing_notes[0])); i++){
        playing_notes[i] = {0, 0, 0, 0, 0};
    }

    MIDI.setHandleNoteOn(handleNoteOn);
    MIDI.setHandleNoteOff(handleNoteOff);
    MIDI.setHandlePitchBend(handlePitchBend);
    MIDI.setHandleStop(handleStop);
    MIDI.begin(MIDI_CHANNEL_OMNI);

    HardwareTimer *MyTim = new HardwareTimer(TIM2);
    MyTim->setOverflow(sampling_rate, HERTZ_FORMAT);
    MyTim->attachInterrupt(sample);
    MyTim->resume();
}

void loop(){
    MIDI.read();
    bool play = false;
    for(int i = 0; i < max_playing_notes; i++){
        if(playing_notes[i].on_time == 0) continue;
        if(playing_notes[i].been_on > playing_notes[i].on_time) playing_notes[i].been_on = -playing_notes[i].off_time;
        if(playing_notes[i].been_on >= 0) play = true;
    }
    if(play){
        digitalWrite(PB9, HIGH);
    }else{
        digitalWrite(PB9, LOW);
    }
}
I'm thinking about using some sort of lookup table for the pulse width depending on pitch. I'll have to figure out how to take velocity into account.

Only a slight problem; higher notes fail to make the SSTC pulse properly, because the pulses are too short, and so they are very hard to hear.

« Last Edit: September 28, 2020, 06:33:49 PM by Zipdox »

Offline Max

  • High Voltage Technician
  • ***
  • Posts: 131
  • Karma: +10/-0
  • "With the first link, the chain is forged. [...]"
    • View Profile
    • My Youtube Channel
Re: Polyphonic Interrupter with STM32F103C8T6 (Bluepill)
« Reply #5 on: September 28, 2020, 11:48:25 PM »
Hi,


I'm thinking about using some sort of lookup table for the pulse width depending on pitch.
Uhm... don't think this would be necessary...? LUTs are useful if the calculation of the right value would take too much time but currently you don't face such a lag of processing time I think?

I'll have to figure out how to take velocity into account.
As a factor. You have the nominal ontime, f.ex. based on the period of the note, and then multiply it with velocity/128. So your velocity scales the ontime between 0 and the nominal value. Note: Your microcontroller doesn't have an FPU, meaning you should avoid floating point calculations whereever possible.

Only a slight problem; higher notes fail to make the SSTC pulse properly, because the pulses are too short, and so they are very hard to hear.
Of course they are! That's exactly what I described and linked from my previous post, including a quite simple solution.


Kind regards,
Max

Offline Zipdox

  • High Voltage Technician
  • ***
  • Posts: 129
  • Karma: +1/-0
    • View Profile
    • Zipdox
Re: Polyphonic Interrupter with STM32F103C8T6 (Bluepill)
« Reply #6 on: October 02, 2020, 10:56:24 PM »
Code: (c) [Select]
#include <MIDI.h>

MIDI_CREATE_DEFAULT_INSTANCE();

#define sampling_rate 96000
#define max_playing_notes 5
#define max_pitch 104

float note_periods[128] = {11741.9717620882, 11082.9455264689, 10460.9075912845, 9873.78196275034, 9319.60916365988, 8796.53969381148, 8302.82785747354, 7836.82593728726, 7396.97869516308, 6981.81818181818, 6589.95883763219, 6220.09286847073, 5870.98588104412, 5541.47276323444, 5230.45379564223, 4936.89098137517, 4659.80458182994, 4398.26984690574, 4151.41392873677, 3918.41296864363, 3698.48934758154, 3490.90909090909, 3294.97941881609, 3110.04643423537, 2935.49294052206, 2770.73638161722, 2615.22689782112, 2468.44549068758, 2329.90229091497, 2199.13492345287, 2075.70696436839, 1959.20648432181, 1849.24467379077, 1745.45454545455, 1647.48970940805, 1555.02321711768, 1467.74647026103, 1385.36819080861, 1307.61344891056, 1234.22274534379, 1164.95114545748, 1099.56746172643, 1037.85348218419, 979.603242160907, 924.622336895385, 872.727272727273, 823.744854704023, 777.511608558842, 733.873235130515, 692.684095404305, 653.806724455279, 617.111372671896, 582.475572728742, 549.783730863217, 518.926741092097, 489.801621080454, 462.311168447692, 436.363636363636, 411.872427352012, 388.755804279421, 366.936617565257, 346.342047702153, 326.90336222764, 308.555686335948, 291.237786364371, 274.891865431609, 259.463370546048, 244.900810540227, 231.155584223846, 218.181818181818, 205.936213676006, 194.37790213971, 183.468308782629, 173.171023851076, 163.45168111382, 154.277843167974, 145.618893182186, 137.445932715804, 129.731685273024, 122.450405270113, 115.577792111923, 109.090909090909, 102.968106838003, 97.1889510698552, 91.7341543913143, 86.5855119255382, 81.7258405569099, 77.138921583987, 72.8094465910928, 68.7229663579022, 64.8658426365121, 61.2252026350567, 57.7888960559616, 54.5454545454546, 51.4840534190015, 48.5944755349276, 45.8670771956572, 43.2927559627691, 40.862920278455, 38.5694607919935, 36.4047232955464, 34.3614831789511, 32.432921318256, 30.6126013175284, 28.8944480279808, 27.2727272727273, 25.7420267095007, 24.2972377674638, 22.9335385978286, 21.6463779813845, 20.4314601392275, 19.2847303959968, 18.2023616477732, 17.1807415894755, 16.216460659128, 15.3063006587642, 14.4472240139904, 13.6363636363636, 12.8710133547504, 12.1486188837319, 11.4667692989143, 10.8231889906923, 10.2157300696137, 9.64236519799838, 9.1011808238866, 8.59037079473777, 8.10823032956401, 7.65315032938209};
float note_numerator = sampling_rate/440.0;
float notePeriod(float pitch){
    while(pitch > max_pitch){
        pitch -= 12;
    }
    return note_numerator/pow(2, (pitch-69)/12.0);
}

typedef struct {
    byte channel;
    byte pitch;
    int on_time;
    int off_time;
    int been_on;
    byte velocity;
} Note;

Note playing_notes[max_playing_notes];

const float min_duty_cycle = 127/4.0;
const float duty_cycle_multiplier = 1/(min_duty_cycle+127)*0.025;
void handleNoteOn(byte channel, byte pitch, byte velocity){
    for(int i = 0; i < max_playing_notes; i++){
        if(playing_notes[i].on_time == 0){
            float period = note_periods[pitch];
            float duty_cycle = (min_duty_cycle+velocity)*duty_cycle_multiplier;
            int on_time = duty_cycle*period;
            if(on_time < 5 && pitch >= 80){
                on_time = 5;
            }else if(on_time < 6 && pitch < 80){
                on_time = 6;
            }
            playing_notes[i] = {channel, pitch, on_time, period-on_time, 0, velocity};
            break;
        }
    }
}

void handleNoteOff(byte channel, byte pitch, byte velocity){
    for(int i = 0; i < max_playing_notes; i++){
        if(playing_notes[i].pitch == pitch) playing_notes[i].on_time = 0;
    }
}

void handlePitchBend(byte channel, int bend){
    for(int i = 0; i < max_playing_notes; i++){
        if(playing_notes[i].channel != channel) continue;
        float period = notePeriod(playing_notes[i].pitch + bend/4096.0);
        float duty_cycle = (min_duty_cycle+playing_notes[i].velocity)*duty_cycle_multiplier;
        int on_time = duty_cycle*period;
        if(on_time < 5 && playing_notes[i].pitch >= 80){
            on_time = 5;
        }else if(on_time < 6 && playing_notes[i].pitch < 80){
            on_time = 6;
        }
        playing_notes[i].on_time = on_time;
        playing_notes[i].off_time = period-on_time;
    }
}

void sample(void){
    for(int i = 0; i < max_playing_notes; i++){
        if(playing_notes[i].on_time != 0) playing_notes[i].been_on++;
    }
}

void handleStop(){
    for(int i = 0; i < max_playing_notes; i++){
        playing_notes[i].on_time = 0;
    }
}

void setup(){
    pinMode(PB9, OUTPUT);
    digitalWrite(PB9, LOW);

    for(int i = 0; i<(sizeof(playing_notes)/sizeof(playing_notes[0])); i++){
        playing_notes[i] = {0, 0, 0, 0, 0};
    }

    MIDI.setHandleNoteOn(handleNoteOn);
    MIDI.setHandleNoteOff(handleNoteOff);
    MIDI.setHandlePitchBend(handlePitchBend);
    MIDI.setHandleStop(handleStop);
    MIDI.begin(MIDI_CHANNEL_OMNI);

    HardwareTimer *MyTim = new HardwareTimer(TIM2);
    MyTim->setOverflow(sampling_rate, HERTZ_FORMAT);
    MyTim->attachInterrupt(sample);
    MyTim->resume();
}

void loop(){
    MIDI.read();
    bool play = false;
    for(int i = 0; i < max_playing_notes; i++){
        if(playing_notes[i].on_time == 0) continue;
        if(playing_notes[i].been_on > playing_notes[i].on_time) playing_notes[i].been_on = -playing_notes[i].off_time;
        if(playing_notes[i].been_on >= 0) play = true;
    }
    if(play){
        digitalWrite(PB9, HIGH);
    }else{
        digitalWrite(PB9, LOW);
    }
}

Offline TMaxElectronics

  • High Voltage Technician
  • ***
  • Posts: 111
  • Karma: +5/-0
    • View Profile
    • My random (and very empty) electronics blog
Re: Polyphonic Interrupter with STM32F103C8T6 (Bluepill)
« Reply #7 on: October 03, 2020, 01:36:46 AM »
Interesting idea with that LUT, but I'm not convinced to be honest (might be because I don't really understand it :P). My Midistick doesn't do any pulse stretching at lower frequencies and sounds fine (in my opinion). But I tend to use midi files that have backing and melody on separate tracks and then use the volume adjust for each to get the mixing right, at least something where my experience mixing bands comes in handy these days...

It also seems a little like the notes are being drowned out by another. Polyphony is great, but it puts a lot of responsibility on the user to select the right tracks to be played. If there are too many different notes playing it gets a little confusing ;) It also makes a difference whether the notes are harmonic to another (in a chord) or a completely different melody. I usually try to keep it to two tracks per synth/coil that play notes at any given moment in time (with a maximum of four simultaneous notes), everything else gets too messy in my opinion. Another thing that could help with that is sustain (where the note volume constantly drops off as it is played), but that would be a major addition to the code, so maybe a long term goal :D You can read up on the envelope of a note if you are interested: https://en.wikipedia.org/wiki/Envelope_(music)

I'm also curious which midi player you use. Everybody seems to have a different preference there :D

Offline Zipdox

  • High Voltage Technician
  • ***
  • Posts: 129
  • Karma: +1/-0
    • View Profile
    • Zipdox
Re: Polyphonic Interrupter with STM32F103C8T6 (Bluepill)
« Reply #8 on: October 03, 2020, 02:36:03 AM »
I use my phone with an OTG dongle and one of those cheap USB MIDI cables and this app. The MIDI cable is suboptimal because it has a shitty connector loose that can cause messages dropping. Also I patched the app with lucky patcher to remove the ads  ;)

Offline TMaxElectronics

  • High Voltage Technician
  • ***
  • Posts: 111
  • Karma: +5/-0
    • View Profile
    • My random (and very empty) electronics blog
Re: Polyphonic Interrupter with STM32F103C8T6 (Bluepill)
« Reply #9 on: October 03, 2020, 02:38:57 PM »
I guess you use the app because you don't have a laptop and don't want to carry your desktop around right? It might be worth looking into using the usb module of the STM32 to do direct USB-Midi conversion. That would circumvent any issues with midi cables and make it easier to use. I didn't read everything but this seems like a god place to start: https://stm32duinoforum.com/forum/viewtopic_f_28_t_4442.html

My MidiStick also communicates this way, and it is actually pretty simple, as long as you don't want to dynamically rename devices or use multiple of them. The dynamic device name stuff took me best part of a month to get working properly ::)

Offline Zipdox

  • High Voltage Technician
  • ***
  • Posts: 129
  • Karma: +1/-0
    • View Profile
    • Zipdox
Re: Polyphonic Interrupter with STM32F103C8T6 (Bluepill)
« Reply #10 on: October 06, 2020, 12:41:33 AM »
I can use my laptop but I's slightly inconvenient.
Regarding using USB, I looked into it but I couldn't find any USB libraries that are compatible with the STM32Duino core. USBComposite requires a different core that I was unable to get working. I might try again when I have more time on my hands. I'm kinda busy this week.
« Last Edit: October 06, 2020, 12:43:55 AM by Zipdox »

Offline profdc9

  • High Voltage Engineer
  • ****
  • Posts: 254
  • Karma: +12/-0
    • View Profile
Re: Polyphonic Interrupter with STM32F103C8T6 (Bluepill)
« Reply #11 on: October 07, 2020, 04:43:20 AM »
I used the USBComposite library for my vector network analyzer ( http://www.github.com/profdc9/VNA ) and Roger's stm32duino core.  You can use it in place of the built-in USB serial if you compile without it.    For example:


void usb_init_serial()
{
  if (usb_initialized)
    USBComposite.end();
  usb_initialized = 1;
  usb_reenumerate();
  USBComposite.clear();
  USBComposite.setProductId(VENDOR_ID);
  USBComposite.setProductId(SERIAL_CDC_PRODUCT_ID);
  CompositeSerial.registerComponent();
  console_setMainSerial(&CompositeSerial);
  USBComposite.begin();
  //while (!USBComposite.isReady());
}

bool usb_init_mass_storage()
{
  DWORD sectors;
  flash_mount_fs_card();
  if (!flash_fs_mounted) return false;
  if (disk_ioctl(0, GET_SECTOR_COUNT, &sectors) != RES_OK) return false;
 
  if (usb_initialized)
    USBComposite.end();
  usb_initialized = 1;
  usb_reenumerate();
  USBComposite.clear();
  USBComposite.setProductId(VENDOR_ID);
  USBComposite.setProductId(MASS_STORAGE_PRODUCT_ID);
  MassStorage.setDriveData(0, sectors, sd_read, sd_write);
  MassStorage.registerComponent();
  console_setMainSerial(NULL);
  USBComposite.begin();
  while (!USBComposite.isReady());
  return true;
}


I have not tried the MIDI support, however, but the USBSerial and USBMassStorage works for me.

I can use my laptop but I's slightly inconvenient.
Regarding using USB, I looked into it but I couldn't find any USB libraries that are compatible with the STM32Duino core. USBComposite requires a different core that I was unable to get working. I might try again when I have more time on my hands. I'm kinda busy this week.
« Last Edit: October 07, 2020, 04:45:34 AM by profdc9 »

Offline TMaxElectronics

  • High Voltage Technician
  • ***
  • Posts: 111
  • Karma: +5/-0
    • View Profile
    • My random (and very empty) electronics blog
Re: Polyphonic Interrupter with STM32F103C8T6 (Bluepill)
« Reply #12 on: October 07, 2020, 02:35:18 PM »
The biggest trouble is setting the USB config descriptor for midi... that took me a few days to get right on the midiStick, the best info I found is the original spec (from 1999 :P). Just google "midi10.pdf".

Once you have that working and manage to receive packets, you deal with them just like normal midi packets, but keep in mind that the actual packet starts at data[1] as the data[0] is the virtual cable ID.

Offline Zipdox

  • High Voltage Technician
  • ***
  • Posts: 129
  • Karma: +1/-0
    • View Profile
    • Zipdox
Re: Polyphonic Interrupter with STM32F103C8T6 (Bluepill)
« Reply #13 on: October 24, 2020, 07:02:13 PM »
I fixed pitch bend and made the program calculate the lookup tables at startup. I also uploaded the code to GitHub.
https://github.com/Zipdox/STM32_Interrupter
Pitch bend demo

Offline Zipdox

  • High Voltage Technician
  • ***
  • Posts: 129
  • Karma: +1/-0
    • View Profile
    • Zipdox
Re: Polyphonic Interrupter with STM32F103C8T6 (Bluepill)
« Reply #14 on: October 28, 2020, 07:27:43 PM »
The pitch bend implementation didn't bend new notes so I made an array for each channel's bend and check for it in the noteOn handler.

Offline Max

  • High Voltage Technician
  • ***
  • Posts: 131
  • Karma: +10/-0
  • "With the first link, the chain is forged. [...]"
    • View Profile
    • My Youtube Channel
Re: Polyphonic Interrupter with STM32F103C8T6 (Bluepill)
« Reply #15 on: October 29, 2020, 04:35:58 PM »
One more time I shall try to give feedback...


Thumbs up for putting the code on GitHub! Makes it much easier to see what you actually changed.
Pitch bend sound good! Is that "glitch" at the end of each note you who changes the note on the keyboard while releasing the pitchbend wheel or is it some kind of artifact generated by the interrupter itself...?

Remember you're not on your usual Arduino Uno with a 16MHz tiny Atmel 8 bit micro with 2kB SRAM. You have a 72MHz 32bit Cortex M3 with 20kB SRAM and advanced stuff like pipelining. Or in other words, for what you're currently doing, this thing is plain overkill. And that's good because it means you can write more flexible and comprehensive code instead of sacrifying everything for the performance.

F.ex. if you ever want to change the ontime, most interrupters have a knob they can turn. Simple, right? Works for pretty much every coil. You on the other hand have to recalculate 128 values, put them in the source, and reflash the microcontroller. For what time saving? Looking a value up from an array takes 1-2 clock cycles. You do this to avoid a division, which takes 13 clock cycles if I remember right. At 72MHz that's around 150ns slower, yes, nanoseconds. Of course this adds up but as I said, you have plenty of CPU time. And if 150ns are an issue, you should stop using Arduino libraries.
Another limitation of your LUTs is that you reduce your pitch bend resolution because you can't write an LUT for all 16384 pitches. More on this issue later.

You're happily mixing uint8_t, uint16_t, uint32_t. Good that the Cortex M3 handles all of them natively. However with growing complexity it'll become harder to keep track of what's what. Additionally every operation that mixes two types will take 1 additional clock cycle for converting one of them to the other's type. Doesn't matter here, but you should know. As long as you're not using 19.99kB out of the 20kB, I'd suggest to keep everything in uint32_t for as long as possible.
Btw. why do you write uint32_t but int instead of int32_t? The beauty about the stdint types is that they explicitly specify the size. A uint32_t is a uint32_t. But an int is a int16_t on the ATMEGA328 and an int32_t on a Cortex M3. Even ignoring that issue it does look quite strange to me... (whatever that's woth)

An issue I had quite early with my interrupter is that doing all of the math in the MIDI handler took too long. It caused the interrupter to miss MIDI commands, causing hanging or missing notes. I don't know how the Arduino MIDI library works but it may be worth thinking about it. Once you have two, three channels that bombard you with a couple controller sweeps at the same time (pitch bend, volume increase/decrease, modulation, pan, etc), processing sums up quickly. You may want to test how fast the processing is, to get a feeling of how much this is an issue (or not). Take a "rich" MIDI file (not a 12kB one, a big one like 60-80kB) and throw more and more channels towards your interrupter. If it handles 16 channels fine - great! If it struggels already with 3 channels, think about separating MIDI handling (channel.pitchbend=pitchbend) and processing (= applying MIDI commands to the notes and output signal). (Increased) buffering can help, too. But too much buffering causes delays; play with it.

Generating the output signals entirely in software is okay as long as everything is fine. However, what if you ever generate a fault condition? Or have a bug in one of the MIDI handlers and the MIDI.read() takes WAY too much time? It'll be hit or miss what happens to your ontime in such a case. Maybe you don't see an issue here at all. Otherwise you could in a first step include the digitalWrite in the sampling ISR. This guarantees at least that MIDI handling can't goof up the output signal timing. It doesn't help with fault conditions though. One would have to know how Arduino implements the Fault interrupts of the Cortex M3... Issue for another day I guess.

Now coming back to the pitch bend issue. You clearly don't want to do float calculations at runtime which makes sense because the Cortex M3 can't do float calculations natively (the Cortex M4F can), making them very expensive (btw. does anyone know how expensive?). However at the same time you struggle with the consequences of this. It may be a good idea to swap to fixed point math, meaning that you handle an 32bit value not as if they'd count 1, 2, 3, ... but 1/128, 2/128, 3/128, ... or any oder divisor. This way you get the resolution you need, without having to work with floating point numbers. @Netzpfuscher does this in his UD3, which is btw. based on a Cortex M3, too.
Less efficient but possibly very helpful in some situations might be to use float functions that can't be replaced easily (f.ex. powf()) in limited cases even at runtime... I mean, @TMaxElectronics interrupter does it without FPU, at only 48MHz and no pipelining, so your Cortex M3 should handle that without problems, too. Edit: His PIC does actually have pipelining and seems to be rather close performance wise.

Something that may help in the future is the Arduino TaskScheduler library. It's not an RTOS, but it does help with doing multiple things at the same time at different update rates and it's super easy to use.

One final question: Do you plan to add one knob or two for controlling things like the ontime...? Or will you keep it hardcoded?


Kind regards,
Max
« Last Edit: October 31, 2020, 02:55:29 PM by Max »

Offline Zipdox

  • High Voltage Technician
  • ***
  • Posts: 129
  • Karma: +1/-0
    • View Profile
    • Zipdox
Re: Polyphonic Interrupter with STM32F103C8T6 (Bluepill)
« Reply #16 on: November 02, 2020, 08:41:44 PM »
I think the glitch is either because of the release of the pitch bend wheel, or because of pressing the next key before releasing the previous one. Could be both as well.

I'm not very experienced with C. I'm also glad that this cheap thing is a beast and I have the flexibility to write imperfect code.

I was already planning on writing some code to fill the onTime LUT at startup as well. I haven't gotten around to doing it yet. I might add a potentiometer too and make the controller read it at startup.

When I selected the data types I chose the smallest viable option to save memory space for the lookup table. That might not have been necessary.

I tried writing MIDI parsing code myself once but I couldn't get it to work and I didn't think it was that important so I stuck to the library. If I ever run into performance issues I will reconsider.
With regards to rich MIDI files, I don't plan on handling control change messages, since most of them don't apply to a Tesla Coil. I'd honestly worry more about saturating the serial interface than handling the pitch bend messages at this point. It's not like a Tesla Coil will sound good with more than 5 or so notes playing at once.

I chose a small LUT for the pitch bend because most pitch bend wheels on MIDI keyboards are only 128 bit anyways, and it's pretty hard to tell the difference anyway unless you're sweeping very slowly.
As for fixed point math, what library do you suggest? I found one on GitHub for Arduino but it doesn't have any documentation at all.

What's the advantage of using a task scheduler instead of interrupts?

Yes I'll probably add a knob when I finish up the hardware enclosure.

Offline Max

  • High Voltage Technician
  • ***
  • Posts: 131
  • Karma: +10/-0
  • "With the first link, the chain is forged. [...]"
    • View Profile
    • My Youtube Channel
Re: Polyphonic Interrupter with STM32F103C8T6 (Bluepill)
« Reply #17 on: November 03, 2020, 02:20:07 AM »
Quote
I was already planning on writing some code to fill the onTime LUT at startup as well. I haven't gotten around to doing it yet. I might add a potentiometer too and make the controller read it at startup.
Honestly I still don't quite understand why you need to calculate those at startup instead of doing it at runtime. I mean, for pitchbend etc I get it, it involves rather complex floating point math like powf. But ontime or duty is in the worst case a multiplication and a division. Nothing to worry about performance wise.

Quote
When I selected the data types I chose the smallest viable option to save memory space for the lookup table. That might not have been necessary.
Probably not necessary. Check the Arduino compiler output; it tells you how much space your variables use. That's not quite equal to the maximum memory footprint, but should give you an estimation.

Quote
With regards to rich MIDI files, I don't plan on handling control change messages, since most of them don't apply to a Tesla Coil. I'd honestly worry more about saturating the serial interface than handling the pitch bend messages at this point. It's not like a Tesla Coil will sound good with more than 5 or so notes playing at once.
I partially agree. It doesn't hurt to test with a rich file; never wrong to know where are the limits of your setup. The serial port can handle quite a bit of data before it saturates. With the usual 3 bytes per command, the default MIDI baud rate of 31250baud/s allows over 1000 commands per second. That should be enough even for most of the more rich MIDI files. I also think that while the interrupter doesn't need to implement every command, it still must be able to handle them correctly (=ignore fast). After all you want to simply play your MIDI channels, without having to remove a ton of commands first.

It is true that most controllers have little to no relevance but not all. IMO there're a few worth considering.
If you happen to have a MIDI file that sets a custom pitch bend range and your interrupter doesn't support it, the file is pretty much unplayable. It'll sound completely wrong (what a surprise). However, tbh, there aren't many files using this and it would require some major changes to your code.
Way more common are nuances (changes in volume) and I do think they make sense, even on a tesla coil. This is done via CC7 (volume) and CC11 (expression) which you can basically treat as the same. You also have CC1 (modulation, = low frequency oscillation of the volume). Here's an example of modulation (together with the pitch bend) at 2:29: https://www.youtube.com/watch?v=H2ykCsD_b5g&t=2m29s The video includes quite a few volume/expression commands, too.  Edit: This video contains even deeper modulation (2:49 and following): https://www.youtube.com/watch?v=unStrSzmy7c&t=2m49s Two CCs you should definetly implement are CC121 (All Sounds Off) and CC123 (All Notes Off). They are f.ex. used when stopping or pausing playback of a MIDI file. Depending on the MIDI software you (or someone else) uses, you could otherwise be facing hanging notes after a playback pause or stop.

Quote
As for fixed point math, what library do you suggest? I found one on GitHub for Arduino but it doesn't have any documentation at all.
I have no idea if there are any fixed point math libraries out there... Since the microcontroller I use has an FPU I did all my math with floats, so I have no experience whatsoever with fixed point stuff. However, you probably don't need an Arduino specific library. You should be able to use any C/C++ fixed point math library if you can find one. Alternatively you can read a bit more about it and implement it yourself. + an - shouldn't require any modification, I think (!) * / wouldn't be that hard either, maybe a few bitshifts.

Quote
What's the advantage of using a task scheduler instead of interrupts?
It allows you to do multiple things at the same time. Lets say you connect a couple potentiometers. analogRead is - compared to normal calulations - pretty slow with usually 100k-1M conversions per second. That's 72-720 clock cycles if your microcontroller runs at 72MHz! For this reason you don't want to continuously read your analog pins in the loop. Instead you could f.ex. read them only every 10ms. For a potentiometer this is more than enough. Now you can't use delays in your loop because it has to be as fast as possible to get correct output signals. So you'd write something like if(millis()>lastCall) {analogRead...} That works as good as the task scheduler library, but it get's pretty messy pretty fast if you have mutliple things at different periods running. The task scheduler library allows you to define a task (basically a function) and specify how frequent it should be called and that's it. No need not handle the millis()>lastCall stuff yourself.
Another possible task could be one that calculates the low frequency oscillation for the modulation controller in case you'd want to implement it.

As I said, its something that may be interesting for the future development because it makes it easy to add new features.


Kind regards,
Max
« Last Edit: November 03, 2020, 02:28:30 AM by Max »

Offline Zipdox

  • High Voltage Technician
  • ***
  • Posts: 129
  • Karma: +1/-0
    • View Profile
    • Zipdox
Re: Polyphonic Interrupter with STM32F103C8T6 (Bluepill)
« Reply #18 on: November 03, 2020, 03:02:31 PM »
If you happen to have a MIDI file that sets a custom pitch bend range and your interrupter doesn't support it, the file is pretty much unplayable.
I checked the MIDI documentation and it appears that custom pitch bend ranges are very difficult to implement. They're part of Registered Parameter Numbers which in turn are part of control change messages. I haven't got a clue as to how I'd do this.

Offline Max

  • High Voltage Technician
  • ***
  • Posts: 131
  • Karma: +10/-0
  • "With the first link, the chain is forged. [...]"
    • View Profile
    • My Youtube Channel
Re: Polyphonic Interrupter with STM32F103C8T6 (Bluepill)
« Reply #19 on: November 03, 2020, 11:43:04 PM »
That's right. Because it's not that common and wouldn't work with your pitchbend LUT I thought it wouldn't be your top priority anyways and didn't go into details. However, since you already read about it, let me say its not that difficult. More work than a normal controller? Yes, for sure. But not much more complicated than pitch bend I'd say.

Each registered parameter has a 14 bit address ("number"), and a 14 bit value ("data entry"). Because a controller message covers only 7 bit, there's a separate controller for the lower ("fine") and upper ("coarse") 7 bit of the RP number and the data entry. That's a total of 4 controllers.
One thing to note is that unlike normal controllers where you have controller number and controller value in one message, RP number and data are "disconnected". You'll need a variable that stores what RPN is currently selected. If you receive a data entry command, you check what RPN is currently selected, and process the data entry accordingly.
Also, the order of the coarse and fine commands is not fixed. You can't "reset" the address (or data) when receiving the coarse value and then add the fine value because the fine value could be received before the coarse value and vice versa. They must only modify their part of the address (or data).

The pitch bend range is a little bit strange because its not an actual 14 bit value, but rather two 7 bit values. The coarse one is the number of semitones while the fine value is the number of cents (1/100 semitone, not 1/128). So unlike most other 14 bit values you can't add them by bitshifting.

You can find my implementation of all common RPNs over here (line 289-330 and 344-365): https://github.com/MMMZZZZ/Syntherrupter/blob/a28066a9e04ad3ba6c50e4533b14d50cdefd7aaa/Syntherrupter_Tiva/MIDI.cpp#L289 Note: the resulting pitchBendRange is divided by 8192 such that it can be multiplied with the current pitch bend command. Usually you'd divide the pitch bend value by 8192, then multiply it with the pitch bend range. But since the pitch bend range is usually only set once at the beginning, I moved the (slower) division over there. tl;dr ignore line 310 and 350.

I also had a quick look at the code of the Arduino MIDI library and it seems like it "knows" about RPNs but I have no idea if it handles them automatically or not.


Kind regards,
Max

High Voltage Forum

Re: Polyphonic Interrupter with STM32F103C8T6 (Bluepill)
« Reply #19 on: November 03, 2020, 11:43:04 PM »

 


* Recent Topics and Posts

post Re: Question about arduino polyphonic MIDI interrupter
[Dual Resonant Solid State Tesla coils (DRSSTC)]
bozidar
Today at 09:41:14 PM
post Re: mini tesla (hopefully not a fail)
[Beginners]
bogdan
Today at 11:38:28 AM
post Re: Simple H-Bridge construction with low parasitic inductances (for SSTC or ...)
[Beginners]
davekni
Today at 02:55:51 AM
post Re: Next Gen DRSSTC
[Dual Resonant Solid State Tesla coils (DRSSTC)]
Intra
December 04, 2020, 08:03:04 PM
post Re: AGFA ADC 5155 X-ray Scanner Teardown (4 part series)
[X-ray]
Mads Barnkob
December 04, 2020, 06:54:06 PM
post Re: Simple H-Bridge construction with low parasitic inductances (for SSTC or ...)
[Beginners]
Da_Stier
December 04, 2020, 04:42:50 PM
post Re: 2017 tour of Arecibo Radio Telescope and RF equipment
[Radio Frequency]
Da_Stier
December 04, 2020, 04:39:58 PM
post [ebay EU] good offer on 1000uF 300V Epcos electrolytic caps
[Sell / Buy / Trade]
Da_Stier
December 04, 2020, 04:28:00 PM
post Re: Ericsson RRUS 11 B4 teardown
[Radio Frequency]
Da_Stier
December 04, 2020, 04:22:38 PM
post 2017 tour of Arecibo Radio Telescope and RF equipment
[Radio Frequency]
station240
December 04, 2020, 04:16:26 PM
post Re: mini tesla (hopefully not a fail)
[Beginners]
davekni
December 03, 2020, 06:58:09 PM
post Re: Have you ever tripped a breaker during normal operation ?
[Dual Resonant Solid State Tesla coils (DRSSTC)]
panjaksli
December 03, 2020, 03:28:56 PM
post Re: steam engine
[Capacitor Banks]
plasma
December 03, 2020, 01:12:03 PM
post Re: mini tesla (hopefully not a fail)
[Beginners]
bogdan
December 03, 2020, 12:05:36 PM
post Re: Single mosfet driving with GDT
[Solid State Tesla Coils (SSTC)]
costas_p
December 03, 2020, 10:55:08 AM
post Re: Have you ever tripped a breaker during normal operation ?
[Dual Resonant Solid State Tesla coils (DRSSTC)]
panjaksli
December 02, 2020, 08:57:34 PM
post Re: Have you ever tripped a breaker during normal operation ?
[Dual Resonant Solid State Tesla coils (DRSSTC)]
davekni
December 02, 2020, 08:17:09 PM
post Re: mini tesla (hopefully not a fail)
[Beginners]
davekni
December 02, 2020, 08:12:55 PM
post Re: Have you ever tripped a breaker during normal operation ?
[Dual Resonant Solid State Tesla coils (DRSSTC)]
panjaksli
December 02, 2020, 07:34:53 PM
post Re: HF Litz wire as a primary coil
[Dual Resonant Solid State Tesla coils (DRSSTC)]
davekni
December 02, 2020, 07:14:18 PM
post Re: Have you ever tripped a breaker during normal operation ?
[Dual Resonant Solid State Tesla coils (DRSSTC)]
davekni
December 02, 2020, 07:05:18 PM
post Re: Have you ever tripped a breaker during normal operation ?
[Dual Resonant Solid State Tesla coils (DRSSTC)]
panjaksli
December 02, 2020, 06:58:40 PM
post Re: Have you ever tripped a breaker during normal operation ?
[Dual Resonant Solid State Tesla coils (DRSSTC)]
Max
December 02, 2020, 06:56:50 PM
post Re: Have you ever tripped a breaker during normal operation ?
[Dual Resonant Solid State Tesla coils (DRSSTC)]
profdc9
December 02, 2020, 06:20:35 PM
post Re: Have you ever tripped a breaker during normal operation ?
[Dual Resonant Solid State Tesla coils (DRSSTC)]
panjaksli
December 02, 2020, 05:24:23 PM
post Re: Have you ever tripped a breaker during normal operation ?
[Dual Resonant Solid State Tesla coils (DRSSTC)]
buchtawill
December 02, 2020, 03:25:59 PM
post Re: Have you ever tripped a breaker during normal operation ?
[Dual Resonant Solid State Tesla coils (DRSSTC)]
panjaksli
December 02, 2020, 03:02:01 PM
post Have you ever tripped a breaker during normal operation ?
[Dual Resonant Solid State Tesla coils (DRSSTC)]
panjaksli
December 02, 2020, 01:36:04 PM
post Re: mini tesla (hopefully not a fail)
[Beginners]
bogdan
December 02, 2020, 08:55:06 AM
post Re: HF Litz wire as a primary coil
[Dual Resonant Solid State Tesla coils (DRSSTC)]
johnf
December 02, 2020, 07:43:14 AM
post Re: How to design FPGA based on EDA technology?
[Electronic Circuits]
Genterman
December 02, 2020, 06:53:52 AM
post Re: DRSSTC trouble
[Dual Resonant Solid State Tesla coils (DRSSTC)]
davekni
December 02, 2020, 03:29:14 AM
post Re: DRSSTC trouble
[Dual Resonant Solid State Tesla coils (DRSSTC)]
buchtawill
December 02, 2020, 02:43:33 AM
post Re: SKP DRSSTC = aliexpress driver + medium coil + large bricks ;)
[Dual Resonant Solid State Tesla coils (DRSSTC)]
Maju
December 01, 2020, 10:44:31 PM
post Re: mini tesla (hopefully not a fail)
[Beginners]
davekni
December 01, 2020, 07:44:14 PM
post Re: transistor markings question
[Beginners]
davekni
December 01, 2020, 07:39:48 PM
post Re: HF Litz wire as a primary coil
[Dual Resonant Solid State Tesla coils (DRSSTC)]
davekni
December 01, 2020, 07:38:53 PM
post Re: How to design FPGA based on EDA technology?
[Electronic Circuits]
petespaco
December 01, 2020, 06:30:33 PM
post Re: Question about arduino polyphonic MIDI interrupter
[Dual Resonant Solid State Tesla coils (DRSSTC)]
bozidar
December 01, 2020, 04:58:29 PM
post Re: Question about arduino polyphonic MIDI interrupter
[Dual Resonant Solid State Tesla coils (DRSSTC)]
TMaxElectronics
December 01, 2020, 04:22:10 PM
post Re: Question about arduino polyphonic MIDI interrupter
[Dual Resonant Solid State Tesla coils (DRSSTC)]
bozidar
December 01, 2020, 03:02:51 PM
post Re: Half-brigde with pot on mosfet gates question
[Solid State Tesla Coils (SSTC)]
TMaxElectronics
December 01, 2020, 12:08:56 PM
post transistor markings question
[Beginners]
NOOBPASTE51
December 01, 2020, 12:02:19 PM
post Re: Question about arduino polyphonic MIDI interrupter
[Dual Resonant Solid State Tesla coils (DRSSTC)]
TMaxElectronics
December 01, 2020, 11:55:01 AM
post Re: HF Litz wire as a primary coil
[Dual Resonant Solid State Tesla coils (DRSSTC)]
TMaxElectronics
December 01, 2020, 11:50:19 AM
post Re: Analog HFBR amplifier
[Electronic Circuits]
TMaxElectronics
December 01, 2020, 11:21:56 AM
post Half-brigde with pot on mosfet gates question
[Solid State Tesla Coils (SSTC)]
costas_p
December 01, 2020, 11:17:16 AM
post Re: Question about arduino polyphonic MIDI interrupter
[Dual Resonant Solid State Tesla coils (DRSSTC)]
bozidar
December 01, 2020, 11:11:50 AM
post Re: mini tesla (hopefully not a fail)
[Beginners]
bogdan
December 01, 2020, 09:29:24 AM
post Re: HF Litz wire as a primary coil
[Dual Resonant Solid State Tesla coils (DRSSTC)]
johnf
December 01, 2020, 07:03:56 AM

Sitemap 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 
SimplePortal 2.3.6 © 2008-2014, SimplePortal