Avoid repeating a random number within a set time?

I'm experimenting with code which will be part of a music player project. However my questions are not about its main purpose (pseudo randomising) but these:

Q1: Is my if() statement the simplest way to pad for 2-digit numbers?

Q2: The current code avoids the same track playing immediately after the previous one. What would be the best approach to avoid repetition within a certain set time? In the eventual sketch I'd probably set this to say a tolerable 20 minutes or so. Scaled for discussion on the code here: assuming one number printed every 100 ms, how to avoid an identical one within say 2 s? I'm reasonably familiar with millis() by now, but so far haven't managed it.

// Prints 5 sets of 15 pseudo random numbers in range: 1-20
int randNumber;
int previous;

void setup()
{
  Serial.begin(115200);
  delay(200);
  Serial.println("RandomSeedA0Test_NoImmedRepeat-3");

  randomSeed(analogRead(0));
  Serial.println();
  Serial.println(F("NEW SESSION"));

  for (int k = 1; k <= 5; k++)
  {

    for (int j = 1; j <= 15; j++)
      // set of up to 15 on one line, but ignoring adjacent repeats
    {
      // Print the numbers on one line.
      randNumber = random(1, 21); // Chooses 1 to 20
      if (randNumber != previous)
      {
        previous = randNumber; // Update previous
        // Pad so that always prints 2 digit numbers, to line up neatly
        if (randNumber < 10) Serial.print("0");
        Serial.print(randNumber); Serial.print("  ");
      }
    } // End of set
    Serial.println(); // Start new line for new session
    delay(1000); // Wait a bit before printing another set
  }
} // End setup

void loop()
{

}

/* Two typical results
  --------------------
Three of the five sets in each run had at least one adjacent repetition so was deliberately not printed.

  RandomSeedA0Test_NoImmedRepeat-3

  NEW SESSION
  11  03  01  13  14  12  20  17  07  12  13  12  09
  18  15  10  12  17  03  19  15  06  03  05  16  01  20  10
  09  10  05  10  01  11  08  01  16  18  04  14  13  18
  06  18  20  10  20  06  13  09  03  09  14  15  08  06  08
  20  12  07  09  10  17  01  13  19  06  15  06  02  18
  
  RandomSeedA0Test_NoImmedRepeat-3
  NEW SESSION
07  08  03  09  06  14  12  19  09  08  11  05  06  18  05  
14  09  02  11  06  09  12  06  14  01  08  10  20  08  
16  07  06  19  14  18  11  12  06  14  02  04  03  
19  07  19  08  12  16  19  01  19  07  19  20  17  05  
07  12  04  07  02  13  03  10  08  03  02  12  10  

*/

Q1. Yes, I see that alla time and it is probably the simple easy way.

More flexible: look into sprintf() which can place items into a fomat description you provide. Cousin of printf().

Q2. Idea only: for each possible number to be served, keep a "last time this was served" time stamp by stashing millis() when you serve it.

When you go to serve a new random selection, see if enough time has elapsed to make serving it OK.

if (millis() - lastServed[theItemIndex] < tooSoonConstant)… // too soon, try another

HTH, you got this I bet.

a7

1 Like

This is one of the problems with random numbers, what is truly random and what humans perceive as random are not the same.

As I said, that's not what my questions are about.

"Simplest" and "best" are subjective, and not defined in your question.

In general when people put on a random shuffle for music they expect it to shuffle like a deck of cards, ie no repeats at all and every track played. If you just don’t repeat in a set time then you might never play a specific track. I would use your pseudo-random generator to organise the tracks and then use your 20 min exclusion for when circling back round so you don’t have repeats in a noticeable period.

2 Likes

Thanks a7. Shortly after posting (and another coffee), I thought I'd cracked it. But I hadn't included the crucial indexed array of your suggestion!

I'll have another go at it, although I anticipate harder than I'd hoped. My music tracks vary widely in duration length (say 2 to 20 mins) . And the array would presumably have to be large to accommodate unlikely but possible very long gaps between repetitions. Haven't done much with arrays so should be a good learning exercise.

@pmagowan
Excellent points, thank you, I was over-thinking it! Your approach should be much simpler to implement.

@jremington

Which is why I only used one of those adjectives.

I think a7 confirmed my hope that it was indeed the simplest, so I can look at the other more versatile methods later.

the best approach

Both undefined. If the program works and meets your requirements, it is "simplest and best".

1 Like

The array would have to be as large as the number of items you are tracking.

And perhaps a parallel array of times rather than a single once size fits all constant, like

 if (millis() - lastServed[theItemIndex] < tooSoonForThis[theItemIndex])… // too soon, try another

but the shuffle may be preferable as it would mean hearing all the songs once in a random order before hearing them again.

a7

Eh? Those were in two quite seperate questions!

Perhaps you could read more carefully before dashing off a criticism?

Yes, that's definitely its major attraction. As well as being easier, I hope.

Also, as implied by my screenshot, the listener (mainly me) is very likely to navigate elsewhere reasonably soon - an hour at most? - after starting a folder averaging about 110 tracks.

I'll postpone your array exercise!

1 Like

If I had "a lot" of tracks and want to avoid to repeat a played track for ... a certain time, I would just store the played tracks in an array / ringbuffer. If a new random track is needed, compare if you find the track in the existing buffer.

/*
  Avoid repeating a random number
  https://forum.arduino.cc/t/avoid-repeating-a-random-number-within-a-set-time/1033379/

  items needs to be a lot larger than the size of the used buffer!
*/
const uint8_t items = 42;  // items on list
const uint8_t size = 10;   // size of ring buffer for already used items
uint8_t used[size];        // stores already used colors
byte index;                // ring buffer index

constexpr uint8_t btn {2};

void initUsed()
{
  for (byte i = 0; i < size; i++)
  {
    used[i] = random(0, items);    // scramble the content of the array (to allow 0 in the first n trys also)
  }
}

bool isUsed(byte actual)
{
  bool result = false;
  for (byte i = 0; i < size; i++)
  {
    if (actual == used[i]) return true;
  }
  return false;
}

int getRandomNumber()
{
  bool found = false;
  int rnd;
  while (found == false)
  {
    rnd = random(0, items);  
    if (!isUsed(rnd))
    {
      used[index] = rnd;
      index++;
      if (index >= size) index = 0;
      found = true;
    }
  }
  return rnd;
}

void action()
{
  byte actual = getRandomNumber();
    Serial.print("index: "); Serial.print(index);
    Serial.print("\tactual: "); Serial.print(actual);
    Serial.println();
    delay(100);
}


void setup() {
  Serial.begin(115200);
  Serial.println(F("Random with some uniqueness for the last minutes"));
  randomSeed(analogRead(A4));
  pinMode(btn, INPUT_PULLUP);
  initUsed(); 
}

void loop() {
  if (digitalRead(btn) == LOW) action();
}

if you really want the check "against playing time", use a structure and add a millis() timestamp to each entry in your buffer.

1 Like

@noiasca

Great, thanks a bunch!

I successfully ran it but now need to study it thoroughly to understand how it works. (I'm an 'Arduino programmer', a relatively novice one, and no C/C++ training.) I suspect it will then save me a lot of work. :slightly_smiling_face:

I've decided against a 'time-based' approach for the reasons mentioned. But another factor (for anyone familiar with the DFR Mini MP3 Player module) is that I'm not yet getting consistent capturing of the currently playing file name (track), which seems an essential requirement.

Edit: Forgot to ask. What exactly do you mean by "Random with some uniqueness for the last minutes" please?

don't take it literally, it's just any name of the sketch. There is no really minutes in the sketch...

The random seeding you are using may result in the exact same sequence of random numbers every time the thing is powered on. To avoid that, you could use EEPROM for the random seed:

void setup()
{
  unsigned long seed;
  EEPROM.get(0, seed);
  randomSeed(++seed);
  EEPROM.put(0, seed);
  ...  
}

This should improve randomization.

EDIT: And for the second question; If each song has an average duration of 3 minutes and you do not want to repeat anything which has played within the last 20 minutes, you only need to store the 7 most recent tracks:

const uint8_t TRACK_ARRAY_SIZE = 7;
uint8_t track_array_pos = 0;
uint8_t track_array[TRACK_ARRAY_SIZE] = {0};

bool can_play(uint8_t track_no)
{
  for (uint8_t i = 0; i < TRACK_ARRAY_SIZE; i++)
    if (track_array[i] == track_no) return false;
  track_array[track_array_pos] = track_no;
  if (++track_array_pos >= TRACK_ARRAY_SIZE) track_array_pos = 0;
  return true;
}

Argument "track_no" may not be "0" for this to work.

BigBoy programming on large systems would resort to a hash. With the Arduino small uC's you may wish to investigate BitArray.

I think it's been long enough so you don't get to play that card anymore…

Just sayin'. You know more than most of my relatives.

a7

Wait, don't you launch the track? So you should know what track is playing.

a7

@Danois90 ,

Thanks, yes, I have been using that EEPROM-based method following help from you and @anon57585045 in July. For the exercise under discussion I kept it simple. And actually, those 30 results look satisfactorily ‘random’ to me. Do you agree?

BTW, with this analog-based seeding method, intuitively I thought an unconnected wire would improve it, but in practice I see no apparent difference.

Thank you for the code, which I’ll try tomorrow.