CS 314 Assignment 9: Memos and Music

Due: November 21, 2017.

Files: Cons.java   Cons.jfugue.java   Event.java     mariagedamour.java     patmatch.lsp   patterns.lsp   patm.lsp

Some functions that you may need are provided in the file Cons.java, and you will need some of your functions from the last assignment. Question 1 may be done in Java or in Lisp; the rest should be done in Java.

  1. Write a recursive function Object solve(Cons e, String v) that solves the equation e for variable v, which we assume occurs at most once in e.

    This function does the same thing as the earlier version of solve; in this version, use patterns to rewrite the given expression. The base cases are the same as before, and you can copy your code for those cases.

    1. If the left-hand side (lhs) of e is v, return e.
         (solve '(= f (* m a)) 'f)  =>  (= f (* m a))
    2. If the right-hand side (rhs) of e is v, return e with its arguments reversed.
         (solve '(= (* m a) f) 'f)  =>  (= f (* m a))
    3. If the rhs is not v and not a Cons, return null.

    4. If the rhs of e is a Cons (i.e. an operation), try to solve the equation by applying algebraic transformations from a list of patterns, algpats. For each pattern in the list, try to transform the expression e using the pattern. If the transformation works (is not null), call solve recursively on the transformed version of e; if the result of solve is not null, return that result. Otherwise, continue through the list of patterns. If all patterns have been tried, return null.

    An initial list of patterns is provided; add patterns to complete the set.

  2. Write a method hashCode() for the class Cons. The hashCode of a Cons should be the XOR of the hashCode of the first times 17 and the hashCode of the rest times 127. The hashCode of null should be 0; use an auxiliary function to make sure that you do not try to call hashCode() on null, which would cause a runtime exception.

  3. Write a class Memoizer (in a file Memoizer.java) that implements memoization of a function call. Memoization is a technique for wrapping an expensive function with a memory structure that remembers previously computed values of the function. If the function is called again with the same argument value, it will be cheaper to look up the saved value than to recompute it.

    The class should be created using new Memoizer(Functor f) where f is a functor that wraps the function associated with the memoizer.

    Use a HashMap within the Memoizer to associate function values with argument values. You can Google "java hashmap" to get documentation of a HashMap.

    Memoizer should have a method Object call(Object x) to perform the wrapped function call. The call method should operate as follows:

    1. First, call should look up the parameter x in the HashMap; if it is present, return the value associated with x (without calling the Functor).

    2. Otherwise,
      1. Call the Functor to compute the value of fn(x).
      2. Add the value fn(x) to the HashMap for the key x.
      3. Return the value fn(x).

    Reference: http://en.wikipedia.org/wiki/Memoization

  4. An Austin company is making a digital piano, drum and percussion synthesizer, and they have hired you to write the software. Since musicians are not always good programmers, the input language that is used to specify a music pattern needs to be simple. The software needs to convert the specified pattern into a time sequence of I/O commands to drive the synthesizer units. Since music is a language, we expect that a music program will be structured as a tree.

    The synthesizer driver program is implemented as a discrete-event simulator, based on a PriorityQueue. You can Google "java priorityqueue" to get documentation of a PriorityQueue.

    A discrete-event simulator processes a sequence of events which are scheduled at particular clock times. The PriorityQueue is used to hold the pending events, with the priority being the time at which an event is scheduled to occur. In operation, the simulator removes the highest-priority (smallest time value) event from the queue, sets the current time to the time of the event, and executes it; the event will typically perform some action, and it may schedule other events for the future. The simulation stops if the queue becomes empty.

    We will assume that time values are integers. Events that occur at the same integer time are considered to be simultaneous, regardless of their order, since the computer is much faster than the music output. A time duration of 1 represents a 1/16 note duration.

    An Event has a scheduled .time() and an .action(); this class is provided. An action is a list, consisting of a command followed by parameters; usually there is only one parameter, the length of time the command lasts. An event is added to the queue by the call addevent(PriorityQueue pq, Cons action, int time) .

    Execution of a command may emit an I/O command to the synthesizer. Write a function execute(PriorityQueue pq, Cons act, int time) that executes an action by emitting appropriate commands.

    The function emit(instrument, time, duration) sends a command to an instrument; the arguments are all int. The instrument values are as follows:

    Commands are as follows, with d representing the duration of the action; usually a duration should be a power of 2, except for rest.


    (seq (boom 4) (bell 4))
    (seq (repeat 2 (kaboom 4)) (repeat 3 (cymbal 2)))
    (repeat 2 (seq (piano 0 C5 4) (piano 0 D5 4) (piano 0 E5 4)))

    It will be helpful to write the totaltime() function (below) first.

  5. Write a function int totaltime(Cons action) that calculates the total time required for an action. For most actions, the time is simply the duration of the action. A repeat action has a time that is the repeat count times the time of the sub-action. A seq action takes the sum of the times of its sub-actions. A sync action takes the maximum time of its sub-actions.

  6. A round is "a musical composition in which two or more voices sing exactly the same melody" [Wikipedia]. Write a function Cons round( Cons melody ) that will take a music program and make a round out of it. We will assume that a round is formed by repeating the same melody with three different voices; the voice is indicated in the input melody by the variable ?i and should be replaced by 0, 1, and 2 to form the three voices. The second voice is delayed by 1/4 the length of the melody, and the third voice is delayed by 1/2 the length of the melody.

The jFugue system, http://jfugue.org, allows sounds to be produced from Java. An interface has been written to allow the output of your program to produce sounds through jFugue. Your program will not be any different whether you use jFugue or not; using jFugue may make debugging easier because you can hear whether the music sounds right. If you would like to use jFugue, download it from the class directory or the jFugue site and install it in the same directory that you use for your program. Probably the easiest way to install jFugue is to download the .jar file and unpack it in the same directory with your program using

jar xf jfugue-4.0.3.jar
In Eclipse, you may need to use "Assign as Working Set" to get Eclipse to use the .jar file. Use the file Cons.jfugue.java and rename it to be Cons.java .

mariagedamour.java is a file by a student that encodes the song Mariage d'amour ; this file can optionally be added inside the main() if you would like a longer piano piece. Compare to Richard Clayderman's version.

Reference: http://en.wikipedia.org/wiki/Discrete_event_simulation