dzlonline/the_synth

Adding more voices

Opened this issue · 39 comments

Is it theoretically possible (i.e. possible with the Arduino's processor) to add more voices to the output?

You can add more voices but we have found that the best compromise between
audio quality and processor overhead is 4 voices. The Arduino has roughly
20% processor power left when running the synth with 4 voices.
On Feb 27, 2015 3:56 AM, "Jesse Friedman" notifications@github.com wrote:

Is it theoretically possible (e.g. possible with the Arduino's processor)
to add more voices to the output?


Reply to this email directly or view it on GitHub
#2.

Last week I've tried to make a 8-voices version of the synth. It's seems to work great, but I haven't try it with anything else than a very little program, and no other things running (i.e. graphical interface, buttons/pots reanding and processing, etc.)
Perheaps by running/porting it to a bigger arduino, like Mega or, better, Due?

The number of voices are limited by processing power vs. sampling rate. Mega has the same processing power as UNO and DUE is a different hardware architecture. I am currently working on a hardware abstracted version of the synth that will run on different platforms.

I think 4 voices/oscillators they are enough for these kind of audio engines. I've been looking around about what I want do with this great libray and I have few issues I would like to talk about.

First at all, I couldn't find a technological explanation about why sample rate is set to 20kHz. Isn't possible to get higher sample rate just because Atmel limitations? Or its because with an higher sample rate the microprocessor is too much busy in generating pwm that it doesn't let any more coding?
If 20kHz is the max sample rate possible should be necessary to apply a 10kHz low pass filter to prevent anti-aliasing (is that the RC circuit described inside the comments?)

Another doubt I have (because I'm a noob and autodidact in diy) it's about wavetables and related PWM output. Wavetables have values in between -128 and 127 (8 bit range), as PWM digital output. And I have seen in the library's code the zero point of the wave is 127. So -127 in the wavetable is 0 in the PWM output. so, ground pin of the output jack should be connected to some voltage corresponding to 127? 2.5 V?

So, I would like to speak about few improvement I don't know if they are possible to get (I just wantto know if they are technologically possible):

  1. Is it possible to get one or more digital filters? I mined in the internet and the equation for filtering is very difficult for me to translate to c++ code. I don't actually know if arduino microprocessor can solve it without performance problems.
  2. Add one or more LFOs capable to modulate osc amplitude/pitch, filter cut off . I just wanna know if it technologically possible with this microprocessor or if it should be necessary to move to arduino due technology.
  3. Add the possibility to tweak envelope throught standard ADSR parameters. This sure can be easily made, right?

Mainly I can understand how the libray works, my main problem it's about output value calculation, because I can't really understand those lines of code :-)

Last. How can I get the output value? I want it to drive a led in relátion with the output.

Alberto

Hi Alberto,

I won't be able to answer all your question, but I can tell about what I've done.
The 8-channels port of the synth I've made works well. It's used on a monome-like object with an 8x8 pad, and unless I push on eight pads at once, the output is great, and it runs without problem.

I think the 20kHz is a right choice between what you can output that will be earable with a simple DC filter, and the arduino limitations. At 20kHz you have 800 ticks per timer interrupt. These 800 ticks are the room left for everything that has to be computed: your program, the ISR routine, etc. I don't know exactly how much it takes, but I think every operation in your program takes at least one tick to compute.
Plus, you have to let the second timer enough time to generate PWM. As it's an 8 bit timer, it needs at least 256 ticks to "output" a value, and it's better to let it run two or three times for each computed values. 256 ticks "scales down" the frequency to 62500. So 20kHz is around three TIMER2 loops for each output value.
It gives you a max earable frequency of 10kHz, that is sufficient for most musical applications.

Digital filters are probably feasible. But I wouldn't be able to do it.

LFO are probably easy to implement. In the formula that creates the PWM output there is the amplitude parameters that already do it. (AMP[0], AMP[1], etc.) This progressively decreases the output level by a ratio that is taken from the envelope table. One can imagine a second ratio that can be taken from one of the waveform table, but with a low frequency.

ADSR can be made too. I wanted to create something similar, but I stopped working on my project as suddenly as I started to. The idea would be to replace the value from the enveloppe table by values from dedicated tables, or values generated on the fly. You've got the EPCW table that stores an accumulator that is used to know what value to take in the enveloppe table, given the length the sound must have. The sound is started by setting the EPCW to 0, and stopped when it's more than 0x8000. This EPCW value could be tested against ADSR limits. For example between 0 and 1000 you use an attack table, then from 1000 to 3000 a decay table, and so on. As the memory space is limited, tables could be the same as those used for enveloppes and waveforms.

Output computation deals with pointers. This is clearly a hard tool to get, but when you understand it it changes life! ;) There are some greats readings about it around the Internet, you'll maybe need to read several ones before to get it. I understood it with this one: http://www.cplusplus.com/doc/tutorial/pointers/

The easiest way to drive your led would simply be to connect it on the OCR2A or OCR2B pin, as these are updated with the output value. Or, you can write the value to a var instead of the pin in SIR routine. Or you can also read the OCR2A/B value when needed.

Pierre-Loup.

Wow! Pierre! I really appreciate your complete answer. It makes me undestanding a lot of useful stuff.

Sure lfo and adsr should be quite easy to get, filter sure I'm not able. I'll tweak a little bit.

Thanks for suggesting me to use OCRA/B values to drive a led. This explanation open another doubt: do OCRA/B pins occupy by timer interrupts when using them or can be used for other purposes?

I understand pointers, but I can't get how to optimize code using them. Anyway thank you very much, keep in touch in git hub

Albertl

Hi Alberto,

You should have a look at the Atmel documentation for atmega328p. You can use timers and keep the OCRxA/B pins free for another use, but if you want to output PWM the pins will be tied to the timer. So, in our case, you can use the timer2 and output it on its two pwm pins (that is what The Synth does), or leave the pins behave as input/output. In that case I don't know how you can get the output values.

I've written a mistake yesterday about the value you can get by reading OCR2A/B: the value you get is the pwm ratio (i.e. a value between 0 and 255), not the 0 or 1 that is readable on the pin.

Hi Pierre
this project is very interesting, i have 2 question:

  1. i don't undenstand hot to play 4 voice via Midi but all voice in same channel eg channel 1
  2. is possible to use a DAC instead PWM ? i would like to use 2 double dac like MCP4822 for obtain 4 different separated wave output, this dac use SPI protocol on pin D10 (Cs) ,D11 (MOSI) and D13 (SCK)
    Many thanks
    Regards
    Mirco

Hi Mirco,
I'm not sure I understand what you mean by playing four voices via MIDI. Do you want to use the mTrigger function, or do you want to map a MIDI input to the Synth?
mTrigger just uses the channel you want to play, and start it with a MIDI note. You can of course tune waveform, enveloppe and length by calling setupVoice for this channel before.
A "channel" on the synth is not the same as a channel in MIDI. It's a single voice with its parameters, that is computed and updated on every main interrupt, for the PWM timer. So if each time you trigger a new sound on a channel, the previous one is cut if running, and you hear the new one. If you want polyphony, you have to map each new sound you want to play to another channel. On the port I made of this library, I've add a function that auto-map a new sound to an not-playing channel, or to the oldest playing channel.

About external DAC, yes I think you can use it. You will have to modify the main ISR to send the updated value to SPI instead of PWM. Maybe you will have to use direct register access to save time on the Arduino SPI library. I've never used it, so I cannot tell.

I don't know exactly what's the goal with your project, but maybe you could use a DAC that is more audio-oriented than the which you plan to use. This one will output a signal centered on a positive tension, so waving between, say, 0 and 5V. An audio DAC will have charge pumps to have a "real" audio waveform, waving around 0v. Pimoroni make a great board with such a DAC, but sadly it's driven by I2S, so is not usable with Atmega. I plan to port the library to the Arduin Due, one day... :)
Also, as the main timer interrupt already compute the mixed channels, only one will be enough for you to hear something. But I suppose if you want four, you have a good reason for this! ;)

Hope it helps.

Pierre-Loup M.

Hi Pierre, thanks for response, so about question 1 i means to play 4 polyphinic notes on same midi channel, so i play with a keyboard i need to listen a chord , in this moment i don't undenstan ho to manage your 4 trigger with 4 key play on my keyboard (i undenstand that trigger have a different channel each for each notes and it's different of midi channel), about DAC i try to modify the code in sunth.h but unfortunally dac out is mute (writeDac function is working in other code). In attach synth.h modified... have you any idea ?
Thanks Regards
Mirco
synth.h.txt

First, I have to clarify something: I didn't wrote this library, but I've used it a lot for a project of mine, and bring some modifications to it, that you can see in my repository.

So I suppose your keyboard is sending MIDI notes on a MIDI channel, say channel 1, and that you want your Arduino running the synth to dispatch your four notes each on different synth channel. One way to do it is to keep track of the last channel you've used by using a counter. When you receive your first note, you play it on first channel (numbered 0), and increment your counter. Then you receive another note, and play it on the next channel. When the counter overlaps the number of channels, then you set it back to 0. In fact each time you receive a note from your MIDI interface, then you play it on the channel number given by the counter.
On my port of the library I added two methods to get: 1/ the last channel that has been triggered; 1/ the channel whom playing is more advanced: If the four channel are already playing, and channel 2 plays a sound that is shortest (less velocity) than channel 1, then channel 2 will be used for the next play, and we will less have the feeling of a note cut before having been played. Have a look at it if you want, maybe it will help you with the right idea to suit your needs?

For your DAC problem, I won't be able to help you without viewing code. Only things I can say is basic advice, like: Have you set the pins direction right? Isn't there a kind of interrupt masking (Timer interrupt have greater priority than SPI interrupts)? Have you used volatiles variables for access in ISR?

Pierre-Loup.

I've just seen you attached code to your previous message. I'm looking at it.

I can see two things in your code that could be a problem.
First, you've commented the original wave generation equation. I have no problem with that, but you only use channel0 to compute your waveforme, thus DAC cannot play anything else. I suppose when you will have one working, you will add back the three other ones.
The second thing is in your writeDAC function. The parameter dacValue has all the 8 top bits at 0, as the TIMER1 COMPA ISR computes a 8 bit number. Then you send the four (three, one being ignored) bits that tell the chip what to do with the value it's been sent, no problem with that. But the last four will always be 0 given they are not set by the ISR. So your chip receives the value 0b0000xxxx instead of 0bxxxxxxxx. You should bit shift the dacValue before to use it. Like:

dacValue <<= 4;
data = highByte(dacValue);
//Setting your data
SPI.transfer(data);
data = lowByte(dacValue);
SPI.transfer(data);

Great, this evening i try your suggest about midi and DAC, i will let know if works fine.
Many thanks
Mirco

I check the code and is correct , i just try with one channel, but don't work, then the the highbyte 0b0000xxxx is also correct last 4 bit are used for outA setting, low byte (8) and first 4 byte of highbyte (togher 8+4=12bit) are used for dac resolution, yesterdasy i just try it but dac didn't play, furthermore i try to uncomment OCR2A but didn't play ... do you think that writedac function is in a right place of code ? any other idea ? Below the info about dac (this have 2 out)
Regards
Mirco
//******************************** MCP4822 ************************************
/*
Bitmasking for setting options in dac:
The four MSB in the Mask 0b0111000000000000 and 0b1111000000000000 is for
setting different options of the DAC setup.
0bX111000000000000 where X is What DAC channel the SPI is writing to.
bit15 X=0 is writing to channel A.
X=1 is writing to channel B.
0b0X11000000000000 where X is Buffered or UnBuffered mode. Buffered uses LDAC
bit14 pin to simuttaneous update both channels.
UnBuffered I guess is writing outputs directly to DAC
outputs and ignoring LDAC pin.
X=0 is UnBuffered.
X=1 is Buffered.

0b01x1000000000000 where X is GAIN selector.
bit13 X=0 is 2X GAIN.
X=1 is 1X GAIN.

0b011X000000000000 where X SHUTDOWN.
bit12 X=0 OUTPUT is DISABLED on selected channel.
X=1 OUTPUT is ENABLED on selected channel.

0b0111XXXXXXXXXXXX where X is the 12 bits to be written to the active channel.
bit 11 down to bit 0


I've read the datasheet this morning before to answer. I know the writeDac function is correct, except what I was pointing out. Your DAC is 12 bits, but as the output computed in the ISR is 8 bits, you have to send it as if it were an 8 bit DAC:
The DAC needs 12 bits of data: 0bxxxxxxxxxxxx.
The data you get from the ISR is 0bxxxxxxxx, which on 16 bits is 0b00000000xxxxxxxx.
So when sending this without bitshifting it left by four bits, you send a value that never exceed 1/16 of the DAC range. That's why you should bitshift it, or multiply it by 16 before to set the four (three + one void) DAC command bits on stream start. What you send to your DAC is 0bCCCC0000 xxxxxxxx, with C beeing a command bit and x a value bit, instead you should send 0bCCCCxxxx xxxxDDDD, with D beeing "don't care" bits, as the ISR outputs an 8 bit value. With bitshifting they will be 0.

I don't know if there is a best place for writing writeDac(). I would have put it in the main program, so as the SPI initialisation.
As you're calling writeDac() from an ISR, you should keep it as short as possible. digitalWrite is a "heavy function", doing some checking before eventually setting pin, direct port manipulation would be better in that case.
Also, as you probably dont change gain value and DAC output enable that often, you could precompute command bits and keep them ready when needed. And send your 16 bits at once, as Arduino as a function to send an int, with SPI.transfer16().

You maybe don't need to make this improvements, but time can be critical when generating waveforms..

Great, so i try to move SPI inizialization and writeDac in main function, about 12bit for DAC value have you a suggestion ? and about port Manipulation maybe is better to user PORTD command ? if yes what is the better way to maniputale CS pin ? after that i think that i can user SPI.transfer16 . Thanks Regards
Mirco

about 12bit for DAC value have you a suggestion ?

Bit shifting? Have you tried it? Does it work? I cannot say more. If I had lying around a MCP4822 I would have try it.

about port Manipulation maybe is better to user PORTD command ?

I suppose you mean using direct register access, so yes: you will gain a lot of time in your ISR (setting a bit in a port, or several at once, is one clock cycle. digitalWrite() can be much, much more). You can manipulate them with the same macros you used to set and clear bits in your function.

Looking at the SPI library in the Arduino source code, I'm finally not that sure you will gain from using transfer16() instead of calling transfer() twice... Give it a try, and see!

Good News, DAC now is working, now i must to add more DAC one for channel, i must to comment the line related OCR2A=OCR2B because if this uncomment is 2 octave down, frequency is a little wrong, if i play C on keyboard, dac play D, now last question, whitch the method to silent a note ? if i put len to 127 note play continally, i would like to silent when key is released, but i don't unenstand ho to do that.
if you need and desire i can load here a code modify
Many thanks
Regards
Mirco

Good new! How did you solve that?
For your several DAC, I would add a parameter in the function call for it between the two DAC (by setting the right CS pin, if my right), and the right channel on the choosen DAC. That was one of the reasons I talked earlier about the precompute of the DAC command bits.
For the ISR, I would simply duplicate the line with the call to writeDAC, with the computed value for the channel you want, and the parameter mentionned above.

I don't know why your note is two octaves down. I have never verified it on the instruments I made with this library... There must be somewhere something that devides the frequencyby four, or looking for a right bitshift of two (>>2) (but I don't think the one in the ISR is for something in your problem: this one is for diminishing the max aplitude of a channel, so that adding four channels don't cause the final wave value to go beyond limits and cause noise)

You can fine tune pitch my modifying the ISR count. In the defines at top of file you have FS, that gives the theorical sampling frequency. This value is used in the begin() function to get a precise value of OCR1A that will give this frequency. By adding or substracting to this value you will tune your real output frequency. The easiest way is to simply add the tunning value on this line, like
OCR1A = 16000000L / FS + 5; or
OCR1A = 16000000L / FS - 23;
You can also define a constant beside FS definition, that will be easiest to change it and remember what it does. Like:
#define FINE_TUNE 12, and add it in the begin() function:
OCR1A = 16000000L / FS + FINE_TUNE;

You can stop a channel before it reach its end by setting its EPCW[channel] value to 0x8000. EPCW is the enveloppe phase accumulator, that's to say it keeps track of the position of the enveloppe table it must read to set the amplitude of the sound. It's set to 0 when the channel is triggered, so the amplitude is at its max (start of the enveloppe), and go crescent until it reaches the end of the enveloppe table, where the amplitde is minimal. And when it reaches 0x8000, the ISR stops incrementing it, and set the amplitude to 0 for this channel (which is sthen silent).
So you can define a stop() function that simply set this value to 0x8000, or above.

I would be curious to see what you are building, if you have a link, video, or something else I'll visit it with pleasure!

Hi Pierre
sorry for delay, i resolved changing the SPI Inizialization, i resolved also the noteoff, now i must to cable all necessary code to drive 6 DAC, because i can drive 2 ports on same DAC (in don't undenstand why) ...
In attach the synth.h modified , i hove that you can try and test it and add on your offical code

Regards
Mirco
synth.h.txt

Hi Mirco,
By two ports on the same DAC, you mean the DAC's channel A and channel B? I don't know what can be the problem with that, your code seems right, excpet for the call to digitalWrite, that you should change to direct port manipulation.
Maybe you could "debug" that part by connecting leds to each channel of each DAC, and try a function that reach the intended goal, in a dedicated Arduino sketch. At low speed, so you can see what happens, what works and what doesn't. And then transpose it to your ISR.

Hi Pierre
exactly, i means channel A and B, if i try my code outsite this sketch it's work, but insite it's didn't work, only one channe workl, about port manipation i must to implement it and i will try dual channel with PORTD manipulation, i hope that it's work, i let you know, unfortunally in this days i have less time to dedicate at this project.

Thanks , Regards
Mirco

Hi Pierre
this evening i'm working about this project, so now with port manipulation i can play 2 notes on same dac (portA and B), now i try to add one more DAC and in this case DAC1 and DAC2 didn't play , if i comment the code of DAC1 or DAC2 other DAC works .... have you any idea ?

Thanks
Regards
Mirco

Hi Pierre
a new issue about DAC, i try to play with 2 DAC, if i transfer a value on first dac and only porta A on DAC 2 the sound are good, but if a transfer the value on portB of DAC 2 there are no sound on all DAC ... i'm cunfused, i think the problem is about SPI function, any idea ?
Many thanks
Regards
Mirco

Hello Mircos,

Given that port manipulation has solved the problem of driving the two channels of one DAC, I think the new problem is quite obvious. Supposing you've correctly set the second DAC (with its two channels).
The synth runs at 20000Hz. That means that 20000 times in a second, the TIMER 1 ISR fires. So there are 800 clock cycles between two interrupts. These 800 clock cycles are of course also used for what you wrote in the ISR. There a chance that the SPI functions overrun thses 800 clock cycles.
There is an easy way to be sure: try running the synth at a lower sample rate (say, 10000Hz), and see if it changes something. Of course it will be detuned by an octave, but you will know if the problem is here.
The sample rate is set in the #defines at the top of the synth.h file.

Aside note: I don't know how is your global sketch, but as we need the update to be ok on each sound sample, we have to be as efficient as possible. It starts by avoiding use of delay() functions, but maybe track use of digitalWrite and replace it by port manipulation (or use fast sigitalWrite library or similar). You can also check that yu conditionnal structures are organised in an efficient way (e.g. if you have five choices in an if..else structure, you better place first the most common case), and so on.

Hi Pierre
global sketch is a example sketch Midi_synth.ino, second DAC is configured like a first and work fine,in fact if i comment the spi.transger of first, second dac works fine. I have 4 spi.transfer instruction and if i comment one of these other 3 dac out works. I will try to decrease sample rate ( i must to undenstand how to do that) and i will try. Thanks Regards
Mirco

Great Pierre, with sample rate to 10000 or 15000 all DAC work fine, now i must to put 3rd DAC and try it, if i undenstud correctly, lower rate means taht i can go to last 1 or 2 octave and quality is same, if difference is this for me is not a problem 7/8 octaves are good.
Many thanks
Regards
Mirco

With another sample rate, the problem is that you will loose the ability to play high frequencies. The sample rate must be at least double the frequency you want to play. That's the reason why audio CD are sampled at 44100, it's a bit more than double 20000Hz, the max frequency audible for humans.
So with 10000Hz sample rate, you can have sounds up to 5000Hz. Maybe that won't be a problem: the highest C on a piano is 4186Hz.

There is one thing you will have to do if you change the sample rate: defining new EFTWS and PITCH in tables.h. You just have to make a prorata, i.e. take the 20000Hz and multiply each value by 2. See the difference between 20000Hz and 16000Hz tables. That way you'll keep the same pitch as before, a do being a do.

Hi Pierre, great for all info, now i can play 6 notes, in effect all notes are detune and i must to correct the tables, have a right mathematic formulas about that ? now i use a bit rate set to 14000.
Many thanks
Regards
Mirco

The formula is quite simple: values in both tables I mentionend are directly linked to sample rate (and to other paramaters, but they don't matter for what we want tot do). So you just have to take each value of the table, multiply them by their sample rate, and divide them by the new sample rate you wanna use.
Example given, for table pitch, the first value at 20kHz is 0x1A, which is 26. By multiplying it by 20000, you have 520000. Then divide it by 14000, and you get your new value, 37, or 0x25 in hexadecimal. That's that simple!
Of course This will be much more faster if you import all the values in a spreadsheet! ;)

Great Perre
of course into a spreadsheet is more easy,last question, i try to add more 2 voices but maybe i have a wrong code, please could you explain me how to add this 2 more voices (i need 6 voices in poly mode)
Many thanks for your availability
Regards
Mirco

You will have to add two values for each arrays that store voices parameters. Look at the variables declaration at the beginning of synth.h
And of course modify the code accordingly to manage this two more voices.

yes i performed that but it seems didn't work .... i try to attach the synth.h
please could you check what i wrong ?
thanks
synth.h.txt

On some tables you have added two values, but not set them. That's not a big deal, but it's better to initialize everything.
The MOD table has not been modified.
And you have got to change two things about the divider: set the right value on declaration, i.e. 6 instead of 4, and modify the test and roll-back on the if statement at the beginning of thr ISR.
Divider enable to minimize computation on each ISR, by computing the enveloppe for only one channel per ISR. That's why it's incremented, and the if statement serves two purposes: generate a tick every four ISR, and roll it back to zero when it goes higher then the number of channels. So you have to change the if statement.

Well, i changed the vaule of divider (set to 6) , about if instructions is correctly this code? or i must to change some other parameters ?

if(!(divider&=0x05))
tik=1;

Thanks

No, it won't work that way. That would be too simple!!
divider &= 0x03 make two things at once: verify if the value is lower than 4, and set divider to 0 otherwise.
It compares the value of divider (which can be 0 to 4, i.e. 0b00, 0b01, 0b10 or 0b11) to 0x03, which is, in binary, 0b11, and drops all bits above the second one. So when it reaches 4, that is 0b100, divide & 0b11 is 0, and so divide becomes 0, as we assign it the value of the comparison. (&= assignement operator.
But it won't work with values that are not multiples of 2. For example 5 is 0b101, so the comparison will fail with every value that has the second bit set, like 2 and three.

So, you can set 8 voices, two of them not being used, and use the same compare + loop back to 0 by using 0x07 for compare value.
Or you can do it in a more classic way, like compare with a value in the if statement (divider > 5) and set it back to 0 when true.

Bonjour mes amis français, Im trying to do the same with STM32F103 plus populairement connu sous le nom blue pill.S'il vous plaît.. Je ne sais vraiment pas quoi faire.aidez moi

Désolé.Je ne suis pas vraiment bon en anglais