SFEMP3Shield playMp3 - How to convert char array to char* for playback

Brand new to C++ so total noob question here. I've created a char array of tracks I want to play from my sdcard. Then I want to loop through them and play them using SFEMP3Shield playMP3 function. But that function requires a char* pointer array. I can't seem to figure out how to convert my normal array into that pointer array.

Here's my code, 'tracks' is a standard char array:

EDIT: See comment further down for my full sketch (which isn't included fully here)

void loop()
{
//tracks is a char array
  for(auto track : tracks){
    if(track != NULL){
      uint8_t result = MP3player.playMP3(track);  //this needs to be a pointer array
      delay(10000);
    }
  }
}

I tried converting tracks to a pointer array before passing it into the foreach loop like this:

void loop()
{
  char* trackList = tracks;
  for(auto track : trackList){
    if(track != NULL){
      uint8_t result = MP3player.playMP3(track); 
      delay(10000);
    }
  }
}

But when I do that the loop gives me an error like this (so not sure if I can pass a pointer into a for each loop?). I don't know what 'begin' is, since it's not part of this loop.

/Arduinostuff/arduino_sample/arduino_sample.ino: In function 'void loop()':
/Arduinostuff/arduino_sample/arduino_sample.ino:38:20: error: 'begin' was not declared in this scope
   for(auto track : trackList){
                    ^~~~~~~~~
/Arduinostuff/arduino_sample/arduino_sample.ino:38:20: note: suggested alternative: 'rewind'
   for(auto track : trackList){
                    ^~~~~~~~~
                    rewind
/Arduinostuff/arduino_sample/arduino_sample.ino:38:20: error: 'end' was not declared in this scope
exit status 1
Compilation error: 'begin' was not declared in this scope

Any direction on what I'm doing wrong? Thanks!

Show us its definition
The array name could be what you are looking for but may be you neee a 2D array

char trackList[maxTracks][maxTrackLenth+1];

Thanks for the reply, here's the function that builds the array. I'm sure its clumsy, but I'm just figuring out how to do this: :slight_smile:

void createTrackArray(char* directory = "/"){
  // Open directory
  if (!dir.open(directory)) {
  }

  char name[40];
  int i = 0;
  while (file.openNext(&dir, O_RDONLY)) {
    if (!file.isDir() && !file.isHidden()) {
      file.getName(name, 40);
      byte len = strlen (name);
      if (len > 4 && strcmp (&name [len - 4], ".mp3") == 0){;
        tracks[i] = name;
        i++;
      }
    
    }
    //always end char array with \0 (?)
    tracks[i] = '\0';
    file.close();
  }

EDIT: And tracks is defined globally so I can use it in the loop. it's up top in the file like this:

char tracks[50];

For the sake of clarity, here's my entire sketch as of now. I didn't want to include it all initially to avoid confusion on what I thought was the problem..but maybe this is better to include it all:

#include <SPI.h>           // SPI library
#include <SdFat.h>         // SDFat Library
#include <SFEMP3Shield.h>  // Mp3 Shield Library

//create objects
SdFat sd;
File dir;
File file;
SFEMP3Shield MP3player; // Create Mp3 library object

//define some vars
const uint8_t volume = 30; // MP3 Player volume 0=max, 255=lowest (off)
const uint16_t monoMode = 1;  // Mono setting 0=off, 3=max
int delayTime;
int dlyMin = 1000;
int dlyMax = 20000;

//how many tracks should we load
char tracks[50];
void createTrackArray(char* = "/");

void setup()
{
  Serial.begin(9600);
  initSD();  // Initialize the SD card
  initMP3Player(); // Initialize the MP3 Shield
  //load our files
  createTrackArray();
}

// Loop
void loop()
{
  char* trackList = tracks;
  for(auto track : trackList){
    if(track != NULL){
      delayTime = random(dlyMin, dlyMax);
      uint8_t result = MP3player.playMP3(track); 
      delay(delayTime);
    }
  }
}

// initSD() initializes the SD card and checks for an error.
void initSD()
{
  //Initialize the SdCard.
  if(!sd.begin(SD_SEL, SPI_HALF_SPEED)) 
    sd.initErrorHalt();
  if(!sd.chdir("/")) 
    sd.errorHalt("sd.chdir");
}

// initMP3Player() sets up all of the initialization for the
// MP3 Player Shield. It runs the begin() function, checks
// for errors, applies a patch if found, and sets the volume/
// stero mode.
void initMP3Player()
{
  uint8_t result = MP3player.begin(); // init the mp3 player shield
  if(result != 0) // check result, see readme for error codes.
  {
    // Error checking can go here!
  }
  MP3player.setVolume(volume, volume);
  MP3player.setMonoMode(monoMode);
}

void createTrackArray(char* directory = "/"){
  // Open directory
  if (!dir.open(directory)) {
  }

  char name[40];
  int i = 0;
  while (file.openNext(&dir, O_RDONLY)) {
    if (!file.isDir() && !file.isHidden()) {
      file.getName(name, 40);
      byte len = strlen (name);
      if (len > 4 && strcmp (&name [len - 4], ".mp3") == 0){;
        tracks[i] = name;
        i++;
      }
    
    }
    //always end char array with \0 (?)
    tracks[i] = '\0';
    file.close();
  }
//debug, see some of our track array that was created
// Serial.print(tracks[0]);
// Serial.println();
// Serial.print(tracks[1]);
// Serial.println();
// Serial.print(tracks[2]);
// Serial.println();
}

That’s your issue
You can store 49 characters and the trailing null char to form a cString in this
It’s not 50 entries for different cString

You need the 2 D array if you want to build a list of cStrings

An empty string would be one with a null char as the first character

Thanks, I appreciate the help. I'm not even sure what to Google to understand this, but I'll try to learn more about what you say here and see if I can figure it out.

So to create a 2d array of this, I initialize the array like this:

//up to 40 tracks, each no more than 14 characters in length
char tracks[40][14];

But then, when trying to assign track names to the array, do I assign them like: tracks[i][14] or like tracks[i] or like tracks[40][i] or...something else?

That's the part I can't seem to find an answer to....

Then, after that, I still need to convert it to a pointer...so is that like:

char* trackList = tracks;

or

char* trackList = tracks[40];

Or something else? Man..C++ is confusing. :slight_smile:

The on line C/C++ reference pages are an excellent place to start (and end).

https://cplusplus.com/reference/cstring/

There are lots of tutorials, too: for example

I still need to convert it to a pointer

Hint: the name of a C-string (a zero terminated character array) IS a pointer.

char hello[] = "Hello";
Serial.print(hello);
1 Like

You want a pointer to the start of a line so in first approach the pointer would be the address in memory for that byte

&(tracks[trackNb][0])

Where trackNb is the track number you want to set

1 Like

C-string! OK, that gets me somewhere, thank you! I have a library book and I just got to the section on these, so I'm hoping it helps push me in the right direction. Thank you!

So when you create a pointer, do you set it equal to this first character? You don't set it equal to the whole array?

I thought you could do:

char* pointerArray = characterArray;

but are you saying I should instead do:

char* pointerArray = characterArray[0][0];

?

start simple:

  • an array is just a set of contiguous bytes reserved somewhere in memory. The number of bytes reserved is the array size times the size necessary to represent one element of the array.
  • a char takes only one byte.
  • So if you define an array of 16 chars with char anArray[16]; then you are asking the compiler to set aside 16 x 1 = 16 bytes
  • those 16 bytes will be somewhere in SRAM, in the memory. Let's say the compiler decides to set that at address starting at byte N° 438, then it could look like this

  • assume you now fill up the array with "Hello World" with the terminating null char for a proper c-string then the memory looks like this

  • if you want to find what char is stored somewhere in the array, you just use the array notation with the square brackets. The 'H' of "Hello World" is at index 0 so you would find it with anArray[0]. The type of this is char as you are referring to a char type in that memory cell.

  • now if you want to find a pointer to some specific part of that array you use the address of operator which in compiler language is &. The pointer is really just the address where you byte is allocated in memory. So for the 'H' character, if you want to know its address (which would happen to also be the start address of the array) you just write &(anArray[0]). ➜ address of the char at index 0. Here it would be the SRAM memory cell at position 438. The type of this is char * as you are referring to a pointer to a char type in that memory cell.

Now this was done so often in C that the language designers decided that the array's variable name was also usable as the address of the start or the array

so from a programming point of view you can use either &(anArray[0]) or just anArray it's the same thing


A 2D array works in the same way.

say you want to store 3 c-string of max 15 characters + the null termination. You would define a variable named anArray2D this way:

char anArray2D[3][16];

this secures 3 x 16 x 1 = 48 bytes somewhere in memory, say starting at address 358

it's just a bunch of bytes one after another but from a programming perspective you really have a 2 index entry to the bytes there

and from a pointer or address perspective, it works the same way
&(anArray[0][0]) would be the pointer (the address in memory) of the first cell in memory (address 358) and &(anArray[1][0]) would be the address in memory for the start of the second c-string (address 374).

but of course mentally you can see this as a 2D array arranged this way


where the first index represents the line number and the second index the column number


And again, because this was done so often, it was decided to simplify this and the name of the array would be again the address of the first byte allocated in memory and if you want to find the address of the start of the individual "line" then you can use just the first part of the notation, for example anArray2D[1] is really the same thing as the address of anArray2D[1][0]

if you run this small program on a UNO

char anArray[16];
char anArray2D[3][16];

void setup() {
  Serial.begin(115200);
  uint16_t anArrayAddress1 = (uint16_t) anArray;
  uint16_t anArrayAddress2 = (uint16_t) & (anArray[0]);

  Serial.print("anArrayAddress1 = "); Serial.println(anArrayAddress1);
  Serial.print("anArrayAddress2 = "); Serial.println(anArrayAddress2);


  uint16_t anArray2DAddress0 = (uint16_t) anArray2D;
  uint16_t anArray2DAddress1 = (uint16_t) anArray2D[0];
  uint16_t anArray2DAddress2 = (uint16_t) & (anArray2D[0][0]);

  Serial.print("anArray2DAddress0 = "); Serial.println(anArray2DAddress0);
  Serial.print("anArray2DAddress1 = "); Serial.println(anArray2DAddress1);
  Serial.print("anArray2DAddress2 = "); Serial.println(anArray2DAddress2);

}

void loop() {}

you'll see that the various representations are really for the same memory cell

hope this helps

1 Like

so long story short, I'm saying either do

char* pointerFirstcStringArray  = characterArray[0];

or

char* pointerFirstcStringArray = &(characterArray[0][0]);

--

When you are confused ask yourself about the type of what you write. if you have

characterArray[0][0];

it refers to a specific cell in the array, so the type is char. It can't be a pointer, it's the actual character stored at that location in memory

An alternative approach is to read the filenames into a block of memory and construct an array of pointers to the start of each string. The disadvantage is the extra space needed for the pointers but the space saved on storing the names is likely to be a lot larger. It means that a name longer that 50 characters will be OK provided that there are enough names shorter than 50 characters to provide the space.

    // define the constants at the start so that if changes need to be made
    // there is only one place that changes are needed.
    const size_t max_tracks = 50;
    const size_t average_name_size = 50;

    // let the compiler do the arithmetic, it is better at it!
    const size_t size = max_tracks * average_name_size;

    // Block of memory for the names
    char names[size];
    char* limit = &names[size]; // pointer to first byte after allocated block

    // Array of pointers to char*, ie they will point to the first char of each name.
    char* tracks[max_tracks];

    int n_tracks = 0;;
    tracks[0] = names; // where the first string will be put

    // Read in the names and call insert_name() for each one

bool insert_name(char* name) {
    char* destination = names[track_index];
    size_t length = strlen(name) + 1; // extra byte for the terminating nul
  
    // check that enough space remains
    // limit points to first byte after the reserved memory so use >=
    if (&destination[length] >= limit) return false;
    if (n_tracks >= max_tracks) return false;
  
    strcpy(destination, name); // safe because we have checked that there is space 
    track_index++;
    tracks[track_index] = destination + length; // equivalent to &destination[length]
  
    return true;
}

Then to iterate over the names you just do

    for (size_t i = 0; i < n_tracks; i++) {
        printf("%s\n", tracks[i]);
    }

NB the above code is completely untested!

If very short of memory then the tracks array can be dispensed with but the code will run slower, perhaps a lot slower. If you want to find the nth track name you search through the names array until you find the nth nul byte. The next byte is the start of the nth name.

Some operating systems provide a way to read the filenames into a block of memory with a single function call. You then have to search through the memory looking for the separators in order to find the individual names.

Common pitfalls with this sort of stuff are the numerous opportunities to make out by one errors. Examples include:

  1. Forgetting to allow for the terminating nul. strlen("abc") is 3 but it needs 4 bytes to store it.
  2. Arrays index from 0 so char array[2] reserves space for two values array[0] and array[1], but array[2] is beyond the end of the array and could contain anything. Writing to array[2] may corrupt data belonging to something else and result in a bug that could be very difficult to find because it probably won't manifest until long after the faulty code was executed and it can be difficult to reproduce. There are tools that can help but the easiest way to fix memory corruption bugs is to not create them!

Getting your head around arrays, pointers and pointer arithmetic is one of the most difficult parts of learning C. It is also one of the most essential and pointers are extremely powerful. Unfortunately they also provide lots of opportunity for mistakes. C++ is better in that respect because it provides libraries that hide a lot of it.

You're telling me :slight_smile: I come from PHP and it seems needlessly difficult to do anything in comparison. But, I know there are advantages that hassle brings, too.

This is a great post and I'm going to pour over it slowly and see if I can understand how it all works. Thanks for taking the time!

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.