Frère Jacques


jacques


ReactiveML has been used to implement a sequencer for mixed music. Here, we present a simple library that takes advantage of the expressiveness of ReactiveML to program the classic French musical round "Frère Jacques".

Traditionally, in western music, a melody is a sequence of notes, where a note is a sound event characterized by a pitch and a duration. Notes can be defined with the following types using a syntax similar to the one of OCaml.

(** Pitch class of a note (Af = A flat, As = A sharp, ...). *)
type pitch_class =
  | A | Af | As
  | B | Bf | Bs
  | C | Cf | Cs
  | D | Df | Ds
  | E | Ef | Es
  | F | Ff | Fs
  | G | Gf | Gs

(** Octave *)
type octave = int
(** Pitch *)
type pitch = pitch_class * octave
(** Duration *)
type dur = float
(** Note*)
type note = dur * pitch

The pitch is represented by a pair (pitch_class, octave), where pitch_class denotes one of the twelve semi-tones, e.g., A, A#, A♭, B, B#..., and octave is an integer. For instance (A,4) denotes the famous A 440Hz.

Using these data types, we can define the score of Frère Jacques as list of notes:

let jacques =
  [1.0, (F,4); 1.0, (G,4); 1.0, (A,4); 1.0, (F,4);
   1.0, (F,4); 1.0, (G,4); 1.0, (A,4); 1.0, (F,4);
   1.0, (A,4); 1.0, (Bf,4); 2.0, (C,5);
   1.0, (A,4); 1.0, (Bf,4); 2.0, (C,5);
   0.5, (C,5); 0.5, (D,5); 0.5, (C,5); 0.5, (Bf,4); 1.0, (A,4); 1.0, (F,4);
   0.5, (C,5); 0.5, (D,5); 0.5, (C,5); 0.5, (Bf,4); 1.0, (A,4); 1.0, (F,4);
   1.0, (F,4); 1.0, (C,4); 2.0, (F,4);
   1.0, (F,4); 1.0, (C,4); 2.0, (F,4);]

In order to play the score, we have to build a link between the logical time and the physical time. Let us define a signal clock which will be emitted every period seconds:

signal clock
let period = 0.01

The process emit_clock emits the signal clock. The function gettimeofday of the Unix module gives access to the physical time:

let process emit_clock =
  (* set the next deadline *)
  let next = ref (Unix.gettimeofday () +. period) in
  loop
    let current = Unix.gettimeofday () in
    (* check if the deadline has been reached *)
    if (current >= !next)
    then
      begin
        emit clock ();
        next := !next +. period; (* update the next deadline *)
      end;
    pause
  end

Now, to wait a duration of dur seconds, we only have to count the occurrences of the signal clock:

let process wait dur =
  let d = int_of_float (dur /. period) in
  for i = 1 to d do
    await clock
  done

A performance is represented by a signal on which notes to play are sent.

signal perf default [] gather (fun x y -> x::y)

The execution of a score is the traversal of the list of notes at the right speed.

let rec process play sequence =
  match sequence with
  | [] -> ()
  | ((dur, pitch) as note) :: s ->
      emit perf note;
      run (wait dur);
      run (play s)

In order to produce actual sounds, notes are converted into UDP messages and sent to an audio environment. This is done by the process sender. This process awaits notes produced on the signal perf and sends UDP messages.

let send_to_audio sock note =
  let m = Music.string_of_note note in
  Network.sendUDP m sock 7400 ()

let process sender =
  let sock = Network.init_client () in
  print_endline ("playing on port : "^(string_of_int 7400));
  loop
    await perf (notes) in
    List.iter (fun n -> send_to_audio sock n) notes
  end

To test our implementation, we can play the theme as an infinite repetition of the score:

let process theme =
  loop
    run (play jacques)
  end

Finally, the main process is the parallel composition of the metronome (the emit_clock process), the theme and the sender process:

  let process main =
  run emit_clock ||
  run theme ||
  run sender

Now to perform a round, we first define a process delayed which delays the execution of a process:

let process delayed p dur =
  run (wait dur);
  run p

Then, we can execute the theme four times in parallel, each time delayed by 8 seconds:

let process round_basic =
  run theme ||
  run (delayed theme 8.0) ||
  run (delayed theme 16.0) ||
  run (delayed theme 24.0)

Finally, we can redefine the round using parallel iterations or recursive processes:

let process round_for =
  for i = 0 to 3 dopar
    run (delayed theme (float (i * 8)))
  done

let rec process round nb_voices =
  if nb_voices <= 0 then ()
  else begin
    run theme ||
    run (delayed (round (nb_voices - 1)) 8.0)
  end

Here is the result of the round:

The complete implementation is available in the directory examples/reactive_asco of the ReactiveML distribution.