Index
Strasheela
Init
GUtils
LUtils
MUtils
Score
SMapping
SDistro
Out
Strasheela
FD_edited
Midi
This functor defines means to output MIDI files. To this end, it makes use of csvmidi (see http://www.fourmilab.ch/webtools/midicsv/). A text file in midicsv file format is output which is transformed into a MIDI file by csvmidi.

The top-level definitions exported by this functor are procedures like OutputMidiFile, RenderMidiFile, PlayMidiFile, and RenderAndPlayMidiFile. Like Strasheela output into other formats (e.g., Csound or Lilypond), the output into MIDI files is primarily customised by clauses which define how certain score objects are transformed into MIDI events. This functor provides low-level functions for outputting virtually every MIDI event possible (e.g., MakeNoteOn, MakeNoteOff, MakeCC) and some higher level functions which simplify the definition of clauses (e.g., NoteToUserEvent, NoteToMidi, NoteToPitchbend). Further functionality is also provided. These include the class MidiNote, some Boolean functions (e.g., HasType, IsNoteOn, HasChannel), and conversion functions (e.g., BeatsToTicks, CentToPitchbend). Several examples demonstrate these procedures (e.g., in strasheela/examples/ the files ContinuousCOntrollersInScore-MidiOutput.oz and Microtonal-MIDI-examples.oz).

The documentation of the lower-level functions in this functor often quotes the documentation of csvmidi for the csv / MIDI event the function creates.

General typing information:

Time: an integer representing the absolute time in MIDI clocks
Channel: an integer in 0-15 (?)
Track: an integer identifying the track to which this record belongs. Tracks of MIDI data are numbered starting at 1. Track 0 is reserved for file header, information, and end of file records.
Text: an atom as 'my Text'


Information on internals

An intermediate format is used for the transformation process: on the Oz side, each record in the CSV representation is represented by an Oz record with label csv and the features track (an int), time (an int), and type (an atom). For exammple

csv(track:1 time:0 type:'Start_track')

Additionally, a record may have feature parameters with a list of type-specific parameters, as in the following example (the note on parameters are [Channel Note Velocity]).

csv(track:2 time:0 type:'Note_on_c' parameters:[1 79 81])

or this exammple (the controller parameters are [Channel ControlNum Value])

csv(track:2 time:0 type:'Control_c' parameters:[1 7 64])

The feature values track and time and integers (see above). Type is a virtual string corresponding to a type in the CSV file format specification (see the end of http://www.fourmilab.ch/webtools/midicsv/). The parameters are a list of values permitted in a virtual string and follow the CSV specification. For example, a title record has the format (note the explicit double quotes, according to the CSV spec).

csv(track:1 time:0 type:'Title_t' parameters:['\"This is my Title\"'])

An CSV score is represented internally by a list of these records.

See the CSV documentation (or the MidiOutput.oz source) for details on the various CSV types.




Functor

Import

Export

Define

proc{OutputMidiFile MyScore Spec}
Creates a MIDI file from MyScore as defined in Spec (see below). OutputMidiFile creates a CSV file which is like an event list (i.e. only a single track is supported) and this file is then transformed into a MIDI file by csvmidi.

Supported score format:

The info-tag 'timeshift', given to a temporal item, specifies a time shift function (a fenv). Example: timeshift(MyTimeshiftFenv). Time shift values are specified as time value offsets in the present timeUnit. For example, if a note has the start time 42 and its container specifies a time shift fenv with the y-value -1.0 corresponding to the start time of this note, then the MIDI note on happens at time 41. Hierarchical nesting of time shift functions is supported: if in the example above this note is recursively contained in other containers which also specify a time shift fenv, then their y-values for the note are added to the note's start time as well.
NOTE: Time shift fenvs also affect the timing of CC fenvs. However, the "spacing" (ccsPerSecond) of continuous controller events and tempo curves etc. are _not_ affected by timeshift fenvs, i.e. CC events remain evenly distributed. Timeshift curves etc only shift the start and end time of whole fenvs together with its event.

The user can control the transformation process by specifing transformation clauses. The format of such clauses is explain in the documentation of Out.scoreToEvents. In OutputMidiFile, the transformation function of each clause must return a list of MIDI events, which are created with functions like MakeNoteOn or MakeCC. This list can contain any MIDI events which correspond to the score object matching the clause (e.g., for a note, the returned MIDI events may include note-on and note-off events, pitchbend, aftertouch, and CC events etc.). However, if the argument removeQuestionableNoteoffs is true, then there must be at maximum a single noteOn event is present in this list and it is coupled with a corresponding noteOff event. See the documentation of ScoreToEvents_Midi for information on the meaning of removeQuestionableNoteoffs.

The default clauses are a transformation where only notes (either instances of Score.note or any subclasses such as MidiNote) are output. Clauses given with the 'clauses' argument are appended before the default clauses (so if you add a clause for MIDI notes, then the default clause still works for other notes).

The argument 'scoreToEventsArgs' expects a record of arguments for the procedure Out.scoreToEvents/ScoreToEvents_Midi called internally. This argument controls which score objects are considered at all for output (see the Out.scoreToEvents documentation for details).

Spec defaults to
unit(file:"test"
csvDir:{Init.getStrasheelaEnv defaultCSVDir}
midiDir:{Init.getStrasheelaEnv defaultMidiDir}
csvExtension:".csv"
midiExtension:".mid"
flags:{Init.getStrasheelaEnv defaultCSVFlags}
headerEvents:[local Track=2 in % fixed track
{MakeTempo Track 0
{BeatsPerMinuteToTempoNumber {FloatToInt {Init.getTempo}}}}
end]
clauses:nil
removeQuestionableNoteoffs: true
scoreToEventsArgs: unit)

DefaultClauses = [isNote#fun {$ MyNote} {NoteToMidi MyNote unit} end]

In this default setting, the variables BeatsToTicks, IsMidiNoteMixin, MakeNoteOn and MakeNoteOff are defined and exported by the present functor.


proc{PlayMidiFile Spec}
Plays a midi file with the player specified in {Init.getStrasheelaEnv midiPlayer} according Spec which defaults to
unit(file:"test"
midiDir:{Init.getStrasheelaEnv defaultMidiDir}
midiExtension:".mid"
flags:{Init.getStrasheelaEnv defaultMidiPlayerFlags})


proc{RenderAndPlayMidiFile MyScore Spec}
Outputs MyScore into a MIDI file and starts the MIDI file player with this file. See OutputMidiFile and PlayMidiFile for documentation of the arguments expected by Spec.

Note that Fenv.renderAndPlayMidiFile extends this definition by support for continuous controllers defined in the score etc.


fun{MakeCSVScore Events}
Expects a list of CSV records (e.g., each created by one of the low-level MIDI event creators such as MakeNoteOn etc.) and returns a full CSVScore in the internal CSV format described above.
MakeCSVScore sorts the input events first by the track number, then by time. Also, it sorrounds all tracks by track start and end events, and surrounds the full score by a file header and end of file event.
NB: In Events, the track number of all events must be >= 1. This means, the direct result of an import from a CSV file can not be used, because it already includes this header events.


fun{ScoreToEvents_Midi MyScore Specs Args}
Variant of Out.scoreToEvents which deletes questionable note off events. Transformation clauses in Specs must return a list of MIDI events.
In principle, multiple notes of the same channel and pitch can overlap in a Midi file. However, there is only a single note ressource and the second note will take over this ressource. What is more problematic, however, is the fact that the first note off event will turn off the note regardless whether the first of the second note was actually longer. ScoreToEvents_Midi avoids this problem by filtering out any note off event which would switch off a note too early. Nevertheless, this function can not change the fact that Midi provides only a single ressource per channel and pitch -- the 'taking over' of this ressource (e.g. restarting of the envelope) by overlapping notes can not be avoided.
Note that removing note off events can lead to problems when displaying the resulting files in a sequencer. A better approach might be to very slightly move note off events forward using the NoteToMidi argument noteOffOffset.


proc{OutputCSVScore Events Spec}
Outputs a CSV file. OutputCSVScore expects Events, a list of CSV records as expected by MakeCSVScore, and a specification of the output file which has the following defaults.
unit(file:"test"
csvDir:{Init.getStrasheelaEnv defaultCSVDir}
csvExtension:".csv")


proc{OutputCSVScore2 Events Spec}
Outputs a CSV file. OutputCSVScore2 expects Events, a list of CSV records representing a full CSV score, including file and track header and end events. OutputCSVScore2 differs from OutputCSVScore in that OutputCSVScore adds those track header and end events. Spec has the following defaults.
unit(file:"test"
csvDir:{Init.getStrasheelaEnv defaultCSVDir}
csvExtension:".csv")


fun{CSVScoreToVS Events}
Expects a full CSVScore in the internal CSV format described above (e.g. as created by MakeCSVScore), and transforms it into a score in the textual CSV described by http://www.fourmilab.ch/webtools/midicsv/ (a VS).


proc{RenderMidiFile Spec}
Transforms a CSV file into a Midi file (by calling midicsv). The Spec defaults are the following.
unit(file:"test"
csvDir:{Init.getStrasheelaEnv defaultCSVDir}
midiDir:{Init.getStrasheelaEnv defaultMidiDir}
csvExtension:".csv"
midiExtension:".mid"
csvmidi:{Init.getStrasheelaEnv csvmidi}
!!?? is flags control needed?
flags:{Init.getStrasheelaEnv defaultCSVFlags})


fun{MakeComment VS}
Expects a VS and returns a CSV comment, ending with a newline (a VS).


fun{MakeTitle Track Time Text}
Function returns a CSV event spec. The Text (an atom) specifies the title of the track or sequence. The first Title meta-event in a type 0 MIDI file, or in the first track of a type 1 file gives the name of the work. Subsequent Title meta-events in other tracks give the names of those tracks.


fun{MakeCopyright Track Time Text}
Function returns a CSV event spec. The Text specifies copyright information for the sequence. This is usually placed at time 0 of the first track in the sequence.


fun{MakeInstrumentName Track Time Text}
Function returns a CSV event spec. The Text names the instrument intended to play the contents of this track, This is usually placed at time 0 of the track. Note that this meta-event is simply a description; MIDI synthesisers are not required (and rarely if ever) respond to it. This meta-event is particularly useful in sequences prepared for synthesisers which do not conform to the General MIDI patch set, as it documents the intended instrument for the track when the sequence is used on a synthesiser with a different patch set.


fun{MakeMarker Track Time Text}
Function returns a CSV event spec. The Text marks a point in the sequence which occurs at the given Time, for example '"Third Movement"'.


fun{MakeCuePoint Track Time Text}
Function returns a CSV event spec. The Text identifies synchronisation point which occurs at the specified Time, for example, "Door slams".


fun{MakeLyric Track Time Text}
Function returns a CSV event spec. The Text gives a lyric intended to be sung at the given Time. Lyrics are often broken down into separate syllables to time-align them more precisely with the sequence.


fun{MakeText Track Time Text}
Function returns a CSV event spec. This meta-event supplies an arbitrary Text string tagged to its Track at Time. It can be used for textual information which doesn't fall into one of the more specific categories given above.


fun{MakeSequenceNumber Track Number}
Function returns a CSV event spec. This meta-event specifies a sequence Number between 0 and 65535, used to arrange multiple tracks in a type 2 MIDI file, or to identify the sequence in which a collection of type 0 or 1 MIDI files should be played.
The SequenceNumber meta-event should occur at the start of the track (at Time zero, implicitly set).


fun{MakeMidiPort Track Time Number}
Function returns a CSV event spec. This meta-event specifies that subsequent events in the Track should be sent to MIDI port (bus) Number, between 0 and 255. This meta-event usually appears at the start of a track with Time zero, but may appear within a track should the need arise to change the port while the track is being played.


fun{MakeChannelPrefix Track Time Number}
Function returns a CSV event spec. This meta-event specifies the MIDI channel that subsequent meta-events and system exclusive events pertain to. The channel Number specifies a MIDI channel from 0 to 15. In fact, the Number may be as large as 255, but the consequences of specifying a channel number greater than 15 are undefined.


fun{MakeTimeSignature Track Time Num Denom Click NotesQ}
Function returns a CSV event spec. The time signature (Num/Denom), metronome click rate, and number of 32nd notes per MIDI quarter note (24 MIDI clock times) are given by the numeric arguments. Num gives the numerator of the time signature as specified on sheet music. Denom specifies the denominator as a negative power of two, for example 2 for a quarter note, 3 for an eighth note, etc. Click gives the number of MIDI clocks per metronome click, and NotesQ the number of 32nd notes in the nominal MIDI quarter note time of 24 clocks (8 for the default MIDI quarter note definition).


fun{MakeKeySignature Track Time Key MajorOrMinor}
Function returns a CSV event spec. The key signature is specified by the numeric Key value, which is 0 for the key of C, a positive value for each sharp above C, or a negative value for each flat below C, thus in the inclusive range -7 to 7. The MajorOrMinor argument is an atom which will be major for a major key and minor for a minor key.


fun{MakeTempo Track Time Number}
Function returns a CSV event spec. The tempo is specified as the Number of microseconds per quarter note, between 1 and 16777215. A value of 500000 corresponds to 120 quarter notes ("beats") per minute. To convert beats per minute to a Tempo value, take the quotient from dividing 60,000,000 by the beats per minute.


fun{BeatsPerMinuteToTempoNumber BeatsPerMinute}
fun{MakeSMPTEOffset Track Hour Minute Second Frame FracFrame}
Function returns a CSV event spec. This meta-event, which must occur at the start of a track (with a zero Time, implicitly set), specifies the SMPTE time code at which it should start playing. The FracFrame field gives the fractional frame time (0 to 99).


fun{MakeSequencerSpecific Track Time Parameters}
Function returns a CSV event spec. The SequencerSpecific meta-event is used to store vendor-proprietary data in a MIDI file. Parameters is a list of the form [Length Data ...].
The Length can be any value between 0 and (2^28)-1, specifying the number of Data bytes (between 0 and 255) which follow. Sequencer_specific records may be very long; programs which process MIDI CSV files should be careful to protect against buffer overflows and truncation of these records.


fun{MakeUnknownMetaEvent Track Time Parameters}
Function returns a CSV event spec. If midicsv encounters a meta-event with a code not defined by the standard MIDI file specification, it outputs an unknown meta-event. Parameters is a list of the form [Type Length Data ...].
Type gives the numeric meta-event type code, Length the number of data bytes in the meta-event, which can be any value between 0 and 228-1, followed by the Data bytes. Since meta-events include their own length, it is possible to parse them even if their type and meaning are unknown. csvmidi will reconstruct unknown meta-events with the same type code and content as in the original MIDI file.


fun{MakeNoteOn Track Time Channel Note Velocity}
Function returns a CSV event spec. Creates an event at Time (an int in MIDI clocks) to play the specified Note (an integer in range 0-127) on the given Channel (an int in 0-15 ?) with Velocity (an int in 0-127).
A note on event with velocity zero is equivalent to a note off.


fun{MakeNoteOff Track Time Channel Note Velocity}
Function returns a CSV event spec. Creates an event at Time to stop playing the specified Note on the given Channel. The Velocity should be zero, but you never know what you'll find in a MIDI file.


fun{MakePitchBend Track Time Channel Value}
Function returns a CSV event spec. The pitch bend Value is a 14 bit unsigned integer and hence must be in the inclusive range from 0 to 16383.
NB: there was is a bug in a former in csvmidi, where pitchbend values must be in [0, 127].


fun{MakeCC Track Time Channel ControlNum Value}
Function returns a CSV event spec. Set the controller ControlNum (an int in 0-127) on the given Channel to the specified Value (an int in 0-127). The assignment of ControlNum values to effects differs from instrument to instrument. The General MIDI specification defines the meaning of controllers 1 (modulation), 7 (volume), 10 (pan), 11 (expression), and 64 (sustain), but not all instruments and patches respond to these controllers. Instruments which support those capabilities usually assign reverberation to controller 91 and chorus to controller 93.


fun{MakeProgramChange Track Time Channel ProgramNum}
Function returns a CSV event spec. Switch the specified Channel (0-15) to program (patch) ProgramNum (0-127). The program or patch selects which instrument and associated settings that channel will emulate. The General MIDI specification provides a standard set of instruments, but synthesisers are free to implement other sets of instruments and many permit the user to create custom patches and assign them to program numbers.
Apparently due to instrument manufacturers' skepticism about musicians' ability to cope with the number zero, many instruments number patches from 1 to 128 rather than the 0 to 127 used within MIDI files. When interpreting ProgramNum values, note that they may be one less than the patch numbers given in an instrument's documentation.


fun{MakeChannelAftertouch Track Time Channel Value}
Function returns a CSV event spec. When a key is held down after being pressed, some synthesisers send the pressure, repeatedly if it varies, until the key is released, but do not distinguish pressure on different keys played simultaneously and held down. This is referred to as ``monophonic'' or ``channel'' aftertouch (the latter indicating it applies to the Channel as a whole, not individual note numbers on that channel). The pressure Value (0 to 127) is typically taken to apply to the last note played, but instruments are not guaranteed to behave in this manner.


fun{MakePolyAftertouch Track Time Channel Note Value}
Function returns a CSV event spec. Polyphonic synthesisers (those capable of playing multiple notes simultaneously on a single channel), often provide independent aftertouch for each note. This event specifies the aftertouch pressure Value (0 to 127) for the specified Note on the given Channel.


fun{MakeSystemExclusive Track Time Parameters}
Function returns a CSV event spec. System Exclusive events permit storing vendor-specific information to be transmitted to that vendor's products. Parameters is a list of the form [Length Data ...].
The Length bytes of Data (0 to 255) are sent at the specified Time to the MIDI channel defined by the most recent Channel_prefix event on the Track, as a System Exclusive message. Note that Length can be any value between 0 and 228-1. Programs which process MIDI CSV files should be careful to protect against buffer overflows and truncation of these records.


fun{MakeSystemExclusivePacket Track Time Parameters}
Function returns a CSV event spec. System Exclusive events permit storing vendor-specific information to be transmitted to that vendor's products. Parameters is a list of the form [Length Data ...].
The Length bytes of Data (0 to 255) are sent at the specified Time to the MIDI channel defined by the most recent Channel_prefix event on the Track. The Data bytes are simply blasted out to the MIDI bus without any prefix. This message is used by MIDI devices which break up long system exclusive message into small packets, spaced out in time to avoid overdriving their modest microcontrollers. Note that Length can be any value between 0 and 228-1. Programs which process MIDI CSV files should be careful to protect against buffer overflows and truncation of these records.


fun{IsCSVEvent X}
Returns true if X is a CSV event.


fun{HasType X Type}
Returns true if X is an CSV event of the given Type (an atom). X must be a CSV event.


fun{IsNoteOn X}
Returns true if X is an event of type 'Note_on_c'.


fun{IsNoteOff X}
Returns true if X is an event of type 'Note_off_c'.


fun{HasChannel X}
Returns true if events of the type X provides a channel as parameter.


fun{BeatsToTicks Beats}
Transforms a temporal value in beats (int or float) into the equivalent in MIDI ticks (int). The division is set by SetDivision.


fun{TicksToBeats Ticks}
Transforms a temporal value in MIDI ticks (int or float) into the equivalent beats (int). The division is set by SetDivision.


fun{CentToPitchbend Cent Resolution}
Expects a cent value denoting a de-tuning amount (a float) and a pitch bend resolution measured in ET semitones (an int, usually in {1, ..., 12}) and returns the corresponding pitchbend value (for cent values in {~100.0, ..., 100.0} an int in {0, .., 16383}). The resolution is specified for both up and down transpositions. For example, if it is set to 2 it corresponds to the default standard pitch bend range of -2..2 semitones (4096 steps/100 cents).


[class info]

Defines a mixin to extend the Score.note class (or any of its subclasses) to a full MIDI note class.
MidiNoteMixin defines the additional attribute channel.

class MidiNoteMixin
   feat label !MidiNoteMixinType end

fun{IsMidiNoteMixin X}

[class info]

Extends Score.note by the additional attribute channel.

class MidiNote from Score.note MidiNoteMixin
   feat label end

fun{NoteToMidi MyNote Args}
fun{NoteToPitchbend MyNote Args}
Expects a note object and returns a pitch class event to tune the MIDI output of MyNote correctly. The pitch bend resolution is set via the optional argument in semitones. Its default value 2 corresponds to the standard pitch bend range of -2..2 semitones, i.e., 4096 steps/100 cents.
For simplicity, NoteToPitchbend always creates pitchbend values which tune a pitch up. Hence, for a correct tuning the note event pitch must be always rounded down the MIDI pitch of MyNote. NoteToPitchbend does this and can thus be directly combined with NoteToMidi.
See NoteToUserEvent for further arguments and their meaning.


fun{NoteToUserEvent MyNote Fn Args}
Simplifies the transformation of a note object MyNote to MIDI events (avoids some code doubling). Fn is a function {Fn Track Start Channel} -- NoteToUserEvent provides these arguments Track Start Channel. Optional Args are track, and channel. If MyNote is a midi note (inherits from MidiNoteMixin), then the channel is taken from MyNote. If a channel is given as argument to NoteToUserEvent, then this channel is used regardless whether MyNote is a midi note or not (i.e. the channel of a MIDI note is overwritten). Otherwise the default channel 0 is used.
The defaults are
unit(track:2
channel:nil)



proc{SetDivision X}
Division (int) is the number of clock pulses per quarter note and is set in the MIDI file header. It defaults to 480.
NB: I do not know whether there exists a maximum value for this (e.g. 960 seems not to work properly: it reduces notes of 1 beat to 0 beat!).


fun{GetChannel X}
Expects a note object and returns a pitch class event to tune the MIDI output of MyNote correctly. Optional args are track, channel (only used when MyNote does not inherit from MidiNoteMixin), and resolution (its default 2 corresponds to the standard pitch bend range of -2..2 semitones, i.e., 4096 steps/100 cents). The defaults are
unit(track:2
channel:nil
noteOffVelocity:0)
Note for a correct tuning the note event pitch must be always round down the MIDI pitch of MyNote. NoteToPitchbend always creats pitchbend values which tune a pitch up.
NoteToPitchbend can be directly combined with NoteToMidi.
*/
fun {NoteToPitchbend MyNote Args}
Defaults = unit(track:2
channel:nil
resolution:2)
As = {Adjoin Defaults Args}
DefaultMidiChan = 0
Start = {BeatsToTicks {MyNote getStartTimeInSeconds($)}}
Pitch = {MyNote getPitchInMidi($)} % float
CentDeviation = (Pitch - {Floor Pitch}) * 100.0
PitchBend = {CentToPitchbend CentDeviation As.resolution}
Channel = if As.channel \= nil
then As.channel
elseif {IsMidiNoteMixin MyNote}
then {MyNote getChannel($)}
else DefaultMidiChan
end
in
{MakePitchBend As.track Start Channel PitchBend}
end


/** %% Returns the MIDI channel (an integer) for the temporal item X. For a midi note, the channel is defined by its respective parameter. Otherwise, the channel is defined as an info tag of the form channel(Chan) either in X or in some temporal container of X. If no channel definition is found, then 0 is returned (i.e. 0 is the default MIDI channel).


End