Project

EFM8 Sound Synthesizer: Playing Melodies via USB

September 17, 2015 by Robert Keim

Part 3 in the “How to Make an EFM8-Based Sound Synthesizer” series.

Part 3 in the “How to Make an EFM8-Based Sound Synthesizer” series.

Previous Articles in the Series

This article is part of a complete project on making an EFM8-based synthesizer. Please begin with the following:

Required Hardware/Software

Description Quantity Digi-Key p/n
Breadboard 1 377-2094-ND
Receptacle-to-plug jumper wires 4 1471-1231-ND
5 V AC/DC wall-mount power supply  1 1470-2771-ND
0.1 µF capacitors 5 399-4266-ND
Fifth-order switched-capacitor lowpass filter 1 LTC1063CN8#PBF-ND
General purpose op-amp 2 LT1638CN8#PBF-ND
Analog power buffer 1 LT1010CN8#PBF-ND
2 µF capacitor 1 490-8835-ND
10 kΩ resistors 2 10KQBK-ND
100 Ω resistor 1 100QBK-ND
8 Ω, 1 Watt speaker 1 GF0771-ND

Project Overview

At the end of the previous article, we had firmware and audio circuitry that together could generate filtered, amplified musical-note-frequency sine waves capable of driving a speaker. We even discovered that the tones produced by this relatively simple system are surprisingly euphonious. But our sound synthesizer will gradually lose its appeal if all it can do is cycle endlessly through the C major scale, so we need a convenient way to turn the audio on and off and, more importantly, to play soothing melodies.

We will accomplish this by establishing a virtual COM port (VCP) USB connection between the EFM8 and Scilab (refer to this article for introductory information about Scilab, and this one introduces the Scilab serial port library). The Scilab script used in this project provides a simple command-line interface whereby the user can start and stop audio playback, set the tempo, and enter musical-note information for a simple melody.

Scilab

Here is the script:

EFM8_SoundSynthesizer_Scilab_Script.zip

First, Scilab configures and opens the VCP. If for some reason the serial port library cannot establish a connection with the EFM8, the Scilab console will state that the COM port cannot be opened and then “abort,” which is the word Scilab uses for “cease script execution and return to normal operation.” If the connection is successfully established, the script enters an infinite command-line-input loop in which the Scilab console window prompts the user to enter a string of characters to send to the EFM8. To send a command, you simply type the appropriate characters then press the enter key. To break the loop and exit the script, you press enter without typing any other characters (note that the VCP connection is closed and the serial library is “unloaded” before the script terminates).

The EFM8 firmware recognizes four types of commands: play audio (“P”), stop audio (“S”), set tempo (“T###”), and create melody (command format discussed below). The play and stop audio commands turn on or off the sound-signal and filter-clock square waves, without otherwise interfering with the normal operation of the firmware. The set tempo command is used to control the playback speed; the tempo is entered in beats per minute (BPM). The firmware currently restricts the tempo to between 60 and 180 BPM, so numbers above or below this range will be changed to 180 or 60, respectively, by the EFM8.

The method of creating a melody may appear somewhat cryptic, but it is easy to implement and can actually be rather quick and intuitive if you are looking at a piece of music while you type in the characters. The melody is entered as a sequence of character pairs comprising a letter (which represents the musical note) and a number (which represents how long the note is played). The firmware currently supports whole notes, half notes, quarter notes, and eighth notes.

When entering a melody, you use “1” for a whole note, “2” for a half note, “4” for a quarter note, and “8” for an eighth note. For the notes themselves, you use the corresponding letter. The command-line interface supports two octaves beginning with C and ending with B (no sharps or flats). The notes in the lower octave are denoted by lowercase letters, and those in the higher octave are denoted by uppercase letters. So if you wanted to play all the notes supported by the interface, in ascending order and with each note being a quarter note, you would enter “c4d4e4f4g4a4b4C4D4E4F4G4A4B4.” Note: The current firmware supports notes only as high as E in the second octave; the higher notes were not needed for the demonstration melodies, and these notes would be progressively more attenuated by the RC lowpass filter, as discussed in the previous article. Also, at some point the filter-clock signal will become unreliable because the PCA interrupts will occur so frequently that the EFM8’s processor will not be able to consistently update the capture/compare register. You can certainly extend the firmware to include these higher notes, but you need to keep an eye on the filter-clock square wave, and you may need to modify the RC lowpass filter for a higher cutoff frequency.

Additional details regarding the command-line interface:

  • For tempo commands, the ASCII representation of the tempo value is converted to a single-byte number so that the EFM8 does not need to perform this conversion. The other commands are passed directly to the EFM8.
  • Playback is automatically enabled when you send a melody command.
  • All commands are echoed back by the EFM8, as shown below in the command-line example.
  • You can specify a rest (i.e., a period in which no note is played) using “r” followed by “1,” “2,” “4,” or “8”; the number specifies the duration of the rest as that of a whole, half, quarter, or eighth note.
  • You can set the tempo at any time, but the playback speed will not change until you enter a new melody (or re-enter the current melody). The default tempo is 120 BPM.
  • Pressing the up-arrow key at the command prompt allows you to cycle through previously entered strings; this is extremely helpful when you are sending long melody commands and just want to change one note, or if you need to re-enter a previous melody.
  • For the sake of simplicity, all commands are currently limited to one 64-byte USB packet. Each note requires two bytes, so the maximum melody length is 32 notes.

Below is an example showing proper use of the command-line interface. The script is executed, and the EFM8 is programmed to play an ascending C major scale with all quarter notes, then a descending C major scale with all eighth notes. Next, the tempo is decreased to 60 BPM and then increased to 180 BPM. Finally, playback is disabled then enabled again before the script is terminated. You will notice that the echoed characters for the tempo commands don’t look correct, but actually they are—Scilab is displaying the ASCII character corresponding to the one-byte tempo number received by the EFM8.

USB Configuration

This article provides an overview of USB communication using the SiLabs VCPXpress library, along with important aspects of adding VCP functionality to a Simplicity Studio project. Here we will briefly review all the steps involved in incorporating VCP communication into our firmware:

1. Copy “VCPXpress.h,” “VCPXpress.lib,” “descriptor.c,” and “descriptor.h” into your project directory:

2. Add the proper include path in the project properties:

3. Add the VCPXpress library in the project properties:

4. Insert a source file that contains code for interacting with the VCPXpress library and processing USB packets:

5. Insert code for new function prototypes, global variables, and preprocessor definitions (build errors will help you to find anything you may have missed), and include “VCPXpress.h” and “descriptor.h” in any source files that require them:

6. Add calls to USB_Init() and API_Callback_Enable() after the call to your hardware initialization function:

7. To suppress linker warnings, copy “OVERLAY(?PR?_USB_WRITEFIFO?EFM8_USBDEP ! *, ?PR?_USBD_READ?EFM8_USBD ! *, ?PR?_USBD_WRITE?EFM8_USBD ! *, ?PR?_VCPXCORE_WRITE?VCPXPRESS ! *)” into “Additional Flags” in the linker settings:

Interrupt Priority

The only change in the EFM8’s peripheral/interrupt configuration is a subtle but important one: the PCA interrupt has been set to high priority.

Initially the filter-clock square wave output was seriously unreliable, presumably because higher-priority interrupts associated with the VCPXpress library were preempting the PCA interrupts. This modification to the priority settings ensures that the PCA interrupts are handled in a timely fashion.

Functional Details

The firmware produces the melodies by converting the command string into a sequence of notes and a corresponding sequence of Timer3 counts, with each sequence stored in a separate array:


void StoreNewMelody()
{
	unsigned char x,y;

	y = 0;

	for(x = 0; x < USBBytesReceived; x += 2)
	{
		NotesSequence[y] = USBRxPacket[x];

		switch(USBRxPacket[x+1])
		{
			case '1':
				Timer3Counts_Sequence[y] = WholeNoteCounts;
				break;
			case '2':
				Timer3Counts_Sequence[y] = HalfNoteCounts;
				break;
			case '4':
				Timer3Counts_Sequence[y] = QuarterNoteCounts;
				break;
			case '8':
				Timer3Counts_Sequence[y] = EighthNoteCounts;
				break;
		}

		y++;
	}

	NumberofNotes = y;

	CurrentNoteIndex = NumberofNotes - 1;	//start playback at the beginning of the melody

	PLAYorSTOP = PLAY;
}

The number of Timer3 counts used for a whole, half, quarter, or eighth note is calculated when a set tempo command is received:


void SetTempo(unsigned char TempoBPM)
{
	if(TempoBPM > 180)
		TempoBPM = 180;

	else if(TempoBPM < 60)
		TempoBPM = 60;

	QuarterNoteCounts = ((float)60/TempoBPM) * 10000;

	WholeNoteCounts = QuarterNoteCounts*4;
	HalfNoteCounts = QuarterNoteCounts*2;
	EighthNoteCounts = QuarterNoteCounts/2;
}

These calculations assume that the duration of a quarter note is one beat, and consequently a whole note is 4 beats, a half note 2 beats, and an eighth note half a beat. Thus, with a tempo of 60 BPM, the notes would have the following durations:

In the main while loop, the program cycles through each element in the musical-note array and sets the sound-signal and filter-clock increments accordingly, then it uses the corresponding value in the Timer3-counts array to implement the appropriate delay.


while(1)
{
	for(CurrentNoteIndex = 0; CurrentNoteIndex < NumberofNotes; CurrentNoteIndex++)
	{
		REPEATED_NOTE = FALSE;
		if(CurrentNoteIndex < (NumberofNotes - 1))
		{
			if(NotesSequence[CurrentNoteIndex] == NotesSequence[CurrentNoteIndex + 1])
				REPEATED_NOTE = TRUE;
		}

		switch(NotesSequence[CurrentNoteIndex])
		{
			case 'c':
				Current_SoundSignal_Increment = SOUND_C5_INCREMENT;
				Current_FilterClock_Increment = FILTCLK_C5_INCREMENT;
				break;

			case 'd':
				Current_SoundSignal_Increment = SOUND_D5_INCREMENT;
				Current_FilterClock_Increment = FILTCLK_D5_INCREMENT;
				break;

			case 'e':
				Current_SoundSignal_Increment = SOUND_E5_INCREMENT;
				Current_FilterClock_Increment = FILTCLK_E5_INCREMENT;
				break;

			case 'f':
				Current_SoundSignal_Increment = SOUND_F5_INCREMENT;
				Current_FilterClock_Increment = FILTCLK_F5_INCREMENT;
				break;

			case 'g':
				Current_SoundSignal_Increment = SOUND_G5_INCREMENT;
				Current_FilterClock_Increment = FILTCLK_G5_INCREMENT;
				break;

			case 'a':
				Current_SoundSignal_Increment = SOUND_A5_INCREMENT;
				Current_FilterClock_Increment = FILTCLK_A5_INCREMENT;
				break;

			case 'b':
				Current_SoundSignal_Increment = SOUND_B5_INCREMENT;
				Current_FilterClock_Increment = FILTCLK_B5_INCREMENT;
				break;

			case 'C':
				Current_SoundSignal_Increment = SOUND_C6_INCREMENT;
				Current_FilterClock_Increment = FILTCLK_C6_INCREMENT;
				break;

			case 'D':
				Current_SoundSignal_Increment = SOUND_D6_INCREMENT;
				Current_FilterClock_Increment = FILTCLK_D6_INCREMENT;
				break;

			case 'E':
				Current_SoundSignal_Increment = SOUND_E6_INCREMENT;
				Current_FilterClock_Increment = FILTCLK_E6_INCREMENT;
				break;

			case 'r':
				SFRPAGE = PCA0_PAGE;
				PCA0CN0_CR = STOP;
				break;
		}

		//delay for the length of time corresponding to the current note
		SFRPAGE = TIMER3_PAGE; TMR3 = 0; while(TMR3 < Timer3Counts_Sequence[CurrentNoteIndex]);

A problem with this approach is that two shorter notes of the same pitch will sound like one longer note. To redress this imperfection, the code checks whether the next note is the same as the current note, and if so, the current note is followed by a short delay during which the sound-signal and filter-clock square waves are disabled:


REPEATED_NOTE = FALSE;
if(CurrentNoteIndex < (NumberofNotes - 1))
{
	if(NotesSequence[CurrentNoteIndex] == NotesSequence[CurrentNoteIndex + 1])
		REPEATED_NOTE = TRUE;
}

//-----------------------------------------------------
//then after the note has been played... 
//-----------------------------------------------------

if(REPEATED_NOTE == TRUE)
{
	SFRPAGE = PCA0_PAGE;
	PCA0CN0_CR = STOP;
	SFRPAGE = TIMER3_PAGE; TMR3 = 0; while(TMR3 < 200);
}

SFRPAGE = PCA0_PAGE;
PCA0CN0_CR = PLAYorSTOP;

SoundSynthesizer_Part3.zip

This technique allows us to actually benefit from the “click-and-pop” sounds that are usually a troublesome aspect of audio circuitry: a quick click or pop generated when the square waves are re-enabled provides an extra bit of definition between repeated notes. However, we don’t want too much click-and-pop, so the DC-blocking capacitor in the circuit has been reduced from 2 µF to 0.1 µF in an effort to moderate these artifacts, which are due in part to transient currents associated with charging filter capacitors.

The following videos provide two sample melodies; the corresponding command-line entries are given in the captions. The sound produced by this system seems vaguely reminiscent of a medieval flute, and the melodies have been chosen accordingly.

Melody command: E4a4a2E4a4a4a8b8C4C4b4C4D4D8D8C4D4E4E8E8D4D4C4C4b2a4C4b4g4a2a2; Tempo command: T180

Melody command: d2a2e2f8e4d2a2C4D2C4a4b4g4a2r2a4D2D4C2a2g4f4e2c4d2a4g2f4e4d4c4d1; Tempo command: T120. Some of the notes in this one are slurred because replacing multiple notes of the same pitch with one longer note made it possible to fit the entire melody into one 64-byte packet.

 

Give this project a try for yourself! Get the BOM.