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.