Shop OBEX P1 Docs P2 Docs Learn Events
Pick a number from 1 to 16 — Parallax Forums

Pick a number from 1 to 16

Hello all,

I am working on a Halloween costume that is to play fart sounds (little childish i know, but who cares..LOL)

I want it to play up to 16 different sounds with the push of a button, but I want what sound file it plays to be random and not repeating. It would be like a lotto, when it plays a sound file, it flags it as played, and it will not play it again. But when all the sound files have been played, it resets the flags for another round.

I gave it a shot, and I for the life of me I cant figure out why it is not working the way it should. I think it is getting stuck looking for a new random number. Does anyone have any ideas on what I can do?

Thanks
TC
CON
        _clkmode = xtal1 + pll16x                                               'Standard clock mode * crystal frequency = 80 MHz
        _xinfreq = 5_000_000


CON
''///////////////////////////////////////////////////////////////////
''///
''///  Pins
''///
''///////////////////////////////////////////////////////////////////

  sd_CS     =   16
  sd_DI     =   17
  sd_CLK    =   18
  sd_DO     =   19

  audio_L   =   20
  audio_R   =   21

  button    =   23  
VAR
  long  track_flag[16]
  long  track_count
   
OBJ
  wav       :   "V2-WAV_DACEngine"
  ran       :   "RealRandom"
  
PUB start | i

  wav.begin(audio_L, audio_R, sd_DO, sd_CLK, sd_DI, sd_CS, -1, -1)
  ran.start

  repeat i from 1 to 16
    track_flag[i] := false
    
  main

PUB main

  repeat 20
    random

  repeat

PUB track(track_number) | i

  case track_number

    1:  wav.play(string("01.wav"))
    2:  wav.play(string("02.wav"))
    3:  wav.play(string("03.wav"))
    4:  wav.play(string("04.wav"))
    5:  wav.play(string("05.wav"))
    6:  wav.play(string("06.wav"))
    7:  wav.play(string("07.wav"))
    8:  wav.play(string("08.wav"))
    9:  wav.play(string("09.wav"))
    10: wav.play(string("10.wav"))
    11: wav.play(string("11.wav"))
    12: wav.play(string("12.wav"))
    13: random 
    14: random 
    15: random 
    16: random

  repeat while wav.getBytePosition < wav.getByteSize  
    track_flag[track_number] := true
    
  track_count := track_count + 1

  if track_count == 16
    repeat i from 1 to 16
        track_flag[i] := false 

PUB random | temp

  temp := (ran.random / $FFF_FFFF) + 1

  if track_flag[temp] == true
    random

  else
    track(temp)

Comments

  • Why do you call random again on failure? That looks like a stack overflow just waiting to happen. No current Spin compiler can optimize out tail recursions. You should instead make it loop until an acceptable number is found.

    Also, track calls random again on invalid indices, which just fill up the stack more. You shouldn't do that either. It will eventually crash.

    It looks like you want to play each track once in a random permutation. I guess this is so it doesn't play the same thing twice in a row. However, your code doesn't prevent that - it could end a cycle of 16 with one sound, and then start over again with the same sound.

    You should look up the Knuth shuffle. Then, to prevent it from playing the same sound twice in a row at the end and beginning of two consecutive cycles, you can make sure they aren't the same and then swap the first sound with a random other sound to fix it. Or, you could keep track of the last 4 sounds played, and keep rejecting random numbers until the number isn't any of those last 4.

    You should use "ran.random & $F" instead of "ran.random / $FFF_FFFF". Your code will only ever return -8 to 7, since ran.random is negative half of the time. These negative indices are probably the most serious problem here - it might sort of work if you just fix that.


    You'll probably be happier indexing your sounds from 0-15 instead of from 1-16. Computers count from 0, and the sooner you switch to 0-indexing, the sooner you'll stop needing to worry about that pesky +1 everywhere. Also, your track_flag array has 16 elements, and yet you write to element 16, which doesn't exist, clobbering track_count instead.
  • TCTC Posts: 1,019
    edited 2016-10-29 02:32
    Thank you Electrodude for the input.... I know the code is not perfect yet. I have trouble understanding somethings, so to battle what I dont fully understand (or to keep it simple at the start) I will do things like count from 1 to 16, knowing that binary starts at 0. This code I posted is me doing about 3 hours of work on it, after just getting done with a 60 hour work week (12 hour days). So needless to say, my mind is not in the best shape to worry about the formality's of a project that will only ever see 3-4 hours of use.

    Why do you call random again on failure? That looks like a stack overflow just waiting to happen. No current Spin compiler can optimize out tail recursions. You should instead make it loop until an acceptable number is found.

    I was calling random on failure only because I was thinking " If the value was already used, try again" I know it is not the best idea, but I did hope it would work.

    Also, track calls random again on invalid indices, which just fill up the stack more. You shouldn't do that either. It will eventually crash.

    Sorry, I'm not fully understanding what you are talking about.

    It looks like you want to play each track once in a random permutation. I guess this is so it doesn't play the same thing twice in a row. However, your code doesn't prevent that - it could end a cycle of 16 with one sound, and then start over again with the same sound.

    That is exactly correct. What fun would it be if I knew what sound was going to be played. Plus, the repetition would be mind numbing.

    You should look up the Knuth shuffle. Then, to prevent it from playing the same sound twice in a row at the end and beginning of two consecutive cycles, you can make sure they aren't the same and then swap the first sound with a random other sound to fix it. Or, you could keep track of the last 4 sounds played, and keep rejecting random numbers until the number isn't any of those last 4.

    You should use "ran.random & $F" instead of "ran.random / $FFF_FFFF". Your code will only ever return -8 to 7, since ran.random is negative half of the time. These negative indices are probably the most serious problem here - it might sort of work if you just fix that.

    I will check out the Knuth shuffle, and try the correction.

    Also, your track_flag array has 16 elements, and yet you write to element 16, which doesn't exist, clobbering track_count instead.

    I seen this problem a little after I posted the question.






  • JonnyMacJonnyMac Posts: 9,104
    edited 2016-10-29 14:11
    I do this stuff all the time -- in fact, this kind of feature is built into the Propeller-powered AP-16+ audio player that I co-designed and coded.

    You'll need a couple global variables: one called last, another called played. With these you can make sure all files have played before any repeats, and that you don't play the same file back-to-back.

    Then use this method to get a file number from 0..15
    pub get_random | lotto
    
      repeat
        lotto := prng.random & %1111                                ' 0..15
        ifnot (lotto == last)                                       ' if not back-to-back
          ifnot (played & |<lotto)                                  ' and not already played
            played |= |<lotto                                       ' add to play list
            if (played == $FFFF)                                    ' if list is full                                    
              played := 0                                           '  reset it
            last := lotto                                           ' save for next cycle
            return lotto                                            ' return file to play
    

    I use Heater's PRNG object, but you can use ?cnt if you want; the key is to mask the random value with %1111 to create a 0..15 range. Since you're using a button press, the cnt register could be anywhere at the time of the press.
  • Here's an example of Heater's Pseudo Random Number Generator displayed on a QuickStart board. I want t make this into a retro computer diplay 32 wide with as few of pins as possible.
  • I just found this thread and tried JonnyMac's method out. It works great and was easy to set up. Thanks again, JM!

    One question...

    In the method above, how do you change the number of files? I am familiar with the code in PBasic that uses modulus.

    I have tried changing this line to reduce the range, from:
    lotto := prng.random & %1111
    

    to one less, such as:
    lotto := prng.random & %1110
    

    But then I get a lockup if the random number is 15 (apparently, I can't directly verify it). So I tried changing this line to match, from:
    if (played == $FFFF)
    

    to:
    if (played == $FFFE)
    

    But that doesn't fix the lockup. Something is out of range, but how do I fix it?
  • Jeff Haas wrote: »
    I just found this thread and tried JonnyMac's method out. It works great and was easy to set up. Thanks again, JM!

    One question...

    In the method above, how do you change the number of files? I am familiar with the code in PBasic that uses modulus.

    I have tried changing this line to reduce the range, from:
    lotto := prng.random & %1111
    

    to one less, such as:
    lotto := prng.random & %1110
    

    But then I get a lockup if the random number is 15 (apparently, I can't directly verify it). So I tried changing this line to match, from:
    if (played == $FFFF)
    

    to:
    if (played == $FFFE)
    

    But that doesn't fix the lockup. Something is out of range, but how do I fix it?

    Remember the numbering begins at 0, so the 15 you are looking for is actually 15-1, which is encoded into a 16-bit word, Try
    if (played == $7FFF)
    

    No I haven't tested it.
  • Thanks, but that doesn't fix it.

    I use a button to trigger the method, and added PST to show what the value of the lotto each time it's run.

    When I change the number of files from %1111 to %1110, the method ends up in a loop and never comes out of it. So the check of the "played" values doesn't work, the PRNG keeps retrying to find a new value.
    pub get_random | lotto            
    
      repeat
        lotto := prng.random & %1110                                ' 0..15
        pst.NewLine
        pst.Str(String("lotto = "))
        pst.Dec (lotto)
        ifnot (lotto == last)                                       ' if not back-to-back
          ifnot (played & |<lotto)                                  ' and not already played
            played |= |<lotto                                       ' add to play list
            if (played == $FFFF)                                    ' if list is full                                    
              played := 0                                           '  reset it
              pst.NewLine
              pst.Str(String("Reset"))
            last := lotto                                           ' save for next cycle
            return lotto                                            ' return file to play
    

    There's a part of the technique here I don't understand.
  • This line,

    lotto := prng.random & %1110

    picks even numbers ranging from 0 to 14

    You were right the first time: the number should be &'d with %1111 to get all numbers from 0 to 15.

    -Phil
  • Phil, I 'm trying to figure out how to change the number of items in the list, say for a list of 0 to 10 instead of 0 to 15. So far I see that any change from %1111 ends up with the method eventually getting stuck in a loop, trying over and over to find a number that it hasn't already picked, and not returning to the method that called it.
  • >>> lotto := prng.random & %1110
    This will only give even numbers.
    >>> if (played == $FFFF) ' if list is full
    This needs to check for $3FFF

    Attached is a working program.

    John Abshier
  • Roy ElthamRoy Eltham Posts: 3,000
    edited 2016-11-02 18:22
    Jeff Haas,

    If your list is arbitrary length, then use modulus.

    lotto := prng.random // 10

    Will give you a number from 0 to 9. The 10 can be any number (although numbers less than 1 are likely not what you want).

    Using & requires that your list length/number be a power of 2.

    Note: you'll have to change the "played == $FFFF" comparison to have the proper number of bits in it to match your list size.
  • Heater.Heater. Posts: 21,230
    edited 2016-11-02 19:12
    Be aware that taking the modulus of the random number generators output to limit the range will introduce a bias. So this:

    lotto := prng.random // 10

    will produce some numbers more often that others.

    Not totally sure but I think mod 10 will produce the numbers 0 to 5 about twice as often as the numbers 6 to 7.

    For a nice explanation and solution see here: https://zuttobenkyou.wordpress.com/2012/10/18/generating-random-numbers-without-modulo-bias/

    Random numbers are slippery things.
  • For small n, compared to the range of the random number generator, the bias is pretty insignificant, since it occurs only when the modulus is applied to results greater than or equal to the largest multiple of n. For large n, yes, you do have to be aware of the bias.

    -Phil
  • Heater.Heater. Posts: 21,230
    I think you may be right Phil. My estimate is way off.

    Still, it's a bias. And easy to remove. If you are fussy like me :)

  • The same bias is true for and'ing when it's a power of 2 value, since it amounts to the same thing.
  • Roy Eltham wrote:
    The same bias is true for and'ing when it's a power of 2 value, since it amounts to the same thing.

    Can you elaborate, Roy? If the desired sub-range is 0 to 2**n - 1 and the range of the RNG is 0 to 2**m - 1, where m >= n, there is no bias from anding.

    -Phil
  • Phil, you are right, I was thinking something other than what I said. And'ing of non-power of 2 values is what I was thinking can introduce biases, but then my brain flipped it. Sorry.
    However, in any case, I think the bias isn't going to matter in the sample sizes here.
  • John, Roy:

    Thanks for the explanation - I've got it now.

    For anyone else in the future...the easy way to generate the (played == ) hex value is to go to:
    http://www.binaryhexconverter.com/binary-to-hex-converter

    Then type in the number of bits you want to track. Hit Convert and there's your answer. There are also lots of apps for your smartphone, but you're already on the computer, so...

    And Heater, thanks! One of the reasons I like this forum is how I learn new things. I didn't know about the modulo bias. Really interesting discussion!
  • JonnyMacJonnyMac Posts: 9,104
    edited 2016-11-03 18:06
    The modulus operator is the most general and will work with any value. As the OP wanted a 16-unit range I optimized that method demo with & which is faster than //.

    I used a variant of that code in my friend's Stranger Things project. He had 200 WS2811 Christmas type bulbs that he wanted to randomly populate. Here's the method that controls populating the initial colors of the string.
    pub colorize_string(ms) | list, last, full, ch, idx, mask
    
    '' Randomizes the colors of the string
    '' -- ms is the fade-up timing
    '' -- attempts to create even distribution of color table
    
      list :=  0
      last := -1
      full := -1 >> (32 - COLORS)                                   ' make full mask
    
      if (ms < 50)
        ms := 0
      
      repeat ch from 0 to STRIP_LEN-1                               ' colorize all bulbs
        repeat
          idx := ||(prng.random // COLORS)                          ' randomize color 
          if (idx <> last)                                          ' if not a repeat
            mask := 1 << idx                                        '  create color mask
            ifnot (list & mask)                                     '  if not already used this cycle
              list |= mask                                          '  add to list
              if (list == full)                                     '  if all colors used
                list := 0                                           '   reset the list
              last := idx                                           '  save for next cycle
              quit                                                  '  quit to color the bulb
          
        fade(ch, ColorTable[idx], 1, 255, ms)
    
    As you can see, I used modulus so that Matt could update the number of colors in the string.
Sign In or Register to comment.