Topics

► Games

► Sound & Music

► Clocks

► GPS

► Tools

► Tutorials

By processor

► ATtiny85

► ATtiny84

► ATtiny841

► ATtiny2313

► ATtiny861

► ATmega328

► ATmega1284

About me

  • About me

Feeds

RSS feed

Tiny Synth

11th October 2014

Tiny Synth is a minimalistic four-voice synthesiser based on an ATtiny85. It can play four note polyphony using triangle waves, with a sound like an electronic organ, and no stored wavetables are needed as the waveforms are calculated dynamically as required. The synth was designed to be as simple as possible, and can be extended to add other features such as envelopes, and different waveforms.

Tunes are composed in a symbolic notation called Ample, originally created for the Music 500 digital synthesiser for the BBC Microcomputer. It's a very compact and intuitive notation, ideal for this purpose. 

Here's the demonstration tune used in the program [1]:

C(-1:C)Fe 2,dE | 4,F(-1:D) 2,eF 4,A(-1:G)g | 2,f(-1:F)eF(-1:E)G 4,f(-1:D)e(-1:C) |
2,d(-2:G)Edc 4,b(-2:G)G | c(-1:C)Fe 2,dE | 4,F(-1:D) 2,eF 4,A(-1:G)g |
2,A(-1:F)gA(-1:E)Cb(-1:G)gB(-1:B)D | 4,c(-1:C)g 8,C ^(^^)

and here's what it sounds like: lukesong.mp3.

Waveforms

The Tiny Synth generates triangle waves, using the technique described in my earlier article Waveform Generation using an ATtiny85.

The Tiny Synth has four channels, so we have an array of accumulators and frequencies, one for each channel:

unsigned int Acc[4] = {0, 0, 0, 0};
unsigned int Freqs[4] = {0, 0, 0, 0 };

The octaves are numbered from -4 to 4, where 0 is the octave containing middle C. The array Scale[] holds the frequencies calculated for the top octave, so to set a note playing the main program writes the appropriate number into the Freqs[] array corresponding to the current channel:

Freqs[Chan++] = Scale[Index] >> (4 - Octave); 

Mixing the four channels

The channels are combined by summing them before outputting them to the DAC. Here's the ISR that mixes the four channels:

ISR(TIMER0_COMPA_vect) {
  signed char Mask, Temp, Sum=0;
  for (int Chan=0; Chan<4; Chan++) {
    Acc[Chan] = Acc[Chan] + Freqs[Chan];
    Temp = Acc[Chan] >> 8;
    Mask = Temp >> 15;
    Sum = Sum + ((char)(Temp ^ Mask) >> 1);
  }
  OCR1B = Sum;
}
The level of each channel is reduced by a factor of 2 before adding it into Sum to ensure that the total fits into the 8-bit DAC register, OCR1B, without overflowing.

Playing the notes

We've already used both of the Timer/Counters in the ATtiny85, and we still need to output the notes of the tune at a regular tempo. Fortunately the ATtiny85 also includes a watchdog timer which provides a programmable interrupt which is ideal for this.

In setup() we set up the watchdog timer to generate an 8Hz interrupt:

  WDTCR = 1<<WDIE | 3<<WDP0;     // 8 Hz interrupt

The Interrupt Service routine ISR(WDT_vect) is then called to increment a counter GlobalTicks:

// Watchdog interrupt counts ticks (1/8 sec)
ISR(WDT_vect) {
  WDTCR |= 1<<WDIE;
  GlobalTicks++;
}

The watchdog interrupt automatically reverts the watchdog timer to causing a reset on the next interrupt; to avoid this you need to set the interrupt bit before the next interrupt:

  WDTCR |= 1<<WDIE;

The main program loop() then steps through the description of the tune in the array Tune[], decodes the Ample music notation, and sets the Freqs[] array, Octave, and Duration as appropriate.

Ample music notation

I chose a symbolic music notation called Ample for encoding the tunes to be played by Tiny Synth. This compact and intuitive notation was originally designed by Chris Jordan as part of the Hybrid Music System for the BBC Microcomputer [2]. The original Ample system was a complete Forth-like programming language [3]; I have just implemented the music notation part.

Notes

To play a scale you simply type the names of the notes. For example, this produces the scale of C Major:

CDEFGABC^

The final ^ is a rest, which silences the music.

To play a note higher in pitch than the last one you enter a different upper-case letter, and to go down in pitch you enter a different lower-case letter; so to create an up and down C Major scale you can type:

CDEFGABCbagfedc^

Repeating the same character plays the same note, irrespective of case.

Pitch

To fix the pitch of a note you can specify the octave number, followed by a colon, where 0 is the middle octave. So:

-1:C 0:C 1:C 2:C

plays four Cs, starting with the C below middle C. You can also use the symbols < and > to shift down or up an octave respectively.

You can include spaces in the tune to improve the layout; they don't affect the sound.

Note length

The note length is set by a number followed by a comma. The initial length is 4; for example, here's the national anthem:

4,CCD | 6,b 2,C 4,D | EEF | 6,e 2,d 4,c | Dcb | 12,C^

You can include | bar lines to indicate the end of each bar; these don't affect the sound.

You can extend a note for the same duration with a / (tie), so you could write the tune as:

2,C/C/D/ | b//CD/ | E///F/ | e//dc/ | D/c/b/ | C/////^

Sharps and flats

To play a sharp precede it with a +, and to play a flat precede it with a -. So to play all the semitones in an octave (white and black notes) you could write:

C+CD+DEF+FG+GA+ABC^

Playing chords

Tiny Synth can play notes on up to four voices. So far all the examples have played on the first voice. To play notes on the second voice enclose it in brackets. For example:

c(G) c(A) F(A) d(G) ^(^)

Each successive note in a bracketed group plays on a different voice, so we can play 4-note chords by writing:

c(GEC) c(AEG) F(ACE) g(GCE) ^(^^^)

The notes inside the brackets don't affect the notes outside, so the notes outside the brackets play as they would have done if the chords weren't there.

Tiny Synth main loop

Here's the main loop of the Tiny Synth program, which parses the Ample music notation:

void loop() {
  char Sign = 0, Number = 0;
  char Symbol, Chan, SaveIndex, SaveOctave;
  boolean More = 1, ReadNote = 0, Bra = 0, SetOctave = 0;
  do {
    do { // Skip formatting characters
      Symbol = pgm_read_byte(&Tune[TunePtr++]);
    } while ((Symbol == ' ') || (Symbol == '|'));
    char CapSymbol = Symbol & 0x5F;
    if (Symbol == '(') { Bra = 1; SaveIndex = LastIndex; SaveOctave = Octave; }
    else if (ReadNote && !Bra) More = 0;
    else if (Symbol == ')') { Bra = 0; LastIndex = SaveIndex; Octave = SaveOctave; }
    else if (Symbol == 0) for (;;) ;          // End of string - stop
    else if (Symbol == ',') { Duration = Number; Number = 0; Sign = 0; }
    else if (Symbol == ':') {
      SetOctave = 1; Octave = Number;
      if (Sign == -1) Octave = -Octave;
      Number = 0; Sign = 0;
    }
    else if ((Symbol >= '0') && (Symbol <= '9')) Number = Number*10 + Symbol - '0';
    else if (Symbol == '<') Octave--;
    else if (Symbol == '>') Octave++;
    else if (Symbol == '-') Sign = -1;
    else if (Symbol == '+') Sign = 1;
    else if (Symbol == '/') ReadNote = 1;
    else if (Symbol == '^') { Acc[Chan] = Silence; Freqs[Chan++] = 0; ReadNote = 1; }
    else if ((CapSymbol >= 'A') && (CapSymbol <= 'G')) {
      boolean Lowercase = (Symbol & 0x20);
      int Index = (((CapSymbol - 'A' + 5) % 7) << 1) + 1 + Sign;
      if (!SetOctave) {
        if (LastIndex && (Index < LastIndex) && !Lowercase) Octave++;
        if (LastIndex && (Index > LastIndex) && Lowercase) Octave--;
      } else SetOctave = 0;
      Freqs[Chan++] = Scale[Index] >> (4 - Octave);
      LastIndex = Index;
      ReadNote = 1; Sign = 0;
    } else digitalWrite(Error, 1);  // Illegal character
  } while (More);
  TunePtr--;
  NextTick = NextTick + Duration;
  do ; while (Ticks() < NextTick);
}

Circuit

The PWM output can be fed straight to an 8Ω loudspeaker, via an electrolytic capacitor to remove the DC. The inductance of the loudspeaker filters out the high-frequency components of the waveform:

TinySynth.png

If you're using an external power supply and you get distorted sound or no sound you may need to decouple the supply with 100nF and 47µF capacitors across the supply lines near the ATtiny85.

To feed the output to an audio amplifier you must include a low-pass filter, otherwise you risk overloading the amplifier. See Waveform Generation using an ATtiny85 for a suitable circuit.

I compiled the program using the ATtiny core extension to the Arduino IDE [4], setting the fuses with the ATtiny85 @ 8MHz (internal oscillator; BOD disabled) option on the Board submenu, and uploaded the program to the ATtiny85 using the Tiny AVR Programmer Board; see ATtiny-Based Beginner's Kit.

Here's the whole Tiny Synth program: Tiny Synth Program.

Extensions

Here are some suggestions for developing Tiny Synth:

  • Add envelopes to the notes.
  • Allow each voice to use a different waveform.
  • Add a drum track based on digitally generated noise.

I hope to add some of these features to Tiny Synth in a future article.

Addendum

17th October 2014: I've updated the Tiny Synth program to correct an error in the Ample parsing.

13th March 2016: I've changed the format of the PROGMEM declaration for compatibility with the Arduino IDE 1.6.x.

27th March 2016: I've initialised Sum to 0 in ISR(TIMER0_COMPA_vect), which avoids clicks between notes.


  1. ^ Thanks to Luke Johnson-Davies for composing the demonstration tune.
  2. ^ Hybrid Music System on Colin Fraser's site.
  3. ^ Hybrid Music System Music 5000 Synthesiser User Guide (PDF) on Chris's Acorns.
  4. ^ ATtiny core for Arduino: arduino-tiny on Google Code.

blog comments powered by Disqus