Simple MIDI Sequencer in 10 lines of code
You can download the complete code here.
The following example will demonstrate how to write an entire module, how to do some initialisation
and how to export some symbols while keeping some other private.
It will also explain time-related instruction.
The active code is indeed 10 lines only.
However, there is also a bit of plumbing code, i.e. imports, header and footer of the module.
Let's start with time.
What we need is the wait instruction.
As the name implies it instructs program to wait specified number of miliseconds.
Therefore [wait 1000] will suspend the program for 1 second.
We will also need to measure time.
Again we will need wait.
Yes, it also retrieves current time (in miliseconds) if you pass variable instead of actual time to wait.
Hence, [res [wait *t] [show *t]] will show you the current time.
To have our time management complete we will need to know the time difference between MIDI events.
Therefore we will need to store the time of the last MIDI event somewhere.
Whenever a new MIDI event arrives, we will be able to subtract the current time from the previous time, thus obtaining the time difference.
A traditional variable last_time will do.
However, be careful with traditional variables. You should observe the difference between assigning and retrieving values.
When you assign, you just pass a value as the only parameter.
When you retrieve, you use colon character and a typical Prolog variable.
Here is the code:
auto := [[var last_time]] ; means on the start-up create a global variable attached to the last_time symbol
...
... ; somewhere in the code
[wait *time] ; get the current time in *time variable
[last_time : *last] ; get the time of previous MIDI event
[sum *last *delta *time] ; calculate the *delta to get the time difference
[last_time *time] ; change the value of the previous time to current time (to be used with the next MIDI event
... ; rest of the code
...
Now we need to be able to open file, save into it some data and close it.
We will use file_writer instruction and a symbol file for it.
First, we will use file_writer to open a file for writing.
It takes two parameters: a symbol and a file name.
For example [file_writer file "sequence.txt"] will open a text file for writing.
Once we have the file opened, we can write into it as if the symbol attached to it was an write instruction.
For example: [file "Hello World!\n"] will write "Hello World!" and a new line into the file "sequence.txt".
We can close the file by using the symbol without any parameter, such as here: [file].
An important thing to remember is how lists can be written into a file.
They must be enclosed in an extra square bracket.
For example, to get [1 2 3 4] written into the file, you will need to write this in the code: [file [[1 2 3 4]]].
This slight complication comes from the fact that texts are written without quotation marks and numbers are written as ASCII characters.
Anyway, we will want to write into the file the MIDI event and the time elapsed from the previous MIDI event.
We already know the *delta. Let's say that our MIDI event is stored in *command variable.
Here is what we need to write:
[file [[wait *delta]] "\n" [*command]]
; please note extra new-line character and extra brackets around commands
Obviously, we want MIDI events stored into the file when they are detected by the system.
We will use the callback mechanism.
The income_midi instruction is called every time when a new MIDI event arrives.
The only thing we need is to provide its definition.
Look at the complete code below, it also shows where the MIDI event *command comes from.
[[income_midi : *command]
[wait *time] ; get the current time in *time variable
[last_time : *last] ; get the time of previous MIDI event
[sum *last *delta *time] ; calculate the *delta to get the time difference
[last_time *time] ; change the value of the previous time to current time (to be used with the next MIDI event
[file [[wait *delta]] "\n" [*command]]
]
One last thing to do is to implement instructions for starting and stoping the recoring.
We can define the record instruction for both purposes.
For example: [record "filename.txt"] would start the recording while [record] (without parameters) would stop it.
During the start, we need to open file and reset the time of the last event to the current time, i.e.:
[[record *file_name] [file_writer file *file_name] [wait *t] [last_time *t]]
During the stop, we need to write an [exit] command into the file, close it and prevent our callback from writing anything anywhere.
A good idea is to assign [] (empty list) to the last_time variable.
This will efectively cause sum to fail.
[[record] [file "\n\n" [[exit]] "\n"] [file] [last_time []]]
And this is it! 10 lines of code.
But how are we going to play-back what we recorded?
Isn't it what MIDI sequencer should do?
Here is a surprise.
We don't need to write any code for play-back.
The play-back code is INSIDE THE RECORDED FILE!
Open it a look at it. What do you see? A legitimate Prolog instructions.
You can play them using the batch command.
Therefore [batch "sequence.txt"] will play what we just recorded.
Moreover, [crack [batch "sequence.txt"]] will play it in a separate thread.
This is how you can play several recordings at the same time.
Of course, this example is very trivial.
Most likely you will want to add quantisation and perfect synchronisation to the project.
There is an internal conductor mechanism for this purpose (described elsewhere).
Anyway, to make the module complete, you will need to add some extra plumbing code.
Therefore, you will need imports:
import studio
program header with declarations of all the symbols used in the code
program sequencer [last_time file record]
most likely you will also need private section to indicate, which symbols are not going to be exported outside
private [last_time file]
... and program footer at the end, which also starts the interactive session command
end := [ [command] ] .
Summary:
You can download the entire example from here.
To start the program, open your terminal window and type: studio sequencer
You will need to set-up your MIDI interfaces, therefore type: [midi] to see what is available
and/or [midi 0 0] to select the default ones.
To start recording, type: [record "sequence.txt"]
To stop recording, just type: [record]
To play back whatever you recorded, use: [batch "sequence.txt"] or [crack [batch "sequence.txt"]] to play it in a separate thread.