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.