CS 198: Lab #3

Lab Exercise 3: Rhythms


Tasks

  1. Start up Max. Create a new patcher.
  2. Create a script engine object.

    mxj net.loadbang.jython.mxj.ScriptEngine 1 4 @script lab3

  3. Still in Max, create two more objects. Type makenote 100 into one of the objects and noteout 10 into the other. Then copy and paste both objects so you have two copies of each.

    Connect the leftmost output of the ScriptEngine object to the left input of the first makenote object. Connect the second output of the ScriptEngine object to the rightmost input of the first makenote object (duration). Then connect the third and fourth outputs of the script engine to the other makenote object.

    Attach the two outputs of each makenote object to its corresponding noteout object. The left output of each makenote should go to the left input of the nouteout. The right output of the makenote should go to the middle input of noteout. Change the second noteout object to noteout 1, or at least a channel other than 10.

    Save your patcher to the Desktop.

  4. Create two message objects. In one message object type script lab3 and in the other put playone 250. The first message box will reload the python script after you have made changes. The play message box will call a function called play with one argument in the lab3.py file.
  5. Open TextWranger and create a lab3.py file on the Desktop. Put your name and the data at the top of the file inside a comment (comments start with a #). Go to the Options menu in Max and choose the File Preferences... option. Set up a new file path to the Desktop.
  6. In your lab3.py file, define a function called playone that takes one argument. The argument eventually will be the current speed of the metronome, designated in ms between each beat. Have the function give pitch values to outlets 1 and 3, and duration values to outlets 2 and 4. The order in which you assign the outlet values is important. Because the pitch values are also generating a bang, you want to set the duration values first. The following is an example.
    def playone( unit ):
        maxObject.outlet( 1, unit-5 )
        maxObject.outlet( 3, unit-5 )
        maxObject.outlet( 0, 52 )
        maxObject.outlet( 2, 55 )
    

    If you lock your patcher, load your script (script lab3 message) and then click the play message, you should get both a percussion and a piano sound simultaneously. Because we hard-coded the numbers, it only plays one thing.

  7. Go back to your python file. At the top of the file, we need to create three variables to hold the state of the program in between calls. One will hold the pitch and duration information for the tune, the other two will be counter variables. Type them in as below.

    ticks = 0
    note = 0
    tune = [ [50, 2], [52, 2], [53, 1], [53, 1] ]
    

    The ticks variable will keep track of where the program is within the note. The note variable will keep track of which note was played last, and the tune variable is a list of lists. Each sub-list contains two values: pitch, and duration.

    To access the pitch of the third note in the tune, we would use the syntax tune[2][0]. To access the duration of the third note, we would use the syntax tune[2][1]. The first index accesses the outer list elements, the second index accesses the inner list elements. Python permits us to make lists elements of lists, so it is possible to have lists of lists of lists, and so on.

  8. Now create a new function called playtwo that takes one argument. As with the playone function, the argument will be the number of ms between metronome beats. We want to make the function playtwo cycle through the tune defined in the prior step.

    To make sure we're accessing the proper variables, put a global statement for tune, ticks, and note.

    The algorithm we want to execute is as follows. Copy and paste the comments below, then we'll fill in the python code.

    def playtwo(unit):
      global tune
      global ticks
      global note
    
      # if the ticks counter is at 0
          # access tune and assign the pitch to play to a local variable
          # access tune and assign the duration to a local variable
          # set outlet 1 to the duration
          # set outlet 0 to the pitch
    
      # add 1 to the ticks counter
    
      # if the ticks counter is equal to the length of the current note
          # set the ticks counter to 0
          # add 1 to the note counter
          # set the note counter to the note counter modulo the length of the tune
    
  9. Once the function is complete, go back to Max and put in a message box playtwo 250. Reload your script and try out the function. Does it play through the tune?
  10. We could now make a second set of global variables and another function to play a second tune/rhythm, but that becomes inefficient very quickly. We would like to have a general function that can play any tune represented as a list.

    The key is to think about all the information required to play a single tune. We need a set of pitch/duration pairs, which note is currently being played, and how many ticks within the note have passed. Therefore, we need three pieces of information. We can store all three inside a single list, which we'll call the tunedata. Create two new global variables tune1 and tune2 as below.

    tune1 = [0, 0, [ [50, 2], [52, 2], [53, 1], [53, 1] ] ]
    tune2 = [0, 0, [ [60, 1], [58, 1], [63, 2], [62, 2] ] ]
    
  11. Create a new function playtune as given in comment form below. We'll fill in the details together.
    # tune data is a 3 element list: ticks, note, pattern
    # pattern is a list of 2-element lists
    def playtune( tunedata, unit, outletbase ):
    
      # if ticks is 0, play the pitch indexed by note
          # get the pitch to play out of tunedata
          # get the duration to play out of tunedata and multiply by unit
          # set the duration outlet
          # set the pitch outlet
    
      # move ticks forwards
    
      # if we're at the end of the current note
          # set the ticks to zero
          # increment the note counter
          # set the note counter to the note counter modulo the length of the tune
    
      # return
    

    Note how the playtune function is identical to the playtwo function in terms of what it does. It just works on the parameters rather than global variables, which makes it much more general purpose.

  12. Finally, we need a function to call from max that will play our global tune1 and tune2 structures. Copy the code below and we'll fill in the details.
    def play( unit ):
       # access global tune1
       # access global tune2
    
       # call playtune with tune1, unit, and 0 for the outlet base
       # call playtune with tune2, unit, and 2 for the outlet base
    

    Create a new message play 250 in Max and test your function. Then put a metronome on it (metro 250) and let it run.

Once you are comfortable with the above material, go on to the next project.