Portable MP3 player

From Dooba Wiki
Jump to navigation Jump to search
Example build of the Maracas kit

Introduction

This tutorial will guide you through building a complete portable MP3 player.

The whole process is split into two major sections: hardware (components, layout, wiring) and software (preparation, code structure, implementation details).

We will start by going over the requirements for this project. Then we will move on to the hardware (building the MP3 player) - and then finally get into the software part (programming our MP3 player).

Objectives

Obviously there is more than one way to build a portable mp3 player. The idea here is to focus on the "basic" requirements for a minimal yet functional player.

We can sum up the key aspects we want to cover as follows:

  • something that plays MP3 (duh...)
  • reads MP3 files from a removable storage media
  • some form of minimal user interface
  • small enough to fit in a pocket
  • battery-powered (we want it to be portable, right?)

Requirements

Based on the list of objectives presented above, these are the components we will need:

This is somewhat "flexible" though - maybe we want just a small monochrome OLED for the display, or maybe we want a larger color TFT. Maybe we don't want any display at all and just want the player to sequentially go through every MP3 file available.

Here we will use the Inpad module for user input. Because this one uses I2C for communication, we included a pair of pull-up resistors for the SDA & SCL lines of the I2C bus. This is explained here. Note that we don't need to include the pair of pull-up resistors if we are not using the I2C bus.

Also, some displays (such as the one in the large ProtoBundle) include an SD card socket and therefore we don't need to include the MicroSD socket to our player when using this kind of display.

The Maracas kit

Maracas is a complete kit for building your custom MP3 player.

It includes everything you need to complete this tutorial.

The only pre-requisite is a soldering iron and some solder.

You can also go full-custom and just buy everything you want for your specific build separately.

Hardware (building the MP3 player)

Medium-sized ioProto board
Common stripboard
Typical solderless breadboard

Let's start with the basics - we will build our player on an ioProto, stripboard or similar (or even a solderless breadboard).

If you are completely new to this, take a few minutes to have a look at using prototyping boards.

This will allow us to build it however we want without the need for a custom-designed PCB (Printed Circuit Board).

There are different ways to do this, depending on personal skill level, experience and desired complexity level.

To make things easier, we first try to place the components on our board without soldering anything. By playing around with the arrangement we can come up with a layout that suits our application.

When we are satisfied with the layout, we can start soldering & wiring the components.

Board size

The size of the board is a critical aspect - it will determine the final size of our build, so we may want to have it as small as possible. However a smaller board will also make it increasingly difficult to place every component we need. We recommend going for the largest possible board that would fit in your pocket. For most cases, this would be either the Medium ioProto or Large ioProto. Some may also prefer to go with a generic piece of stripboard to get an exact specific size.

Here we will go with the easiest option: we will start by placing everything on one side of the board, then we will use simple wire to connect everything.

Why not place components on both sides?

Having components on one side makes it easier to avoid complications. When placing components on both sides of the board, we must be careful to progressively solder the components and the wiring in the proper order. It is very common to solder something only to then realize that we can no longer access some points under that part, and have to either spend a lot of time and effort de-soldering things, or abandon the build entirely. A lot of frustration can be avoided by keeping to just one side for the components.

Stacking

Even if we stick to just one side for components, that doesn't stop us from using some clever tricks to save space. A very efficient technique is to "stack" certain components.

Here is a "light" example of stacking - the Inpad user input module sits on top of both the ioNode and the Aecho:

Mp3 player stacking.jpg

Common ground

As with any electronic system, components need to share a common ground (GND).

This means that we need to connect together the "GND" pin(s) of every component.

This will become apparent when we start looking at diagrams just below.

Power

Let's start with the basics: adding a power supply.

Disable VIN-VUSB jumper

By default, any ioNode has the VIN-VUSB jumper soldered, to allow powering it directly from USB. Our ioNode needs to have the VIN-VUSB jumper NOT soldered.

We don't want to power everything directly from USB. In fact, we want to use USB power (VUSB) only for charging the battery.

Since every ioNode is shipped with VIN-VUSB soldered, we need to de-solder it. Simply heating up the small solder bridge with the tip of a soldering iron should be enough to split it.

Wiring the Nomad battery module

The Nomad module will manage the LiPo battery and provide power to the system through its two VOUT outputs (3.3V and 5.0V).

We need to connect VUSB of the ioNode to the input (VIN) of the Nomad.

Let's then connect the 5.0V VOUT from the Nomad to the input (VIN) of the ioNode.

We also want to connect the BAT LEVEL output from the Nomad to an analog input (ARD) pin on the ioNode. This will allow us to determine the battery level from our code later on.

Finally, we want to connect the CHARGING output from the Nomad to a digital input (DIO) on the ioNode. This will allow us to know when the battery is charging.

For example, we can use the following:

Nomad pin ioNode pin
VIN VUSB
VOUT (5.0V) VIN
BAT LEVEL P0
CHARGING P29

The diagram below shows an example wiring:

Mp3 player diagram 0 pwr.png

SPI bus

We will need to connect a few things to the SPI bus for this build (at least the Aecho MP3 player module).

Continuing from the previous diagram, let's look at the SPI bus exposed for clarity:

Mp3 player diagram 1 spi.png

Aecho

The Aecho MP3 player module is a key component of our build - it will decode whatever MP3 data we push to it via the SPI bus.

The module actually provides two SPI interfaces, and therefore features two "chip select" inputs: CS for general control and DCS for MP3 data.

We need to connect both of these to digital outputs on the ioNode.

The Aecho also has a DREQ output (used to tell us when we can push more MP3 data) and a RESET input. Let's also connect both of these to some DIO pins on our ioNode.

For example, let's connect the following:

Aecho pin ioNode pin
DCS P10
CS P9
DREQ P8
RESET P7
SCK SCK
MISO MISO
MOSI MOSI

We also need to power the Aecho - let's connect its VIN pin to the VOUT (5.0V) output of the Nomad.

Finally, we need to have a common ground between all of our components, so let's connect the Aecho's GND pin to the general GND "hub".

The following diagram shows how to wire this:

Mp3 player diagram 2 aecho.png

Display

We want our MP3 player to have some form of display to interact with it easily.

Many options are available for this. In this build we will focus on using a typical 128x160 color TFT LCD.

These displays are visually very nice and provide a decent compromise between pixel count, price and size.

We need to power this display from the VOUT (5.0V) output of the Nomad and connect its GND to the common ground.

Backlight

TFT LCDs usually have some backlight that we can control via PWM (Pulse Width Modulation).

We need to connect the 'BACKLIGHT +' input of the display to a PWM output from the ioNode, and 'BACKLIGHT -' to the common ground.

LCD Control

These TFT LCD's are controlled via SPI (write only), so we need to connect the SCK and MOSI pins of the display to the SPI bus. We also need to connect the display's CS (chip select) pin to a digital output on the ioNode.

Displays such as this one generally use 2 extra digital pins for control: 'RESET' and 'D/C'. We will need to connect these to some of the ioNode's digital outputs.

Integrated SD card

Many displays like this one include a (Micro)SD card socket. This will serve as main storage for our MP3 player.

SD Card uses only the SPI bus and a 'CS' (chip select) line which we need to connect to another digital output on our ioNode.

Note: if the display doesn't include a (Micro)SD card socket (like many I2C OLED displays), we need to include a separate storage. A simple option is to use a MicroSD socket. The Dooba shop has some options: MicroSD socket - SIP and MicroSD socket - DIP.

Example wiring

Putting all of the above together, we can suggest the following wiring for the display:

Display pin ioNode pin
RESET P3
A0 or D/C P4
LCD MOSI (often labelled as SDA) LCD MOSI
LCD SCK (often labelled as SCL) LCD SCK
LCD CS (often labelled as just CS) P5
LCD BACKLIGHT + (often labelled as LED+) P11
LCD BACKLIGHT - (often labelled as LED-) GND
SD SCK (often labelled as just SCK) SCK
SD MISO (often labelled as just MISO) MISO
SD MOSI (often labelled as just MOSI) MOSI
SD CS P6

Adding this to the previous diagrams, we now get a wiring like below:

Mp3 player diagram 3 lcd.png

Inpad

In this example build we will use an Inpad module for user input. The Inpad features 8 inputs (5-way switch / mini-joystick and 3 buttons) exposed through an MCP23008 I2C I/O expander.

To use it, we need only to connect the SCL and SDA lines to the matching pins on the ioNode. One thing we need to watch out for is that as soon as we start using the I2C bus, we need to add pull-up resistors to SCL & SDA.

This boils down to adding a resistor between each line (SCL & SDA) and 3.3V. The resistors should usually be 4.7k or 10k.

Like everything else, we also need to power the module. For this we will use the VOUT (3.3V) output from the Nomad, and connect the GND of the Inpad to the common ground.

If we add this to what we've seen so far, we get something like this:

Mp3 player diagram 4 inpad.png

Complete wiring

Putting everything together from what we saw up to now, we can finally look at the complete picture.

Here is the complete wiring for this build (except pull-up resistors and grounds):

Component pin ioNode pin
Nomad BAT LEVEL P0
Nomad CHARGING P29
Aecho DCS P10
Aecho CS P9
Aecho DREQ P8
Aecho RESET P7
Aecho SCK SCK
Aecho MISO MISO
Aecho MOSI MOSI
Display RESET P3
Display A0 or D/C P4
Display MOSI (often labelled as SDA) MOSI
Display SCK (often labelled as SCL) SCK
Display CS P5
Display BACKLIGHT + (often labelled as LED+) P11
Display BACKLIGHT - (often labelled as LED-) GND
SD SCK (often labelled as just SCK) SCK
SD MISO (often labelled as just MISO) MISO
SD MOSI (often labelled as just MOSI) MOSI
SD CS P6

The power supply distribution from the Nomad module is presented below:

Component pin Nomad pin
ioNode VUSB Nomad VIN
ioNode VIN Nomad VOUT (5.0V)
Aecho VIN Nomad VOUT (5.0V)
Inpad VIN Nomad VOUT (3.3V)
Display VIN Nomad VOUT (5.0V)

The final wiring diagram for the complete build with everything included looks like this:

Mp3 player diagram complete.png

Actually building it

One technique to simplify assembly is soldering the ioNode first.

Since it is quite small but usually has a lot of connections, it will be a decisive part of the build process.

Soldering the ioNode first

Maracas build 0.jpg

Adding the Aecho right from the start

For this build, we will also add the second "core" component before we start wiring: the Aecho.

From there, we can already start preparing the wiring.

Maracas build 1.jpg Maracas build 2.jpg

Wiring everything

The idea is that before we start covering up the board with components, we want to lay down some wiring for those components.

Doing this now will be much easier than adding the wiring later on.

By planning where each component will go, we can wire up the entire board, preparing the connections for every component that we will add:

Maracas build 3.jpg Maracas build 4.jpg

Adding the rest of the components

Once we have wired everything, we can add the components:

Maracas build 7.jpg Maracas build 5.jpg

Finishing the build

Our build is almost complete.

Now we must bridge some solder points on the back of the board where the internal connections provided by the board were not enough.

We use the "central bar" to distribute the common ground through the whole board.

Maracas build 8.jpg

Done!

And we are done!

Our build is complete, and we now have our fully-assembled portable MP3 player:

Maracas build 6.jpg

Now we can move on to Software (programming the MP3 player).

Other layout ideas

Some layout ideas are presented below. Please note that these are merely suggestions - also they don't show the I2C pull-up resistors, as these are generally pretty easy to fit into any design. Don't forget to add them!

Large ioProto

As mentioned above, going for the larger ioProto will make it much easier to fit everything - the following pictures show a simple layout with everything on one side, no stacking, and a bit of extra room:

Mp3 proto large 0 sm.png Mp3 proto large 1 sm.png

Medium ioProto

With a little bit of stacking, we can fit everything on a medium-size ioProto:

Mp3 proto med 0.png Mp3 proto med 1.png

Below is another example with a medium-sized ioProto, but this time using a I2C OLED display.

Note the addition of a MicroSD socket (stacked under the Aecho), since the I2C OLED display does not include an SD card socket (unlike the SPI color-TFT display used previously).

Mp3 proto med oled 0.png Mp3 proto med oled 1.png

For the more adventurous, here is one last example layout which uses both sides of the board to reduce stacking.

WARNING - Keep in mind that this type of design is not recommended for beginners as it greatly increases the risk of failing the build.

Mp3 proto med dual 0.png Mp3 proto med dual 1.png

End result

Below are some pictures from an example build, complete with 3D-printed case:

Maracas with case.jpg

Maracas with case in hand.jpg

Software (programming the MP3 player)

In this section we will look at how to write the software for our MP3 player.

Please note that for simplicity we will stick to the wiring shown in the previous section. If you wired your MP3 player differently you will obviously need to adapt some details. Specifically, your substrate will need to be adapted to reflect the wiring you used for your own build.

If you have never used the Dooba SDK, we recommend you take a moment to read our Discover Dooba tutorial, especially the Software section.

We will assume you have the SDK installed, otherwise have a look at Installing the SDK.

General idea

We will aim to design a simple software for our MP3 player - one that displays a file browser, allowing us to play any MP3 file from the (Micro)SD card.

When the player reaches the end of a file, it should start playing the next file in the same directory, until the end of the directory is reached.

When the end of the directory is reached, the player should stop.

We will use two of the three Inpad buttons to control the volume. The 5-way mini-joystick will be used to browse files.

A status bar will display volume changes as well as the battery level / charging status.

Finally, we also want the screen/user interface to auto-lock after a few seconds of inactivity to save on battery.

Basics

Let's start with the basics: a folder for our application, containing a dfe.conf file and a substrate file.

We can do all of this through dscaff, or create them manually. In any case, we will have to manually edit our substrate to reflect our hardware.

Maracas scaff.gif

We will need at least the following libraries:

  • eloop
  • ionode
  • aecho
  • nomad
  • sdcard
  • vfat
  • vfs

Depending on the specific build we are targeting, we will also need additional libraries for the display and other features. For the build presented in this tutorial, we use a 128x160 TFT LCD and an Inpad module, and we will make use of the Yolk UI framework to build a user interface. We also want to have the scli (Serial Command Line Interface) for testing / debugging. This means we will also want to add the following libraries:

  • yolk
  • gfx
  • st7735r
  • scli

However we do it, we should end up with something like this:

Maracas scaff struct.png

We simply called our MP3 player "maracas" after the kit, but you can choose whatever name you want.

Substrate

Before we begin writing code for our application, we should describe our hardware setup in the substrate.

Below is a complete substrate example for this build, matching the wiring presented in the previous sections.

This may look intimidating at first sight, but is actually pretty simple and should be fairly self-explanatory - we simply declare everything we are using and indicate the I/O pins used for each component:

# Substrate for the Maracas MP3 player

# ioNode
uses :ionode

# SCLI
uses :scli, prompt: '#maracas > ', hello: ' * Maracas\n * Portable MP3 player\n\nType \"help\" to see available commands.'

# Nomad (battery management)
uses :nomad, name: 'bat0', bat_level_pin: 0, charging_pin: 29

# Display
uses :st7735r_gfx, name: 'dsp0', rst_pin: 3, dc_pin: 4, cs_pin: 5, profile: :gen128x160

# SD Card (in most cases we will only use one partition, so let's not waste memory with multi-partition support)
uses :sdcard_storage, name: 'sdc0', cs_pin: 6, partitions: 1

# Aecho (mp3 decoder)
uses :aecho, { name: 'mpd0', rst_pin: 7, dreq_pin: 8, cs_pin: 9, dcs_pin: 10 }

# Automount Everything
uses :vfs_automount

# Yolk
uses :yolk, name: 'ui0',

	# Display
	dsp: 'dsp0',

	# Style (Use 'tftbasic' style included with Yolk)
	style: :tftbasic,

	# Backlight (TFT LCD Backlight on PWM pin P11)
	bklt: { mode: :pwm, pin: 11 },

	# Input (Inpad module)
	input: {
		mask: 0xff,
		bit_nums: { up: 2, down: 3, left: 4, right: 0, center: 1 },
		driver: { type: :ioexp, addrs: [0x48] }
	}

Code structure

We will use eloop for our application, meaning that we can already start creating a C source file to hold the basic skeleton. We'll call it "maracas.c":

// Application Skeleton for Maracas MP3 player

// Internal Includes
#include "substrate.h"

// Main Initialization
void init()
{
	// Init Substrate
	substrate_init();
}

// Main Loop
void loop()
{
	// Update Substrate
	substrate_loop();
}

This is a very "standard" basic skeleton for any application. This makes use of the substrate to both initialize and update the various hardware components and drivers.

Status bar & Auto-off

The first thing we may want to do is attach some plugins to our user interface. Specifically, we mentioned earlier that we want a status bar to display volume and battery level / status. We also wanted to have the display and user interface automatically turn off after some inactivity period.

Status bar

In our substrate we declared our Nomad battery management module as 'bat0'. We also know that the Nomad library provides methods to get the battery level / charging status. We can therefore connect these methods to the status bar plugin to have it display this information.

Auto-off

The auto-off plugin included with Yolk takes care of turning off the user interface after a few seconds of inactivity. Pressing a key will wake it up.

Let's modify our code

We can extend our maracas.c source file to do just that once the substrate is initialized:

// Application Skeleton for Maracas MP3 player

// External Includes
#include <yolk/util/sbar.h>
#include <yolk/util/auto_off.h>

// Internal Includes
#include "substrate.h"

// Yolk Utilities
struct yolk_sbar ysb;
struct yolk_auto_off yao;

// Main Initialization
void init()
{
	// Init Substrate
	substrate_init();

	// Attach Status Bar & Hook Power State to Nomad
	yolk_sbar_attach(&ysb, ui0, YOLK_SBAR_POS_TOP);
	yolk_sbar_set_pwr_callbacks(&ysb, (yolk_sbar_get_bat_pct_t)nomad_get_bat_pct, (yolk_sbar_is_charging_t)nomad_is_charging, bat0);	

	// Attach Auto-Off
	yolk_auto_off_attach(&yao, ui0);
}

// ...

Detecting (Micro)SD Card

It would be nice if our MP3 player could display a warning message when the (Micro)SD card is not present / not detected. This would avoid the awkward situation of just displaying a sad empty browser.

We can accomplish this very easily by looking at the available VFS mountpoints.

If some SD card is present and mounted, we can go further and display the file browser. If no mountpoints are available, we can display a message using a standard Yolk Dialog.

Let's add this to maracas.c, after we've configured our plugins:

// Application Skeleton for Maracas MP3 player

// External Includes
#include <yolk/util/sbar.h>
#include <yolk/util/auto_off.h>

// Internal Includes
#include "substrate.h"

// Yolk Utilities
struct yolk_sbar ysb;
struct yolk_auto_off yao;

// Main Initialization
void init()
{
	// Init Substrate
	substrate_init();

	// Attach Status Bar & Hook Power State to Nomad
	yolk_sbar_attach(&ysb, ui0, YOLK_SBAR_POS_TOP);
	yolk_sbar_set_pwr_callbacks(&ysb, (yolk_sbar_get_bat_pct_t)nomad_get_bat_pct, (yolk_sbar_is_charging_t)nomad_is_charging, bat0);	

	// Attach Auto-Off
	yolk_auto_off_attach(&yao, ui0);

	// Check SD card present
	if(vfs_mountpoints == 0)
	{
		// Display Error Message and Die
		yolk_dialog_enter(ui0, 0, 0, YOLK_ICON_ERROR, "Insert media", "No SD card found!");
		return;
	}
}

// ...

At this point we can already try to build & flash our firmware into our MP3 player - if no SD card is inserted, we will get a nice error message:

File browser

Let's get to the more interesting stuff now. If a SD card is present, we want to display a file browser and be able to navigate through the file system.

The Yolk user interface framework includes a generic file browser utility.

We can use this for our MP3 player. Let's include the basics for it in our maracas.c source file:

// Application Skeleton for Maracas MP3 player

// External Includes
#include <yolk/util/sbar.h>
#include <yolk/util/auto_off.h>
#include <yolk/util/file_browser.h>

// Internal Includes
#include "substrate.h"

// Yolk Utilities
struct yolk_sbar ysb;
struct yolk_auto_off yao;

// File Browser object
struct yolk_file_browser yfb;

// File Browser Event Handler - This will be called by the file browser when an event occurs
void fb_event_handler(struct yolk_file_browser *b, struct yolk *yk, void *user, uint8_t event)
{
	// ToDo: Respond to events here!
}

// Main Initialization
void init()
{
	// Init Substrate
	substrate_init();

	// Attach Status Bar & Hook Power State to Nomad
	yolk_sbar_attach(&ysb, ui0, YOLK_SBAR_POS_TOP);
	yolk_sbar_set_pwr_callbacks(&ysb, (yolk_sbar_get_bat_pct_t)nomad_get_bat_pct, (yolk_sbar_is_charging_t)nomad_is_charging, bat0);	

	// Attach Auto-Off
	yolk_auto_off_attach(&yao, ui0);

	// Check SD card present
	if(vfs_mountpoints == 0)
	{
		// Display Error Message and Die
		yolk_dialog_enter(ui0, 0, 0, YOLK_ICON_ERROR, "Insert media", "No SD card found!");
		return;
	}

	// Enter File Browser (only looking for mp3 files)
	if(yolk_file_browser_enter(&yfb, ui0, "", ".mp3", fb_event_handler, 0, YOLK_ICON_FOLDER, "Maracas"))
	{
		// Error
		yolk_dialog_enter(ui0, 0, 0, YOLK_ICON_ERROR, "Error :(", "Failed to open file browser!");
	}
}

// ...

Handling basic browsing

Whenever an event occurs (user selects something or presses 'back'), the file browser will execute the callback method we passed to it: void fb_event_handler(struct yolk_file_browser *b, struct yolk *yk, void *user, uint8_t event).

For now, this method doesn't do much. Let's cover the basics and at least have it browse forward and back through the filesystem:

// File Browser Event Handler - This will be called by the file browser when an event occurs
void fb_event_handler(struct yolk_file_browser *b, struct yolk *yk, void *user, uint8_t event)
{
	// Check Event - What exactly happened?
	if((event == YOLK_MENU_EVENT_SELECT) || (event == YOLK_MENU_EVENT_RIGHT))
	{
		// "Select" event - Check whether object is a file or directory
		if(b->selected_otype == VFS_OTYPE_FILE)			{ /* A file was selected! Do something with 'b->selected_path' / 'b->selected_path_len' */ }
		else
		{
			// A directory was selected - try to browse into it
			if(yolk_file_browser_browse_selected(b))	{ yolk_dialog_enter(yk, 0, 0, YOLK_ICON_ERROR, "Error :(", "Failed to browse directory!"); }
		}
	}
	else if(event == YOLK_MENU_EVENT_BACK)
	{
		// "Back" event - Try to browse back
		if(yolk_file_browser_browse_back(b))			{ yolk_dialog_enter(yk, 0, 0, YOLK_ICON_ERROR, "Error :(", "Failed to browse back!"); }
	}
	else												{ /* NoOp */ }
}

We can see in the code above that when an element is selected in the file browser, we first check to see if it was a directory or a file.

If a directory is selected we want to browse it. If a file is selected we do nothing for now, but this is where we will add some code later on to try to play the file.

When a 'back' event is triggered, we want the browser to go back up one level.

Let's try to build and flash this - now if we stick a SD card into our MP3 player, we get much further:

Error Handling

We are already starting to see a pattern of errors related to browsing, which should all be handled the same way.

Let's take a minute to refactor our code and make it a little easier to read and re-use by defining a method to handle this type of generic error:

// Re-Browse - Called by the Error Dialog
void rebrowse(struct yolk *yk, struct yolk_file_browser *b)
{
	// Re-Browse
	yolk_file_browser_browse_path(b);
}

// Display Error and Re-Browse
void error_and_rebrowse(struct yolk *yk, struct yolk_file_browser *b, char *msg)
{
	// Error & Re-Browse
	yolk_dialog_enter_std_btn(yk, 0, YOLK_DIALOG_STD_BTN_OK, (yolk_dialog_cb_t)rebrowse, (yolk_dialog_cb_t)rebrowse, b, YOLK_ICON_ERROR, "Error :(", "%s", msg);
}

This will display an error message, and then re-browse the current directory. Let's improve our previous error handling:

// File Browser Event Handler - This will be called by the file browser when an event occurs
void fb_event_handler(struct yolk_file_browser *b, struct yolk *yk, void *user, uint8_t event)
{
	// Check Event - What exactly happened?
	if((event == YOLK_MENU_EVENT_SELECT) || (event == YOLK_MENU_EVENT_RIGHT))
	{
		// "Select" event - Check whether object is a file or directory
		if(b->selected_otype == VFS_OTYPE_FILE)			{ /* A file was selected! Do something with 'b->selected_path' / 'b->selected_path_len' */ }
		else
		{
			// A directory was selected - try to browse into it
			if(yolk_file_browser_browse_selected(b))	{ error_and_rebrowse(yk, b, "Failed to browse directory!"); }
		}
	}
	else if(event == YOLK_MENU_EVENT_BACK)
	{
		// "Back" event - Try to browse back
		if(yolk_file_browser_browse_back(b))			{ error_and_rebrowse(yk, b, "Failed to browse back!"); }
	}
	else												{ /* NoOp */ }
}

Playing mp3 files

Now that we have a file browser set up, the next step is playing mp3 files when they are selected.

The way to accomplish this is to add some code to play the file in our file browser event handler method - void fb_event_handler(struct yolk_file_browser *b, struct yolk *yk, void *user, uint8_t event).

Specifically, we made this method browse into directories when they are selected and browse back when the 'back' action is triggered. We also left a space to handle the situation when the user selects a file:

// File Browser Event Handler - This will be called by the file browser when an event occurs
void fb_event_handler(struct yolk_file_browser *b, struct yolk *yk, void *user, uint8_t event)
{
	// Check Event - What exactly happened?
	if((event == YOLK_MENU_EVENT_SELECT) || (event == YOLK_MENU_EVENT_RIGHT))
	{
		// "Select" event - Check whether object is a file or directory
		if(b->selected_otype == VFS_OTYPE_FILE)		{ /* A file was selected! Do something with 'b->selected_path' / 'b->selected_path_len' */ }

		// ...
	}

	// ...
}

Let's insert some code there to play the file:

// MP3 Player End-Of-File
void mpd0_eof(void *user)
{
	// ToDo: Write Meth
}

// File Browser Event Handler - This will be called by the file browser when an event occurs
void fb_event_handler(struct yolk_file_browser *b, struct yolk *yk, void *user, uint8_t event)
{
	// Check Event - What exactly happened?
	if((event == YOLK_MENU_EVENT_SELECT) || (event == YOLK_MENU_EVENT_RIGHT))
	{
		// "Select" event - Check whether object is a file or directory
		if(b->selected_otype == VFS_OTYPE_FILE)
		{
			// A file was selected - try to play it
			if(aecho_play_n(mpd0, b->selected_path, b->selected_path_len, mpd0_eof, 0))		{ error_and_rebrowse(yk, b, "Failed to play file!"); }
		}

		// ...
	}

	// ...
}

Note how we created a new 'mpd0_eof' method, and passed it to the 'aecho_play_n' method. This callback method (mpd0_eof) will be executed when the end of the currently playing file is reached.

For now, it doesn't need to do anything. We will come back to it later.

Volume control

Playing files is nice, but this will not get us very far if we can't control the output volume.

We can use the other buttons on the Inpad module to control the volume.

Let's add a bit of code to our main loop to check those buttons and change the volume in increments of 16:

// Define Volume Buttons - Taken from Inpad specification (https://wiki.dooba.io/index.php?title=Inpad#Usage)
#define	MARACAS_BTN_VOLUP	0x40
#define	MARACAS_BTN_VOLDOWN	0x80

// ...

// Main Loop
void loop()
{
	uint8_t show_vol;

	// Update Substrate
	substrate_loop();

	// Handle Volume Buttons
	show_vol = 0;
	if(yolk_in(ui0, MARACAS_BTN_VOLUP))		{ aecho_vol_up_x(mpd0, 16); show_vol = 1; }
	if(yolk_in(ui0, MARACAS_BTN_VOLDOWN))	{ aecho_vol_down_x(mpd0, 16); show_vol = 1; }

	// Display Volume in Status Bar
	if(show_vol)							{ yolk_sbar_set_progress(&ysb, aecho_get_vol_pct(mpd0), YOLK_ICON_MUSIC); }
}

Adding a header file

We are now starting to have a few methods in our maracas.c source file. This is ok but the compiler needs to see the methods defined BEFORE they are called. This can quickly become problematic if two methods call or refer to each other for example.

To simplify everything, we will create a C header (.h) file that will declare everything we need, so it can be accessed regardless of where it was defined.

If you want to read more about this, check out these articles:

So, let's create a new 'maracas.h' file and declare all of our methods in it. We can also take this opportunity to move the macros we defined for the volume buttons there, so they don't clutter up our C source file (maracas.c). Finally, we also need to move our external includes to our new header file so they are accessible right from there.

// Application Header for Maracas MP3 player

#ifndef	__MARACAS_H
#define	__MARACAS_H

// External Includes
#include <yolk/util/sbar.h>
#include <yolk/util/auto_off.h>
#include <yolk/util/file_browser.h>

// Internal Includes
#include "substrate.h"

// Define Volume Buttons - Taken from Inpad specification (https://wiki.dooba.io/index.php?title=Inpad#Usage)
#define	MARACAS_BTN_VOLUP		0x40
#define	MARACAS_BTN_VOLDOWN		0x80

// Re-Browse
extern void rebrowse(struct yolk *yk, struct yolk_file_browser *b);

// Display Error and Re-Browse
extern void error_and_rebrowse(struct yolk *yk, struct yolk_file_browser *b, char *msg);

// MP3 Player End-Of-File
extern void mpd0_eof(void *user);

// File Browser Event Handler
extern void fb_event_handler(struct yolk_file_browser *b, struct yolk *yk, void *user, uint8_t event);

// Main Initialization
extern void init();

// Main Loop
extern void loop();

#endif

Now we can update our maracas.c source file and make it a bit simpler - we can remove the includes at the top and replace them by including 'maracas.h' instead, and we can remove the volume button definitions. These changes may not look like much, but they now allow us to reference our methods in any order, even before they are defined (and we will definitely need this for the next section).

// Application Skeleton for Maracas MP3 player

// Internal Includes
#include "maracas.h"

// ...

Continuous playing

The last thing we need to cover, in order to make our mp3 player more "usable", is to automatically play the next file in the directory when the End-Of-File (EOF) is reached.

As we saw earlier, the 'mpd0_eof' method will be called when the end of the currently playing file is reached.

We can insert some code in that method to start playing the next file in the same directory when we reach the end.

To accomplish this, we first need to keep track of the directory path where the current file is playing, as well as the index of the currently playing file within that directory.

Then, Yolk's file browser utility can be used to compute paths based on item index, while respecting the filter extension.

Putting all of this together, we will create a new method that saves the index and path of the file, and then plays the file. First, we declare our new method in our 'maracas.h' header file:

// Application Header for Maracas MP3 player

#ifndef	__MARACAS_H
#define	__MARACAS_H

// ...

// Play MP3 File (by Directory Path and File Index)
extern void play_file(char *path, uint8_t path_len, uint8_t index);

// MP3 Player End-Of-File
extern void mpd0_eof(void *user);

// ...

#endif

Now that this is done, let's implement this new method in our 'maracas.c' source file:

// ...

// File Browser object
struct yolk_file_browser yfb;

// Currently playing info
char play_path[VFS_PATH_MAXLEN];
uint8_t play_path_len;
uint8_t play_idx;

// Path Buffer (used to compute complete path of currently playing file)
char path_buf[VFS_PATH_MAXLEN];

// ...

// Play MP3 File (by Directory Path and File Index)
void play_file(char *path, uint8_t path_len, uint8_t index)
{
	uint8_t plen;
	uint8_t otype;

	// Compute complete file path
	if(yolk_file_browser_compute_path_n(&yfb, path, path_len, index, path_buf, &plen, &otype))
	{
		// We probably reached the end of the directory at this point - We can just stop playing and return
		aecho_stop(mpd0);
		return;
	}

	// Try to play the file
	if(aecho_play_n(mpd0, path_buf, plen, mpd0_eof, 0))				{ error_and_rebrowse(ui0, &yfb, "Failed to play file!"); return; }

	// Set Play Path & Index
	memcpy(play_path, path, path_len);
	play_path_len = path_len;
	play_idx = index;
}

// MP3 Player End-Of-File
void mpd0_eof(void *user)
{
	// ToDo: Write Meth
}

Now that we have this new simplified 'play_file' method, we can use it to make our mp3 player continuously play the next file in the directory, until the end of the directory.

For this, we must do two things:

  • implement the 'mpd0_eof' method to increment the play index and try to play the next file
  • replace the call to 'aecho_play_n' by our new 'play_file' in our 'fb_event_handler' method
// MP3 Player End-Of-File
void mpd0_eof(void *user)
{
	// Try to play next file in directory
	play_file(play_path, play_path_len, play_idx + 1);
}

// File Browser Event Handler - This will be called by the file browser when an event occurs
void fb_event_handler(struct yolk_file_browser *b, struct yolk *yk, void *user, uint8_t event)
{
	// Check Event - What exactly happened?
	if((event == YOLK_MENU_EVENT_SELECT) || (event == YOLK_MENU_EVENT_RIGHT))
	{
		// "Select" event - Check whether object is a file or directory
		if(b->selected_otype == VFS_OTYPE_FILE)
		{
			// A file was selected - try to play it
			// (yolk_menu_selected_item_value() returns the selected item's value,
			// which in the case of the File Browser is just the index of the file within the directory)
			play_file(b->path, b->path_len, yolk_menu_selected_item_value(yk));
		}

		// ...
	}

	// ...
}

And that's it! We have completed the basic firmware for our portable MP3 player! Congratulate yourself and post some pictures of your creation!

Complete source code

This section will simply list the complete source code for the MP3 player firmware that we created above.

dfe.conf

name: maracas
type: app
mmcu: atmega1284p
freq: 10MHz
deps:
  - eloop
  - scli
  - sdcard
  - st7735r
  - gfx
  - aecho
  - ionode
  - yolk
  - vfat
  - vfs
  - nomad

substrate

# Substrate for the Maracas MP3 player

# ioNode
uses :ionode

# SCLI
uses :scli, prompt: '#maracas > ', hello: ' * Maracas\n * Portable MP3 player\n\nType \"help\" to see available commands.'

# Nomad (battery management)
uses :nomad, name: 'bat0', bat_level_pin: 0, charging_pin: 29

# Display
uses :st7735r_gfx, name: 'dsp0', rst_pin: 3, dc_pin: 4, cs_pin: 5, profile: :gen128x160

# SD Card (in most cases we will only use one partition, so let's not waste memory with multi-partition support)
uses :sdcard_storage, name: 'sdc0', cs_pin: 6, partitions: 1

# Aecho (mp3 decoder)
uses :aecho, { name: 'mpd0', rst_pin: 7, dreq_pin: 8, cs_pin: 9, dcs_pin: 10 }

# Automount Everything
uses :vfs_automount

# Yolk
uses :yolk, name: 'ui0',

	# Display
	dsp: 'dsp0',

	# Style (Use 'tftbasic' style included with Yolk)
	style: :tftbasic,

	# Backlight (TFT LCD Backlight on PWM pin P11)
	bklt: { mode: :pwm, pin: 11 },

	# Input (Inpad module)
	input: {
		mask: 0xff,
		bit_nums: { up: 2, down: 3, left: 4, right: 0, center: 1 },
		driver: { type: :ioexp, addrs: [0x48] }
	}

maracas.h

// Application Header for Maracas MP3 player

#ifndef	__MARACAS_H
#define	__MARACAS_H

// External Includes
#include <yolk/util/sbar.h>
#include <yolk/util/auto_off.h>
#include <yolk/util/file_browser.h>

// Internal Includes
#include "substrate.h"

// Define Volume Buttons - Taken from Inpad specification (https://wiki.dooba.io/index.php?title=Inpad#Usage)
#define	MARACAS_BTN_VOLUP		0x40
#define	MARACAS_BTN_VOLDOWN		0x80

// Re-Browse
extern void rebrowse(struct yolk *yk, struct yolk_file_browser *b);

// Display Error and Re-Browse
extern void error_and_rebrowse(struct yolk *yk, struct yolk_file_browser *b, char *msg);

// Play MP3 File (by Directory Path and File Index)
extern void play_file(char *path, uint8_t path_len, uint8_t index);

// MP3 Player End-Of-File
extern void mpd0_eof(void *user);

// File Browser Event Handler
extern void fb_event_handler(struct yolk_file_browser *b, struct yolk *yk, void *user, uint8_t event);

// Main Initialization
extern void init();

// Main Loop
extern void loop();

#endif

maracas.c

// Application Skeleton for Maracas MP3 player

// Internal Includes
#include "maracas.h"

// Yolk Utilities
struct yolk_sbar ysb;
struct yolk_auto_off yao;

// File Browser object
struct yolk_file_browser yfb;

// Currently playing info
char play_path[VFS_PATH_MAXLEN];
uint8_t play_path_len;
uint8_t play_idx;

// Path Buffer (used to compute complete path of currently playing file)
char path_buf[VFS_PATH_MAXLEN];

// Re-Browse
void rebrowse(struct yolk *yk, struct yolk_file_browser *b)
{
	// Re-Browse
	yolk_file_browser_browse_path(b);
}

// Display Error and Re-Browse
void error_and_rebrowse(struct yolk *yk, struct yolk_file_browser *b, char *msg)
{
	// Error & Re-Browse
	yolk_dialog_enter_std_btn(yk, 0, YOLK_DIALOG_STD_BTN_OK, (yolk_dialog_cb_t)rebrowse, (yolk_dialog_cb_t)rebrowse, b, YOLK_ICON_ERROR, "Error :(", "%s", msg);
}

// Play MP3 File (by Directory Path and File Index)
void play_file(char *path, uint8_t path_len, uint8_t index)
{
	uint8_t plen;
	uint8_t otype;

	// Compute complete file path
	if(yolk_file_browser_compute_path_n(&yfb, path, path_len, index, path_buf, &plen, &otype))
	{
		// We probably reached the end of the directory at this point - We can just stop playing and return
		aecho_stop(mpd0);
		return;
	}

	// Try to play the file
	if(aecho_play_n(mpd0, path_buf, plen, mpd0_eof, 0))				{ error_and_rebrowse(ui0, &yfb, "Failed to play file!"); return; }

	// Set Play Path & Index
	memcpy(play_path, path, path_len);
	play_path_len = path_len;
	play_idx = index;
}

// MP3 Player End-Of-File
void mpd0_eof(void *user)
{
	// Try to play next file in directory
	play_file(play_path, play_path_len, play_idx + 1);
}

// File Browser Event Handler
void fb_event_handler(struct yolk_file_browser *b, struct yolk *yk, void *user, uint8_t event)
{
	// Check Event
	if((event == YOLK_MENU_EVENT_SELECT) || (event == YOLK_MENU_EVENT_RIGHT))
	{
		// "Select" event - Check whether object is a file or directory
		if(b->selected_otype == VFS_OTYPE_FILE)
		{
			// A file was selected - try to play it
			// (yolk_menu_selected_item_value() returns the selected item's value,
			// which in the case of the File Browser is just the index of the file within the directory)
			play_file(b->path, b->path_len, yolk_menu_selected_item_value(yk));
		}
		else
		{
			// A directory was selected - try to browse into it
			if(yolk_file_browser_browse_selected(b))				{ error_and_rebrowse(yk, b, "Failed to browse directory!"); }
		}
	}
	else if(event == YOLK_MENU_EVENT_BACK)
	{
		// "Back" event - Try to browse back
		if(yolk_file_browser_browse_back(b))						{ error_and_rebrowse(yk, b, "Failed to browse back!"); }
	}
	else															{ /* NoOp */ }
}

// Main Initialization
void init()
{
	// Init Substrate
	substrate_init();

	// Attach Status Bar & Hook Power State to Nomad
	yolk_sbar_attach(&ysb, ui0, YOLK_SBAR_POS_TOP);
	yolk_sbar_set_pwr_callbacks(&ysb, (yolk_sbar_get_bat_pct_t)nomad_get_bat_pct, (yolk_sbar_is_charging_t)nomad_is_charging, bat0);

	// Attach Auto-Off
	yolk_auto_off_attach(&yao, ui0);

	// Check SD card present
	if(vfs_mountpoints == 0)
	{
		// Error Message and Die
		yolk_dialog_enter(ui0, 0, 0, YOLK_ICON_ERROR, "Insert media", "No storage media found!");
		return;
	}

	// Enter File Browser (without any extension filter)
	if(yolk_file_browser_enter(&yfb, ui0, "", ".mp3", fb_event_handler, 0, YOLK_ICON_FOLDER, "Maracas"))
	{
		// Error
		yolk_dialog_enter(ui0, 0, 0, YOLK_ICON_ERROR, "Error :(", "Failed to open file browser!");
	}
}

// Main Loop
void loop()
{
	uint8_t show_vol;

	// Update Substrate
	substrate_loop();

	// Handle Volume Buttons
	show_vol = 0;
	if(yolk_in(ui0, MARACAS_BTN_VOLUP))		{ aecho_vol_up_x(mpd0, 16); show_vol = 1; }
	if(yolk_in(ui0, MARACAS_BTN_VOLDOWN))	{ aecho_vol_down_x(mpd0, 16); show_vol = 1; }

	// Display Volume in Status Bar
	if(show_vol)							{ yolk_sbar_set_progress(&ysb, aecho_get_vol_pct(mpd0), YOLK_ICON_MUSIC); }
}