Given that we were sampling at a rate of 6kHz we were sampling every 168µs (in actuality the period of 6kHz is 166.66µs however our timer1 gave us 168µs evenly when setting the prescaler to 64 and using the compare match interrupt on OCR1A equals 42. Thus our real sample rate was 5952Hz but we will still use the 6kHz sampling rate in the report for clarity). Given that our micro controller was running at 16MHz this translates that we had a total of 2688 cycles to get the data, put it into the buffer, process it and output it. This meant that although we had enough time to process the data we did not have the leeway to “mess around”. We had to use our time discretely and every cycle had to be accounted for.
We also ideally wanted to use up all the 2kb of memory present in the Mega 32 for the data buffer but of course we had to settle for 1.6kb since we needed to use up some space for intermediate variables and for the hardware stack. As an aside, one problem we encountered was that suddenly the LCD display was displaying garbage. The program compiled perfectly so we had no idea of what was happening. Thinking back in time we figured that the error occurred only after we had created one more char variable. We hit ourselves against the computer monitor for sometime and then understood that adding one more global variable (with the data stack held constant at a maximum of 200 bytes-already lowered from 500bytes) meant that the hardware stack had one less byte and it was overwriting other parts of memory not allocated for the system/hardware stack. We expanded the hardware stack by minimizing the data stack by 100 more bytes (down to 100bytes) and everything worked again. As you can see we were tight in time and now in memory. So how did we pull it off?
First, everything that could go in flash went in flash memory. Huge dual dimensional string arrays for LCD displaying were stored here with their corresponding pointers living in SRAM. To decrease computational burden and hence save some cycles we restricted multiplication operations and limited ourselves to multiplying and dividing by two or shifting by 1. This greatly reduces a possible 100 cycle instruction (for numbers like (NUM*.5) to a simple one cycle instruction. But this means we reduced ourselves to only multiplying and dividing by two? No. It is pretty easy to multiply by numbers like 0.125, 0.25 and 0.375 by simple shift operations which can be seen in the code in the appendix inside the “mult” function. The mult function was originally coded in C and not in assembly for simplicity however if serious timing issues would arise in the future we would have gone to assembly. Luckily we did not have this problem.
As far as program flow and organization we used time-scheduled tasks. These time scheduling tasks were achieved with the aid of Timer0 and Timer1. Timer0 was set to interrupt every half of a millisecond. Once we had this half of a millisecond timer it was easy to call functions in multiples of halves of millisecond and more practically in multiples of milliseconds. Two tasks executed every 50 milliseconds and one every 100 milliseconds or a tenth of a second. The tasks which were executed every 50 milliseconds were the button0 and button1 debouncer. The LCD display operated every tenth of a second. The debounce function identically as the debounce function we have been using all year in class. This function mainly assures when a button is pressed, it is really pressed. In code, this means that when the variable pushFlag0 is a 1, button0 has been pressed. The LCD display function basically checks if any button has been pushed (if any flags have been raised) and if so, updates the variable effectIndex, a variable holding the current effect(if button0 was pushed). If button1 was pressed the LCD display function updates the property for the effect. After these changes have been made the function proceeds to update de LCD display with the new effects or effect property.
Another thing that is occurring behind the scenes is the analog to digital conversion (ADC). The ADC is occurring constantly and when it is done with a conversion it goes into a ISR (interrupt service routine) and stores the new converted value into a temporary variable Ain. Then the ISR proceeds to reactivate the AD converter for conversion and the process repeats infinitely. Retrieving converted values via interrupts is wise since it does not stall the processor. The ADC takes about 13 cpu cycles or 0.8 µseconds to make a conversion. This means that many samples (around 208 for each sample used) are really thrown away. This apparent waste of cycles are due to the fact that conversion initiation in another function (where it should have gone) was problematic. Because we did not encounter timing problems we did not see why to fuss with the ADC if the whole circuit was behaving properly.
The heart of the program is an ISR performed every 168µs via the use of a Timer1 output compare interrupt. This ISR takes the sample from the ADC stores it into the buffer and proceeds to processes it. Once this is done, the processed sample is sent to the DAC or digital to analog converter so that it could be then go through some analog processing including low pass filtering. To do the actual coding of the guitar effects we had to study closely how the effects where defined mathematically and then try to implement it in our micro controller. For two of our four effects (mainly Reverb and Echo) we had to use past data values of our samples. Hence we had to always store our samples in our “circular” buffer. A circular buffer is nothing more than a long array of memory which when the index reaches its maximum it starts over again and it overwrites the old data. The circular buffer hence gives the impression of having infinite memory. However, this is clearly a false statement since your data is constantly being overwritten. In terms of time, a “longer” circular buffer means more time for samples to be overwritten. In our case as we mentioned earlier our circular buffer gave us at most a third of a second before our data was rewritten. In terms of effects implementation, this meant that at its best an echo arrived a third of a second after its original sound and not a millisecond later.
To implement our effects we had to first discover what
they were and how were they defined mathematically. Most of these resources
were obtained from the article “Implementing Professional Audio Effects
with DSP” by Micea et al. (see Appendices page)
Reverb or reverberation is the acoustical effect of rooms and enclosed buildings on the sound waves. On a large auditorium for instance sound waves are reflected off walls, floors, carpets, people etc. before the sound actually gets to the listener. As a result, the sound heard at any given time is the sum of the sound from the source as well as the reflected sound. Hence the equation for reverberation would look something like this.
y[n]= x[n] + gain1*x[n-d1] + gain2*x[n-d2] + gain3*x[n-d3]…
Where y[n] is the output and x[n] is the input sample and gainN are constants and dN are also time(index) constants. Luckily it turned out that the largest delay dN corresponds to about a third of a second this was good since it amounts to almost exactly the amount of memory we had for the Mega32.
The tremolo effect is a variation in amplitude gain in the original sound wave. The equation would look something like this.
Where v (volume) stands for a time varying function of
time and x and y are as above.
This function is almost always a sinusoid. However using the sine function would have taken too many cpu cycles and multiplying by any number not a divisor of two was too costly. Hence we created v[t] to be a triangle wave whose numbers where .125,.250,.375,.5,.625,.75,.875 and 1 and back down again in a triangular fashion. Using this triangle wave to multiply against x[n] was easy and cheap(in cpu cycles) since at most any multiplication would take a total of three shifts and 2 adds.
Distortion was first seen in old vacuum tube amplifiers when the signal gain was just too high for the amp to withstand. At first distortion was seen as a fault in the amplification of sound, however as time passed distortion became innate in music and even amplifiers started bringing their own distortion effects with them. Hence the musician was able to purposely distort his music and moreover, control the overall distortion he desired.
In old amplifiers distortion translated into a chopping off of the sound wave. It turns out that “nice” distortion occurs when only one side of the sound wave is chopped off and the other is left to pass uncorrupted. This gives the effect of clarity and at the same time of distortion in the overall sound. In our distortion effect we decided to cut off the top side of the wave as opposed to the bottom.
In terms of implementation, distortion was the easiest effect to implement. Because sound was digitized we could set all values above some parameter “highVal” to highVal.
Earlier in the report I started saying that we were not able to implement and echo in the 8515 and that is why we switched to the Mega32 and to other effects. The truth is that the echo came to us by surprise. Although it is true that most echo processors have parameters where you can change the total time delay (delay between echoes) and the gain (how fast the echo decays exponentially) we did not have this functionality since we did not have memory to make an echo time delay longer than a third of a second. We discovered the echo by testing our reverb effect. The reverb is in fact many echoes of shorter time delay and varying gain clustered together. Once we discovered a short echo was possible to implement we went for it. Of course we wouldn’t have all of the parameters we would have had in the 8515 but we were still thrilled that we were going for our fourth guitar effect and everything was behaving properly.
The basic echo is done simply by adding a delayed past sample to the current sound sample. The equation would look like this.
y[n]=x[n] + gain*x[n-delay]
Where delay in our case should be as long as possible or 1/3 of a second. What we would hear is hence a single echo 1/3 of a second after the original sound (not very interesting). The figure below shows a schematic of this implementation.
The other type of echo is a more realistic one and is the one we decided to implement. This echo effect is described by a first order difference equation and is seen in the schematic below. The feedback gain "loop" feeds the current output again back into the delay as seen below. This echo sounds more realistic because it has lots of echoes depending on the gain. That is, once after the original sound, we hear echoes which decay in time exponentially. It is possible thus to have no attenuation and have infinite echoes. Of course in our circuit because we had so little time between echoes if we chose an gain too big we would get too much feedback and all we’d get out is noise. Hence to be able to have a looping effect (zero gain) we must have at least enough time for the initial sound to die out before the echo is received. Below is a schematic of the echo effect implementation and we invite you to look at the code for it’s implementation.