DFPlayer mini + how to detect when audio clip is finished?

I have a DFPlayer mini board, to give some audio playback for some projects.
https://www.dfrobot.com/wiki/index.php/DFPlayer_Mini_SKU:DFR0299

I am also using the DFRobot library… (unless someone has a better/newer library they can recommend?)

Triggering an audio clip playback is fairly straight forward… what I can not grasp/get working so far is how to automatically detect when an audio clip has finished playing, as to trigger an another clip to play programmatically.

I tried to do some searches, and while topics were returned, nothing yielded any info on detecting when a file was complete/had finished playing.

Here is a quick sketch I threw together to illustrate where I want to listen/check for the current audio file (2 or 002.wav) and trigger audio file 3 or 003.wav to LOOP…

Summary:
power on (boot sound plays)
listen for button press.
– if pressed (button state change)
– then play the power on sound (002.wav)

if button still pressed, wait for power on sound to finish and automatically loop audio clip 003.wav (and continue to loop) while button is being pressed…

when button is release (button state change)…
– play power down sound (004.wav)

I either can NOT get the 003.wav to loop immediately after the 002.wav completes…

-OR-

The looping of 003.wav starts too fast and cuts off the 002.wav before it finishes.

//import needed libraries
#include "Arduino.h"
#include "SoftwareSerial.h"
#include "DFRobotDFPlayerMini.h"


//project vars
const int buttonPin = 19; //Pin (A5) the pin that the pushbutton is attached to
int buttonState = 0; // current state of the button
int lastButtonState = 1; // previous state of the button
boolean doCompletionCheck = false;

//FSM state control/vars
#define S_IDLE 1
#define S_POWERON 2
#define S_POWERDOWN 3

//FSM init vars
static int state = S_IDLE; // initial state is 1, the "idle" state.

//instantiate class instances
SoftwareSerial mySoftwareSerial(10, 11); // RX, TX (DFPlayer connections)
DFRobotDFPlayerMini myDFPlayer;

void setup() {

  //debug (monitor)
  Serial.begin(115200);

  //talk to DFPlayer
  mySoftwareSerial.begin(9600);

  //declare pin & state
  pinMode(buttonPin, INPUT); // initialize the button pin as a input
  digitalWrite(buttonPin, HIGH); //use interal pull up resistors

  //check on DFPlayer state (ready for use?)
  Serial.println(F("Initializing DFPlayer ... (May take 3~5 seconds)"));
  if (!myDFPlayer.begin(mySoftwareSerial)) {  //Use softwareSerial to communicate with mp3.
    Serial.println(F("Unable to begin:"));
    Serial.println(F("1.Please recheck the connection!"));
    Serial.println(F("2.Please insert the SD card!"));
    while (true); //keep checking,...repeat
  }
  Serial.println(F("DFPlayer Mini online."));

  //why?
  myDFPlayer.setTimeOut(500); //Set serial communictaion time out 500ms

  //----Set volume----
  myDFPlayer.volume(9);  //Set volume value (0~30).
  Serial.print(F("CURRENT VOLUME: ")); //read current volume
  Serial.println(myDFPlayer.readVolume()); //read current volume

  //----Set different EQ----//
  myDFPlayer.EQ(DFPLAYER_EQ_ROCK);

  //----Set source device, we use SD as default----//
  myDFPlayer.outputDevice(DFPLAYER_DEVICE_SD);

  //play boot sound on power-up
  myDFPlayer.play(1);

  Serial.print(F("INTIT STATE: "));
  Serial.println(state);

  //delay to let boot sound play and start 'listening' for interaction
  delay(3000);
}

void loop() {

  //FSM state listener
  switch (state) {
    case S_IDLE:

      //check main button state
      buttonState = digitalRead(buttonPin);

      //** button state HAS changed (compare) **//
      if (buttonState != lastButtonState) {
        
        //-- button state changed and is: pressed (ie: down) --//
        if (buttonState == LOW) {
          
         //any time button is pressed down, trigger power on sound
          state = S_POWERON;

          
        //-- button state changed and is: not pressed (ie: up) --//
        }else {       
              
          //any time button is released, trigger power down sound     
          state = S_POWERDOWN; 
                         
        }
        

      //** button state has NOT changed  **//      
      }else{
               
        //check if -still- pressed
        if(buttonState == LOW) {
          //Serial.println(F("READ STATE: "));
          //Serial.println(myDFPlayer.readState());
                    
         if(doCompletionCheck == true){      
            Serial.println(F("watching audio file for completion"));     
            
            //dedicated function attempt for checking track completion
            trackComplete(2, 3, 'l');              
            
            /*
            if(myDFPlayer.readType()==DFPlayerPlayFinished && myDFPlayer.readCurrentFileNumber()==2) {  
              Serial.println(F("audio finished..."));
              
              myDFPlayer.loop(3);
              doCompletionCheck = false;
            }
            */
          } 

                                   
        }else{
          //Serial.println(F("BUTTON STILL -NOT- PRESSED........ "));    
        }
      }

      //update/save the current button state for next loop/cycle
      lastButtonState = buttonState;
    break;

    case S_POWERON:
      //play power up      
      myDFPlayer.play(2);      
      doCompletionCheck = true;      
      
      //return to button checking
      state = S_IDLE;
    break;

    case S_POWERDOWN:     
      myDFPlayer.play(4); 
      
      //return to button checking
      state = S_IDLE;    
    break;
  }

}

//playMode options: p = play or l = loop
void trackComplete(int currTrack, int newTrack, uint8_t playMode) {  
  Serial.print(F("Current Track that is playing: "));
  Serial.print(myDFPlayer.read());
  Serial.print(F(" / "));
  Serial.println(myDFPlayer.readCurrentFileNumber());
  
  Serial.print(F("Checking for completetion of track: "));
  Serial.println(currTrack);
  Serial.println(F(""));
  Serial.println(F(""));
  
  //if(myDFPlayer.readType() == DFPlayerPlayFinished && myDFPlayer.read() == currTrack) {
  if(myDFPlayer.readType() == DFPlayerPlayFinished && myDFPlayer.readCurrentFileNumber() == currTrack) {
  //if(myDFPlayer.readType() == DFPlayerPlayFinished) {
    
    doCompletionCheck = false;
    
    Serial.print(F("TRACK MATCH: "));
    Serial.print(myDFPlayer.readCurrentFileNumber());
    //vs.
    Serial.print(F(" / "));
    Serial.println(myDFPlayer.read());        
    
    Serial.println(F("Setting completion listener off---->"));
    switch (playMode) {
      case 'p':
        myDFPlayer.play(newTrack);
      break;

      case 'l':
        delay(800);
        myDFPlayer.loop(newTrack);
      break;      
    }  
      
  }
  //back to button checking
  state = S_IDLE; 
}

Seems like I can not get a consistent results from the DFPlayerFinished and currentTrack functions… to reliably know when the 002.wav audio clip has finished so I can automatically trigger a looped file after that.

Are you supposed to send a stop() command or something before playing/looping another file?

Anyone see what I’m doing wrong? or have some experience with these DFPlayers? (and how to detect when a clip has finished?)

Bonus question #2:

Whenever I power on my Arduino or power it down… I get a loud POP from the speaker connected to the DFPlayer… how can I fix this? I have it set up as shown in the link… with an additional 1k resistor on both RX & TX lines…

Thanks
-xl

To detect that a track is completed (assuming you start them one at a time) the link provided has the answer! just read the Busy pin in your loop routine

Busy pin LOW -> playing
Busy pin HIGH -> finish!

As to your second question, to get rid of the 'pop', the general rule is that you should turn your amp(speaker) ON last and OFF first ie
if you want to get rid of the 'pop' consider adding an external Amp.

if you google "amplifier anti pop circuit" you find some circuit examples.

Thank you for the reply..

Question:

1.) Is there no way to detect/tell if the song has ended via software? Function? Callback?...etc?

2.) Many people use these little DFPlayer modules... (many who DO NOT use an external amp)..
How are they not having this issue? (or have dealt with it?)

The DFPlayer is being powered by the VIN pin.. (I could try an external power supply and just connected grounds perhaps?)

As far as turning ON the speaker.. I could maybe use an I/O pin? (not sure if that will provide enough juice though)..

but that would never work for cutting the power.. (as the power is cut, meaning the Arduino isnt powered either)..

Perhaps its my approach in the sketch that is the problem? (how I am checking for the clip completion?)

xl97:
1.) Is there no way to detect/tell if the song has ended via software? Function? Callback?..etc?

what happened when you tried readState()?

EDIT:

On a project I did, I used the function like this to decide to play a new file or use the advertise() capability:

void playSystemAlert(unsigned char alert)
{
  if(userSpecificData.systemSounds)
  {
    uint32_t timerStart = millis();
    int currentState = MP3.readState();
    if(currentState != 513)
    {
      MP3.playFolder(0x00, alert);
    }
    else
    {
      MP3.advertise(alert);
      while(millis() - timerStart < 2000) // fadeTimer has multi uses here!!!
      {
        // do some housekeeping functions
      }
      MP3.stopAdvertise();
    }
  }
}

** well somehow my post got lost?? (bummer)

@BulldogLowell

Thanks for the reply.

I stumbled upon the readState() method… but wasnt sure on how to utilize it? (or what responses get returned and what they meant)

I tried it once… (returned 512, but that was also the size of my microSD card I am using… so ignored it).
When tried in the loop of checking for audio completion… I got a 513 response… (but wasnt clear what that even meant)

I see you are purposely checking for a 513 state/response… (or lack of)… so that must mean file is currently playing?

I wonder if its worth trying to check not only the readState, but also in conjunction with the file# to ensure better reliability?

What is the difference between trying to check the end of the file/completion using:

myDFPlayer.readType() == DFPlayerPlayFinished
vs
myDFPlayer.readState()

It would be nice to get a breakdown of the differences between these functions:

myDFPlayer.read() 
vs
myDFPlayer.readCurrentFileNumber()

Both get current file# of track playing???

or more so:

myDFPlayer.readType() == DFPlayerPlayFinished
vs
myDFPlayer.readState()

When & why to use one over the other?

Perhaps the following:

In my ‘button still pressed’ loop above…

I will still call my trackComplete() function

if(doCompletionCheck == true){     
    trackComplete(2, 3, 'l');             
}

I guess technically I dont even need the first currTrack parameter anymore? (unless there is a way or should be used to track the completion of the right audio file?)

and update my trackComplete() function to be like:

void trackComplete(int currTrack, int newTrack, uint8_t playMode) {   
  if(myDFPlayer.readState() != 513) {   
    //stop calling this function in IDLE loop
    doCompletionCheck = false;

    switch (playMode) {
      case 'p':
        myDFPlayer.play(newTrack);
      break;

      case 'l':
        delay(800);
        myDFPlayer.loop(newTrack);
      break;     
    } 
     
  }
  //back to button checking
  state = S_IDLE;
}

I cant seem to ever consistently get the end of the track ‘callback’… either never or only ‘sometimes’…

When I get it ‘sometimes’… it cuts the 002.wav file short to start the loop… (to me that says the file completion was falsely detected/triggered)…

Thanks!

because this is a Serial device, you have to develop your program flow to not be too obtrusive. I'd look to make calls to readState() every 100 or 250milliseconds or so, so as not to flood the communications. You should be able to deal with such a small delay in audio playback...

So you think I am ‘flooding’ the DFPlayer with serial commands/calls too fast? Is that it?

Where is a list of readState() return values? So I can understand what values mean what? (ie: how did you know 513 meant file is currently playing?)

And you think adding a delay(100); in my trackComplete function will address/fix this?

xl97:
So you think I am 'flooding' the DFPlayer with serial commands/calls too fast? Is that it?

Where is a list of readState() return values? So I can understand what values mean what? (ie: how did you know 513 meant file is currently playing?)

And you think adding a delay(100); in my trackComplete function will address/fix this?

Look at the data sheet for the device.

delays suck; use a check against millis()

True..
No blocking delays...
(I was just trying to relay the information for confirmation)..

But ultimately.. you just think I am sending the serial request too often/fast? (odd that it either wouldnt happen/play anything... or it cuts my initial clip (002.wav) I am listening for completion, short to start the 'looping' of track 3).

Also.. since you are using it..

what is the difference between:
advertise()
and just calling another/new
play()
action/command?

xl97:
True..
No blocking delays...
(I was just trying to relay the information for confirmation)..

But ultimately.. you just think I am sending the serial request too often/fast? (odd that it either wouldnt happen/play anything... or it cuts my initial clip (002.wav) I am listening for completion, short to start the 'looping' of track 3).

Also.. since you are using it..

what is the difference between:
advertise()
and just calling another/new
play()
action/command?

I already gave you my thoughts on checking the playback, less is more regarding Serial.

advertise() pauses the current playback, plays the desired music/sound and resumes playback when you call stopAdvertise().

I use it to allow for alerts during music playback, but you have to know how long it takes the advertisement to playback. So, if you use it, just put the alert file and the duration into a struct,

struct Ads {
  int file;
  uint32_t duration;
}

Ads ads[] = {
  // here
}

just call the file and pause the duration in your function...

BulldogLowell:
Look at the data sheet for the device.

Are you referring to the .pdf of the module? (I did see anything about the readState() response values?)

Or are you referring to the datasheet of the chipset itself? Which I -think- is this one? (YX5200-24SS)

xl97:
Are you referring to the .pdf of the module? (I did see anything about the readState() response values?)

Or are you referring to the datasheet of the chipset itself? Which I -think- is this one? (YX5200-24SS)

the pdf outlines the various commands, represented in a byte chain that is interpreted by the module.

I had a hell of a time expanding that library to get more from the device, but it was over a year ago, so I've forgotten much of it. I'd stick to the library methods until you get playback working as you want it to work.

I'm not even sure on HOW to read/interpret those byte chains I see in the documentation.

Looks like readState() has 4 possible return responses. (I see no mention of 512, or 513..etc anywhere)

Is that the outcome/response from those 'byte chains' they list?

Interpretation of returned data
Returned Data Status
7E FF 06 42 00 01 01 xx xx EF - A track in USB flash drive is being played
7E FF 06 42 00 02 02 xx xx EF - A track in SD card is paused playing
7E FF 06 42 00 01 00 xx xx EF - A track in USB flash drive is stopped playing
7E FF 06 42 00 10 00 xx xx EF - Module in sleep

Somehow you calculate: "7E FF 06 42 00 01 01 xx xx EF" to get 512 or 513...etc?

When I got home tonight.. I'll focus solely on the readState() method.. and see if I can get some stable response.

This is what I have found with the DFPlayer Mini Module (Marked : MP3-TF-16P) :

When it is finished playing a file from T-Flash Card, it returns the following string :

7E FF 06 3C 00 00 yy xx xx EF - yy is the track number that is finished played

So there is no need to Read Status or monitor the Busy pin all the time,
it will send you the above string when it is finish playing.
Your Serial Receive routine must listen for the message and confirm.

Also found in another document :
3.3.2 .Returned Data of Track Finished Playing
U-Disk finish playback 1st track 7E FF 06 3C 00 00 01 xx xx EF
U-Disk finish playback 2nd track 7E FF 06 3C 00 00 02 xx xx EF
TF card finish playback 1st track 7E FF 06 3D 00 00 01 xx xx EF
TF card finish playback 2nd track 7E FF 06 3D 00 00 02 xx xx EF
Flash finish playback 1st track 7E FF 06 3E 00 00 01 xx xx EF
Flash finish playback 2nd track 7E FF 06 3E 00 00 02 xx xx EF
1.The module will enter into pause status automatically after being specified playing, if customers need
such application, they can specify track to play ,the module will enter into pause status after finishing
playing ,and wait for the commands sent by MCU.
2 In addition, we opened a dedicated I/O as decoding and pausing status indication. See Pin 16, Busy
1).Output high level at playback status;
2).Output low level at pause status and module sleep;
3. For continuous playback applications, it can be achieved as below, if it finishes the first tracking of the
TF card, it will return
7E FF 06 3D 00 00 01 xx xx EF
3D ---- U-disk command
00 01 ---- expressed finished playing tracks.
If the external MCU receives this command, please wait 100ms. And then sending the playback command
[7E FF 06 0D 00 00 00 FF EE EF], because inside the module it will first initialize the next track
information. In this case, the module can be played continuously.
4. If the currently finish playing the first song, the track pointer automatically point to second song, If you
send a "play the next one” command, then the module will playback the third song. And, if the module
finishes playing the last one, the player will automatically jump to the first pointer, and pause.
5. After specifying device, the module play pointer will point to device root directory of the first track,
and enters the pause state, and wait MCU sending track playing command.

You might find my post #20 on https://forum.arduino.cc/index.php?topic=522677.15 of interest.

It seems the DFPlayer module is sometimes returning two Finished messages. If you receive a finished message you then need to ensure there isn’t another one waiting to be processed as well. Otherwise the second will be hanging around and appear to be the Finish for your next sound.