Playing an item: how MakeSound() works

This page is for programmers who want to read, maintain or extend the code that turns a timed item into sound: real-time MIDI, a MIDI file, a Csound score, an event list, and the pianoroll that displays it. All of this is done by one function, MakeSound(), in the console's source file MakeSound.c. The page describes its algorithm step by step.

The code lives in three C files of the console (source/BP3/):

  • MakeSound.c contains MakeSound() and its subroutines;
  • Graphic.c contains PreparePianoroll(), which computes the layout of the pianoroll before the item is played;
  • -BP3.h defines the PianoRoll structure shared by these two files.

Comments of the form // note 12 have been placed in these files. Each one points to the numbered note of this page that explains the code below it. Searching for // note 12 finds every place that note 12 describes. Line numbers are not given because they change with every revision.

Where MakeSound() stands in the production of music

A grammar produces an item: a string of terminal symbols, notes, performance controls and polymetric structures such as {2, C4 D4 E4, F3 G3}. Playing this item takes four steps, all called from PlayBuffer1() in PlayThings.c:

  1. PolyMake() (Polymetric.c) expands the polymetric structure into a regular grid called the phase diagram.
  2. FillPhaseDiagram() places every sound-object, note and control on this grid, and SetTimeObjects() (TimeSet.c) computes the date at which each one starts and ends. Constraints such as overlapping, truncation, pivots and cyclic objects are resolved here.
  3. MakeSound() reads the list of timed objects and sends their events, in chronological order, to the selected outputs.
  4. DrawItem() draws the graph of sound-objects, if requested.

When MakeSound() starts, therefore, all decisions about when things happen have been taken. Its task is to merge the events of all objects into a single chronological stream, to compute the MIDI or Csound values of each event (key, velocity, channel, controls), and to keep track of which keys are sounding so that every NoteOn gets its NoteOff.

The data received by MakeSound()

The main input is the global table p_Instance. Each entry (*p_Instance)[k], for k = 2 to *p_kmax, is a sound-object instance: one occurrence of an object in the item. (Indices 0 and 1 are not used for instances.) Its main fields are:

  • object, called j in the code: what is played. Its value tells what kind of object it is: 
    • j == 0: no object; the instance is skipped.
    • 1 < j < Jbol: a sound-object, whose prototype is defined in a -so file. (Terminal 0 is the prolongation _ and terminal 1 is the silence -.) Its events are in (*pp_MIDIcode)[j], a list of MIDI bytes, and possibly in (*pp_CsoundScore)[j], a list of Csound score lines.
    • Jbol ≤ j < 16384: a time pattern, which marks time but produces no sound.
    • j ≥ 16384: a simple note such as C4, whose MIDI key is j - 16384.
    • j < 0: an out-time object (written <<…>>), which has no duration. The code works with -j and sets outtime.
  • starttime, endtime: dates in milliseconds computed by SetTimeObjects().
  • truncbeg, truncend: durations cut at the beginning and at the end of the object, when constraints did not allow it to be played entirely.
  • alpha, beta: dilation ratios. alpha is the ratio between the duration of the object in the item and the duration of its prototype. beta is the ratio applied to the dates of its events. They are equal unless the object is cyclic: a cyclic object stretched by 3 keeps beta = 1 and repeats its periodical part instead (note 4).
  • nseq: the sequence (line of the phase diagram) on which the object is placed. Performance parameters are stored per sequence.
  • channel, velocity, transposition, scale, blockkey, instrument: the values of the performance controls (_chan(), _vel(), _transpose(), _scale(), _ins()…) in force when the object starts.
  • contparameters: the values of continuous parameters (pitchbend, volume, modulation, pressure, panoramic, and Csound parameters), with their start and end values and their mode: fixed, stepwise or continuous.
  • map0, map1, mapmode: key mapping (_keymap()), possibly varying from map0 to map1 during the object.

The other arguments of MakeSound() are tmin and tmax, the limits of the item in time, maxnsequences, the number of sequences, and imaxstreak and p_delta, which are only used to draw the background of the pianoroll.

Overview of the algorithm

Each instance is a stream of events ordered by date. MakeSound() merges these streams, together with the streams of messages generated by continuous controls, into one chronological stream. It does so without sorting the events: it keeps for each instance k the index (*p_inext)[k] of its next event and the date (*p_nextd)[k] of this event, and repeatedly:

  1. picks the instance whose next event comes first, at date t2;
  2. sends the events of this instance that are due at t2 (none of them can be later than the next event of another stream);
  3. saves the position reached in this instance, and goes back to step 1.

This is a k-way merge. Finding the next instance scans all instances, so the cost is proportional to the number of events times the number of instances. It does not require building and sorting a list of all events, and it allows an item to be played while it is computed in real time.

Four dates, all in milliseconds, drive this loop:

  • t0: the origin of the current performance on the output clock;
  • t1: the date of the current event, relative to t0;
  • t2: the date of the next event in any other stream, relative to t0;
  • t3: the date at which the current instance ends, minus its post-roll.

Every event is sent at date t0 + t1. The global Tcurr holds the same date in units of Time_res milliseconds. It is read by the MIDI driver, by scripts and by the functions that write files, so the code updates it whenever t1 changes.

The steps of MakeSound() are:

  1. Preparation: choose the outputs, allocate the working tables, scan the instances to find the first one and the first event of each (notes 1–4).
  2. Pianoroll: compute its limits in time and pitch, and draw its background (note 5).
  3. Outputs: open the Csound score and the MIDI file, tune the MIDI device (notes 6–7).
  4. Performance loop, repeated Nplay times: set the time origin (notes 8–10), then run the scheduler (notes 11–36).
  5. End of the item: release the keys still sounding, append the final silence, close the Csound score, free the memory (notes 37–40).

Notes 41 to 51 describe the subroutines.

Step 1 — Preparation

Note 1 — Choosing the outputs

The function returns at once if a performance is already running (SoundOn) or if the user has pressed PANIC. Without a trace path (--traceout), no graphics can be saved, so they are switched off.

Two local flags decide where the events go:

  • cswrite is TRUE if a Csound score is written. This happens when Csound output is selected and the item is not played in real time (or FileWriteMode is NOW), or when a MIDI file is converted to Csound. In the rest of the code, cswrite selects the Csound branch of every decision: no MIDI event is sent, and CscoreWrite() (CsoundScoreMake.c) writes score lines instead.
  • The global MIDIfileOn is TRUE if a MIDI file is written, under the same condition.

Real-time MIDI is rtMIDI, the event list is EventListOn, and the pianoroll is showpianoroll. The pianoroll is not drawn during a MIDI capture (ItemCapture) or when converting MIDI to Csound. If no output is selected, nothing is done. When several items are produced, ItemNumber counts them, and production stops after MaxItemsProduce items.

The pianoroll is drawn while the item is played, in the same pass: each note is drawn when its NoteOff is sent. It does not require a separate traversal of the events.

Note 2 — Working tables

All tables are Handles (see ConsoleMemory.c), allocated at the start and freed at the end (note 40).

  • Per instance (size MaxObjects):
    • p_inext: index of the next event to send; p_inext1: index of the first event, restored at each performance;
    • p_nextd: date of the next event; Infpos means that the instance is over;
    • p_onoff: TRUE once the instance has started (note 16);
    • p_silence: TRUE when the NoteOns of the instance must be silenced because its end is truncated (note 34);
    • p_istartperiod, p_iendperiod, p_periodgap, p_icycle: the periodical part of cyclic objects (notes 4 and 32).
  • Per MIDI channel and key (16 × 128): 
    • p_keychannel[ch][key]: the number of NoteOns of this key not yet followed by a NoteOff. Several objects can play the same key at the same time, so this is a counter, not a flag;
    • p_last_timeon[ch][key]: the date of the last NoteOn of this key, used to draw the note on the pianoroll and to detect NoteOns that are too close (note 28).
  • Per Csound instrument and key: p_keyinstrument[ins][key], the same counter as p_keychannel, used when cswrite is TRUE.
  • Per channel and continuous parameter: p_active, p_t2cont and p_seqcont describe the continuous controls being sent (note 19).
  • Per sequence:
    • pp_currentparams[nseq]: the performance parameters used by CscoreWrite() (note 17);
    • p_control[nseq]: the streams of continuous MIDI controls (note 19).
  • p_currmapped: the keys changed by key mapping, so that a NoteOff finds the key of its NoteOn (note 51).

Note 3 — Scanning the instances

A first loop over all instances prepares the scheduler. For each instance it:

  • finds the number of events of the object, im, with Find_im(). This is the number of MIDI bytes of the prototype, or the number of lines of its Csound score if that score is played (note 49). A time pattern has no events.
  • sets beta = alpha if the object is played less than twice and its prototype is not of fixed scale. Only a cyclic object keeps a beta different from alpha.
  • finds the first event of the object, (*p_inext1)[k]. The dates of events are obtained by adding the delays between consecutive events, multiplied by beta (EventDelay(), note 46).
  • finds the instance that starts first, kfirstinstance, at date t11, and the start date of the next one, t22. A negative pre-roll moves the start of an object earlier.

The scheduler starts with instance kfirstinstance, t1 = t11 and t2 = t22. As soon as this instance starts, t2 is computed again from all instances (note 16).

Note 4 — Cyclic objects

A sound-object prototype can be declared cyclic: when it must last longer than its prototype (alpha > 1), its events are not stretched; instead, the part that follows cyclicafter is repeated. GetPeriod() (SoundObjects2.c) returns cyclicafter, the date at which the periodical part starts, and objectperiod, its duration.

The scan stores:

  • (*p_istartperiod)[k]: the first event at or after cyclicafter;
  • (*p_iendperiod)[k]: the last event of the first cycle, extended to the following events that are simultaneous with it;
  • (*p_periodgap)[k]: the time between the start of a period and its first event, plus the time between its last event and the end of the cycle;
  • time_nextperiod_start: the date at which the second cycle starts.

For an object that is not cyclic, istartperiod remains -1. Note 32 explains how the scheduler uses these values.

Step 2 — The pianoroll (Graphic.c)

Note 5 — Limits in time and pitch

The pianoroll is an image in which each note is a horizontal bar: its position is its date, its height is its key. Before the first note can be drawn, the size of the image must be known. PreparePianoroll() computes it:

  • The end of the item, tmax, is the largest end date of all instances, increased by the post-roll of sound-objects and by the opposite of a negative pivot. The image covers Nplay repetitions of the item.
  • The range of keys, minkey to maxkey, includes the key of every simple note and the key of every NoteOn in the MIDI codes of sound-objects, after transposition, expansion and key mapping (note 51). For a sound-object whose Csound score is played, the pitch of each score line is converted to a key by CsoundPitchToKey(), according to the pitch format of its instrument: octave point pitch-class (OPPC), octave point decimal (OPD) or frequency in Hertz (CPS). The range is then widened to multiples of 6.
  • The image starts at date 0, or earlier if a pivot or a pre-roll creates negative dates. leftoffset is the x position of date tmin, topoffset the y position of the top of the note scale, and hrect the height of one key (3 pixels). The width is limited to 32000 pixels.

These values are returned in a PianoRoll structure (roll in MakeSound()), which is passed to every function that draws a note. If ShowGraphic is set, PreparePianoroll() also draws the background (DrawItemBackground()) and the note scale (DrawNoteScale()). The field noteoffset is the x position from which notes are drawn. It is set by MakeSound() for each performance (note 10).

Step 3 — Opening the outputs

Note 6 — Preparing the outputs

  • In real time, the clock of the driver is read. When improvising, the time taken to compute the item is recorded in MaxComputeTime if it is the longest so far.
  • If the diapason A4freq is not 440 Hz, TuneMIDIdevice() sends the A4 tuning to all channels of the MIDI device (note 44).
  • For Csound, PrepareCsFile() opens the score. For a MIDI file, PrepareMIDIFile() (MIDIfiles.c) writes its header. In both cases Nplay is set to 1: repetitions are only meaningful in real time.

Note 7 — Resetting the performance parameters

For each sequence, the Csound performance parameters are reset: no key is sounding (level[key] = 0), there is no transposition, and no parameter is active. paramsinstance, the instance whose parameters are currently stored for this sequence, is set to 0 (note 15). All streams of continuous controls are marked inactive (ibm = -1).

Step 4 — The performance loop

Note 8 — Repeating the item

The item is played Nplay times, or forever if SynchroSignal is PLAYFOREVER. Each repetition is called an occurrence. stop() (ConsoleMain.c) is called at the start of each occurrence and before each event: it checks whether the user has clicked STOP or PANIC in the interface, which creates a file in temp_bolprocessor/.

Note 9 — Resetting the instances, truncated beginnings

At the start of each occurrence, every instance is reset: its next event is its first event (p_inext1), it has not started (p_onoff FALSE), and its next date is its start date.

If the beginning of an instance is truncated (truncbeg > 0), the events before the truncation are not played. FirstEventAfterTruncation() (note 47) finds the first NoteOn at or after the truncation, and the time this_gap between them. Starting at a NoteOn avoids sending NoteOffs or controls of notes that were cut. The next date of the instance is then its start date plus this_gap.

All keys are released in the tables of note 2, all switches (pedals) are off, and no continuous control is active.

Note 10 — The time origin

t0 is the date, on the clock of the output, at which date 0 of the item will be played. It depends on the situation:

  • Normally, currenttime = Tcurr × Time_res, the current date of the output. In real time and in MIDI files, MIDIsetUpTime is added once, to give the MIDI device time to receive the first messages.
  • If the item is played as a continuous flow (CyclicPlay) and the driver is already late, the item starts at the current time of the driver.
  • When an item is played line by line in chunks, each chunk starts at the end of the previous one, ChunkEndTime.
  • If t1 is negative (an object starts before date 0 because of a pivot or a pre-roll), t0 is moved later by -t1, so that no event has a negative date.
  • When several items follow each other (improvisation, all items, chunks), t0 is the end of the previous item, LastTime. PianorollShift records this offset so that the pianoroll of each item starts at its own origin.

The pianoroll's noteoffset is set here: it is leftoffset, shifted back if all dates were shifted by -t1.

ChunkEndTime and, in real time, ExpectedEndTime are computed at the first occurrence. ExpectedEndTime is the date at which the item will end, displayed by the clock of produce.php.

For Csound, WriteCsoundTempo() writes the tempo statement at the start of the score (note 45).

Note 11 — The scheduler

The loop while(kcurrentinstance > 0) is the heart of MakeSound(). Each iteration works on one instance, kcurrentinstance:

  1. read the context of the instance (notes 12–15);
  2. if the instance has not started yet, start it: send its initial controls, script and switches (notes 16–20);
  3. send its events while t1 ≤ t2 and t1 ≤ t3 (notes 21–33);
  4. record where it stopped, or that it is over (note 34);
  5. send a message of a continuous control if one is due (note 35);
  6. find the next instance or control (note 36).

The loop ends when NextInstance() returns 0: no instance has an event left.

Here is an example with C4 {D4, E4 F4} at 60 beats per minute. Instance 2 is C4 (0–1000 ms), 3 is D4 (1000–2000), 4 is E4 (1000–1500), 5 is F4 (1500–2000). The next dates of the instances are 0, 1000, 1000 and 1500.

  1. Instance 2 starts, and t2 becomes 0, its own date. It sends NoteOn C4 at 0. Its next event, the NoteOff at 1000, is later than t2, so it is suspended with next date 1000.
  2. Instances 2, 3 and 4 now have the earliest date, 1000. The first one in the table is chosen: instance 2 sends NoteOff C4 at 1000 and is over.
  3. Instance 3 sends NoteOn D4 at 1000 and is suspended until 2000. Instance 4 sends NoteOn E4 at 1000 and is suspended until 1500.
  4. At 1500, instance 4 comes before instance 5: it sends NoteOff E4, then instance 5 sends NoteOn F4.
  5. At 2000, instance 3 sends NoteOff D4, then instance 5 sends NoteOff F4.

At equal dates, instances are taken in the order of the table, which is the order of the item. A note that ends is therefore released before the next note of the same sequence starts, as at step 4.

Note 12 — Context of the current instance

Each time the scheduler comes back to an instance, it reads its parameters: object j, scale, block key, channel (converted by ChannelConvert(), note 49), sequence nseq, dilation ratios. A negative j indicates an out-time object (see “The data received by MakeSound()”).

Note 13 — Number of events

The events of an instance are indexed by ievent, from 0 to im - 1:

  • for a sound-object whose MIDI codes are played, ievent is the index of a byte in (*pp_MIDIcode)[j]. A NoteOn takes three entries (status, key, velocity). Each entry has a time field, the delay in milliseconds since the previous entry, before dilation;
  • for a sound-object whose Csound score is played, ievent is the index of a line of the score, (*pp_CsoundScore)[j];
  • a simple note has im = 6: two “virtual” messages of three bytes, a NoteOn at ievent = 0 and a NoteOff at ievent = 3 (note 33).

The pre-roll and post-roll of the prototype (ObjectPreRoll(), ObjectPostRoll(), note 46), its cyclic period (note 4), and its strike again setting (note 28) are also read here.

Note 14 — End date and duration

The instance must stop at t3, its end date minus the post-roll of its prototype. objectstarttime is its start date minus the truncated part, and earlier by a negative pre-roll. objectduration = t3 - objectstarttime is used to spread continuous controls and key mappings over the object. A negative or unreasonably long duration (over 500 seconds) indicates an error in the time-setting. It is reported, and the duration is set to 0.

Note 15 — Restoring the Csound parameters of a resumed object

The Csound parameters (note 17) are stored per sequence, not per instance. If an object is interrupted by the start of another object on the same sequence, its parameters are replaced. When the scheduler comes back to the first object, its own parameters are therefore copied back with SetObjectParameters(). paramsinstance records which instance the parameters of the sequence belong to, so the copy is only made when it has changed. Example: in _volumecont _volume(30) a C4 … _volume(127), if sound-object a lasts longer than its time slot, its events after the start of C4 keep the volume variation of a.

Note 16 — Starting an instance

The first time the scheduler comes to an instance, (*p_onoff)[k] is FALSE. The instance is then started:

  • it is added to the event list if one is written (AddEventToList());
  • its MIDI channel is computed by MIDIchannel() (note 49);
  • unless it is out-time, SendInitialControls() sends its volume, panoramic, pitchbend, pressure and modulation on this channel if they have changed (note 42);
  • t2 is computed again, since the start of this instance may change which stream comes next (NextInstance() and NextControlDate(), note 48).

Note 17 — Csound parameters

The parameters of the instance (contparameters) are copied to those of its sequence (pp_currentparams[nseq]->params) by SetObjectParameters(). CscoreWrite() reads them to compute the parameter fields of each score line: for a continuous variation, it interpolates between startvalue and endvalue (or along a table of points) according to the date of the event within the object.

The table of a sequence is created with only the five MIDI parameters (IPANORAMIC + 1), and enlarged here to MaxParameters + IPANORAMIC + 1 entries, adding the parameters declared in the Csound instruments. The table of an instance always has this size, since SetObjectParams() (SetObjectFeatures.c) creates it that way. A different size is reported as an error.

SetObjectParameters() marks a parameter inactive in these cases, so that CscoreWrite() uses its default value:

  • volume or panoramic, when the prototype does not accept them;
  • an additional parameter without a value;
  • a continuous MIDI parameter set on another channel than the object's.

Note 18 — Scripts

An object can carry script commands, such as _script(wait for C3 channel 1) or _script(MIDI send Start). ExecuteScriptList() executes them in order when the object starts, through ExecScriptLine() (Script.c). A command can make the performance wait. The time spent waiting is added to LapWait, and WaitEndDate is shifted accordingly.

Note 19 — Continuous MIDI controls

A continuous variation of a MIDI parameter, such as _pitchbend(0) _pitchcont … _pitchbend(200), cannot be sent as a single message: it is sent as a stream of messages at regular intervals during the object. For each of the five MIDI parameters (pitchbend, volume, modulation, pressure, panoramic) whose mode is CONTINUOUS, a stream is set up in (*p_control)[nseq].param[index]:

  • starttime, duration: the start date and duration of the object;
  • startvalue, endvalue, and possibly a table of points point with imax entries;
  • channel: the channel on which the variation was set;
  • ibm: the number of messages, computed by Findibm() from the duration and the rate (messages per second) set for this parameter and channel in the settings; ib: the number of messages already sent.

Only one stream per channel and parameter can be active at a time. (*p_active[ch])[index] tells whether a stream is active, (*p_seqcont[ch])[index] on which sequence it is, and (*p_t2cont[ch])[index] the date of its next message. If a stream was still active on this channel (it may not have ended because of rounding), its last message is sent first. The new stream sends its first message at once, and the date of its next message may become the new t2.

When a Csound score line of the object is played (PlayCsoundLine()), these streams are skipped: Csound variations are handled by CscoreWrite() (note 17). The messages of the streams are sent by SendControl() (notes 35 and 50).

Note 20 — Switches and dilation ratio

Switches are the MIDI controllers 64 to 69, including the sustain pedal (64), set with _switchon() and _switchoff(). SendSwitches() (note 43) sends those that have changed when the object starts. The state of the switches of each channel is kept in currswitchstate[ch], one bit per switch.

If the prototype asks for it, SendDilationRatio() (note 44) sends the dilation ratio beta of the object to a MIDI controller. This allows a synthesizer to adapt the sound to the speed of the performance.

Note 21 — Sending the events of an instance

The inner loop sends the events of the current instance as long as:

  • t1 ≤ t2: no other stream has an event before this one;
  • t1 ≤ t3: the instance has not reached its end;
  • ievent < im: the instance has events left.

When two streams have events at the same date, the current instance goes first, which avoids switching between instances for simultaneous events. Each event is processed by notes 22 to 31, then t1 and ievent are advanced to the next event (notes 32 and 33).

Note 22 — End of a cyclic object

When a cyclic object reaches its end (t1 ≥ t3) and integer cycles are not forced, the last cycle is incomplete. If the last event of the object has been reached, all keys still sounding on its channel are released, and the loop moves on to the end-of-period test (note 32).

Before each event, howmuch is computed: the fraction of the object already played, from 0 to 1. Key mapping uses it to move progressively from map0 to map1 (note 51).

Note 23 — Csound score lines of a sound-object

If the prototype has a Csound score and a Csound score is written, the line ievent is passed to CscoreWrite() with its date t0 + t1 and the dilation ratio beta, which scales its duration. If the end of the object is truncated, cut_the_end is the part of this line that falls after the truncation, and its duration is shortened accordingly. No line is written while the object is silenced (note 34).

Note 24 — Decoding an event

Otherwise, the event is a MIDI message. Its status byte c0 is read:

  • for a sound-object, from its MIDI codes;
  • for a simple note or a time pattern, it is NoteOn if ievent == 0, else NoteOff.

MIDIchannel() separates the channel from the status byte (note 49). localchan is the channel on which the message is sent: the one in the MIDI code, the default channel of the prototype, or the channel of the object.

For a NoteOn or NoteOff, the key c1 and the velocity c2 follow. They are read from the next two bytes for a sound-object, or computed for a simple note: GoodKey(j) and velocity 127, which ClipVelocity() will scale. A time pattern produces nothing: its events are skipped.

Note 25 — Transposition and expansion

The key is transposed (_transpose()) and expanded (_keyxpand()) by TransposeAndExpand(). The order of these two operations depends on the order in which they were written in the item (transposefirst). GoodKey() brings the key of a simple note into the range 0–127 by octaves, with an error message.

Note 26 — NoteOff

A NoteOff, or a NoteOn with velocity 0, which MIDI treats as a NoteOff, is handled as follows:

  1. If key mapping changed the key of the matching NoteOn, RetrieveMappedKey() returns the mapped key (note 51).
  2. The counter p_keychannel[localchan][c1] is decreased. If it was already 0, the NoteOff is ignored: its NoteOn was merged with another one (note 28) or not played.
  3. The NoteOff is only sent when the counter reaches 0, i.e. when no other object is still holding this key. In Csound, every NoteOff is passed to CscoreWrite(), which finds the matching NoteOn and writes the score line with its duration. The counter p_keyinstrument of the instrument is decreased.
  4. In MIDI, SendNoteOffAndDraw() sends the NoteOff and draws the note on the pianoroll from its NoteOn date (note 41).

Note 27 — NoteOn: key mapping

If key mapping applies to this object (always for simple notes, according to the prototype for sound-objects), MapThisKey() gives the key to play (note 51). The pair (original key, played key) is stored by StoreMappedKey() after the NoteOn is sent, so that the NoteOff of the original key releases the played key. The counter of the key is then increased.

Note 28 — NoteOn: repeated keys

Two problems arise when a key is struck while it is already sounding:

  • MIDI cannot play the same key twice on the same channel. If the prototype (or the settings) asks to strike again (StrikeAgainOf(), note 46), a NoteOff is sent before the new NoteOn. Otherwise the NoteOn is not sent (strike = FALSE), and the key goes on sounding.
  • Two NoteOns of the same key very close in time are often an artefact of the polymetric structure, for example a note ending and the same note starting a few milliseconds later. If the key is still sounding and the delay since its last NoteOn (delta_timeon) is not more than MaxDeltaTime, the second NoteOn is skipped, and the two notes are merged. MaxDeltaTime is 20 ms, or twice the quantization if quantization is on.

In Csound (cswrite), NoteOns are never merged, and no NoteOff is written before a repeated key, since a score can contain overlapping notes of the same key. If strike again is off, however, the repeated NoteOn is not written either.

Note 29 — NoteOn: velocity and sending

The velocity is computed by ClipVelocity(): the velocity in the MIDI code (or 127 for a simple note) is multiplied by the velocity of the object (_vel()) divided by 127. It can be replaced by the value of a parameter Kx controlled from a MIDI input (velcontrol), and randomised by _rndvel(). The result is limited to 127.

The NoteOn is then sent by SendMIDIevent(), or passed to CscoreWrite(). Its date is stored in p_last_timeon for the pianoroll, and in starttime[key] of the sequence for Csound. No NoteOn is sent while the instance is silenced (note 34). In Csound, the note is drawn on the pianoroll by CscoreWrite() when it writes the score line.

Note 30 — Advancing time after a note

Reading a NoteOn or NoteOff of a sound-object consumed three bytes: ievent was increased twice. t1 is advanced by the delays of the two data bytes, multiplied by beta. (These delays are normally 0.) The delay of the next status byte is added in note 33.

Note 31 — Other MIDI messages

Other messages of sound-objects are sent as they are, with the channel of the object added to channel messages:

  • three-byte messages, such as control changes or pitchbend, with NORMAL_EVENT;
  • two-byte messages, such as program change or channel pressure, with TWO_BYTE_EVENT;
  • single bytes, such as Start, Stop or Continue, with RAW_EVENT.

The timing clock byte (0xF8) is not sent: prototypes use it internally to create silences. In Csound, these messages are ignored.

Note 32 — New period of a cyclic object

When a cyclic object reaches the last event of its period (ievent ≥ iendperiod), the next event is the first event of the period (istartperiod). Its date is the start of the next period plus periodgap, and time_nextperiod_start moves forward by one period. (*p_icycle)[k] counts the cycles.

A new period is played as long as it starts before t3 and the number of cycles ncycles computed by the time-setting has not been reached. Unless the prototype forces an integer number of cycles, send_more allows the last, incomplete cycle to be played: it is cut at t3 (note 22).

Note 33 — Advancing to the next event

ievent is increased, and t1 is advanced by the delay of the new event, multiplied by beta:

  • for a sound-object, the delay of the next MIDI byte or Csound line (EventDelay(), note 46);
  • for a simple note or a time pattern, 1000 ms. The NoteOff of a simple note therefore comes beta × 1000 ms after its NoteOn: a note lasts one beat at the tempo of the item, scaled by its dilation ratio.

If the new date is later than t2 or t3, the inner loop ends.

Note 34 — Suspending or ending the instance

When the inner loop ends, one of two things has happened:

  • The instance has events left (ievent < im): it is suspended. (*p_inext)[k] records its next event and (*p_nextd)[k] the date of this event. If this date is beyond the end of the object, the end of the object was truncated by the time-setting: the next date is set to t3, and if truncend > 0, the instance is marked silenced. Its remaining events will still be read, so that every NoteOff is sent, but its NoteOns will not.
  • The instance has no events left: (*p_nextd)[k] = Infpos, and it will never be chosen again.

Note 35 — Sending a message of a continuous control

If the next event is a message of a continuous control (icont and chancont designate it), SendControl() sends it (note 50). This part is skipped for Csound, where continuous parameters are handled in the score (note 17).

Note 36 — Finding the next event

NextInstance() returns the instance whose next date is the earliest, and its date in t2obj. NextControlDate() then looks for an active continuous control whose next message comes at or before this date (note 48):

  • If there is one, its message is sent (back to note 35), and the search starts again.
  • Otherwise, the scheduler goes back to the instance found, with t1 = its next date (note 11). t2 is set to the date of the next event among all streams, including this instance, so the instance will at least send one event before t1 ≤ t2 fails.

Step 5 — End of the item

Note 37 — After the last occurrence

In real time, an Active Sensing message is sent, which keeps the MIDI driver active so that it can still handle MIDI input. If a pianoroll is displayed, the function jumps to OUTGRAPHIC, which skips the release of keys.

Note 38 — Exits

Three labels handle the end of the function:

  • OVER is reached after an error or an interruption. ENDREPEAT is a normal end. STOP or PANIC return ABORT, and the message “(Stopped playing)” is displayed.
  • OUTGRAPHIC: unless a pianoroll is displayed or Csound is written, ReleaseKeysAndSwitches() (note 43) sends a NoteOff for every key still sounding and switches off every pedal still on. When improvising, playing chunks or playing cyclically, the next item continues the music, so this is only done if the result is RESUME.
  • GETOUT frees the memory and returns.

Note 39 — Silence at the end of an item

An item can end with a silence, for example C4 D4 - -. No event marks the end of this silence, so the next item would start too early when several items are played in a row (improvisation, chunks). The duration of the final silence, add_time, is the difference between the end of the last instance and the end of the last instance that is not a silence. It is materialised:

  • in MIDI, by two NoteOns of key 0 with velocity 0 (i.e. two NoteOffs), at the start and at the end of the silence;
  • in Csound, by a line i1 date duration 0.00 0 0 ; silence.

Note 40 — Closing the Csound score and freeing memory

For Csound, LastTime is advanced by the duration of the item, so that the next item follows it in the score. Unless several items are written in a row, the section ends with an s line. (When the score belongs to a sound-object prototype, prototype.php deletes this line.)

All tables of note 2 are freed. SoundOn is reset. If a pianoroll was displayed, Tcurr is restored to its value at the start of MakeSound().

Subroutines

Note 41 — Sending a MIDI event

All MIDI output goes through SendToDriver() (MIDIstuff.c), which sends the event to real-time MIDI, the MIDI file and the event list, and handles microtonality (scale, blockkey). It takes a MIDI_Event structure and a date in milliseconds. Three helpers fill this structure:

  • SendMIDIevent(type, status, data1, data2, kinstance, scale, blockkey, date, nseq, p_rs) builds the event and sends it. type is NORMAL_EVENT, TWO_BYTE_EVENT (status and data2), or RAW_EVENT (a single byte, given in both status and data2: real-time MIDI reads the first, MIDI files the second). kinstance, scale and blockkey are only needed for NoteOn and NoteOff.
  • SendControlChange(chan, control, value, date, nseq, p_rs) sends a control change.
  • SendNoteOffAndDraw() sends a NoteOff and, if the pianoroll is displayed, draws the note from the date of its NoteOn, stored in p_last_timeon.

p_rs points to the running status, which lets MIDI omit a status byte identical to the previous one. It is reset to 0 wherever the next message must carry its status byte.

Note 42 — Initial controls of an object

SendInitialControls() sends, at the start of an object, the values of volume, panoramic, pitchbend, pressure and modulation of the object, if they differ from the last value sent on its channel (p_Oldvalue). A value is not sent if a continuous control of the same parameter is active on this channel: the stream has priority. Volume and panoramic can be taken from a parameter Kx controlled from a MIDI input. Pitchbend and modulation are sent as 14-bit values (LSB and MSB). The flags ChangedVolume, ChangedPitchbend, etc. record that these parameters have been changed, so that they can be reset later.

Note 43 — Switches

SendSwitches() compares the state of the switches of each channel required by the object with currswitchstate[ch], and sends the six controllers (64–69) of the channels where they differ. If PedalReleaseDefault is set, the switches that remain on are released and pressed again at the start of each object, which renews the sustain as a pianist would.

ReleaseKeysAndSwitches() is called at the end of the item. It sends a NoteOff (NoteOn with velocity 0) for each key whose counter is not zero, and switches off the switches that are on.

Note 44 — Dilation ratio, tuning, MPE

  • SendDilationRatio() converts beta, in the range 0.01 to 100, to the range 0–127 on a logarithmic scale (32 × log10(beta) + 64) and sends it to the controller chosen in the prototype.
  • TuneMIDIdevice() converts the diapason A4freq to a number of semitones and cents relative to 440 Hz, and sends them on every channel with the RPN for A4 tuning (controllers 101/100, then data entry 6/38).
  • SetUpMPEreceiver() would configure the receiver for MPE (MIDI Polyphonic Expression): mode 3, a lower zone on channels 2–16, and a pitchbend sensitivity of ±2 semitones. It is not used at present: its call is disabled in MakeSound(), because this setup proved unnecessary and created noises with some synthesizers.

Note 45 — Csound tempo

WriteCsoundTempo() writes a tempo statement t … 60 at the start of the Csound score. With several items in a row, only the first one gets it. If the score can no longer be written (it may have been closed by another application), the file is reopened and the statement is written again.

Note 46 — Properties of sound-object prototypes

  • ObjectPreRoll(), ObjectPostRoll() and ObjectPivot() return the pre-roll, post-roll and pivot position of prototype j, in milliseconds. Each is either a fixed value (FIXVALUE) or a value that scales with the object, multiplied by beta.
  • StrikeAgainOf() returns the strike again setting of the prototype, or that of the project if the prototype does not set it (note 28).
  • EventDelay() returns the delay, before dilation, between event i - 1 and event i: from the Csound score of the prototype if it is played, else from its MIDI codes.

Note 47 — First event after a truncation

FirstEventAfterTruncation() walks through the MIDI codes of the prototype, adding the delays, until it finds a NoteOn with a non-zero velocity at or after the truncated date truncbeg. It returns its index and, in *p_gap, the time between the truncation and this NoteOn. If there is no such NoteOn, it returns im, and the instance plays nothing.

Note 48 — Next instance, next control

  • NextInstance() returns the instance with the smallest next date p_nextd, and this date. If several instances have the same date, the first one in the table is chosen. When called from the scheduler with p_silence, it also resets the silence flag of the instances it passes, except the current one.
  • NextControlDate() looks among the active continuous controls of all channels for one whose next message comes at or before *p_t2. If it finds one, it returns TRUE with its date in *p_t2, its parameter in *p_icont and its channel in *p_chancont.

Note 49 — Channels, instruments, MIDI or Csound

  • ChannelConvert() returns the channel number of an instance, 1 to 16 when set. A value of 128 or more means that the channel is given by a parameter Kx (_chan(K3)), whose value may come from a MIDI input.
  • MIDIchannel() returns the channel (0–15) on which an event is sent. For a channel message of a sound-object, it removes the channel from the status byte *p_c0, then uses the default channel of the prototype if it has one, or the channel of the message if the prototype says so. Otherwise it uses the channel of the object. In Csound, the channel is 0.
  • ThisInstrument() returns the Csound instrument of an instance: the one set by _ins(), the one assigned to the prototype, or, if the prototype's score defines its own instruments, the instrument of the score line being played. InstrumentIndex() returns its index in the table of instruments of the -cs file.
  • PlayCsoundLine() is TRUE if a Csound score is written and the prototype has a Csound score: then its score is played rather than its MIDI codes. PlayMIDIcode() tells whether the prototype has MIDI codes. Find_im() returns the number of events accordingly.

Note 50 — Streams of continuous controls

  • Findibm() returns the number of messages of a stream: its duration multiplied by the rate set in the settings for this parameter and channel (messages per second).
  • SendControl() sends message ib of a stream. Its value is given by GetTableValue() at the position ib / ibm, and its date is t0 + starttime + duration × ib / ibm. Pitchbend and modulation are sent as 14-bit values, pressure as a two-byte message, volume and panoramic as control changes, on the controllers chosen in the settings. Then ib is increased and the date of the next message is stored in p_t2cont. When the stream is complete, it is deactivated. If another sequence has a stream of the same parameter on the same channel, that stream takes over from the current date.
  • GetTableValue() returns the value at position alpha (0 to 1): a linear interpolation between the start and end values, or between the points of a table if the variation was defined by one.

Note 51 — Key mapping

A key map, set by _keymap(), defines a linear mapping from the range of keys p1–p2 to the range q1–q2. Keys outside the first range are unchanged.

  • KeyImage() applies a mapping to a key, rounding to the nearest key.
  • MapThisKey() chooses the mapping according to mapmode: none, a fixed map, or, in continuous mode, a map interpolated between map0 and map1 according to howmuch, the fraction of the object already played.
  • StoreMappedKey() records the pair (original key, played key) for an instance and channel, and RetrieveMappedKey() finds it, and deletes it, when the NoteOff of the original key arrives. Without this table, a NoteOff mapped with a different howmuch would release the wrong key.

Limitations and oddities

  • The variables ievent, im and localchan are reused from one instance to the next in a few places. For example, ThisInstrument() is called with the ievent of the previous instance when an instance is resumed. This has no effect in the tested cases, but care is needed when changing these parts.
  • For a time pattern, im is not set in the scheduler. It keeps the value of the previous instance.
  • ContinuousParameter(), in MakeSound.c, is not used at present. It is kept for later use.
  • SendDilationRatio() may be wrong when the controller number is less than 64, as a comment in the code says.
  • The time spent by scripts waiting (note 18) is measured with clock(), i.e. processor time, not wall-clock time.

Debugging a performance

Three variables at the top of MakeSound.c switch on detailed traces in the console's messages:

  • trace_makesound traces every step of the scheduler: the dates t1, t2, t3 of each event, the instance chosen, cycles and truncations;
  • trace_missed_noteoff reports NoteOffs that found no sounding key;
  • trace_maps traces key mappings.

In the settings, Trace NoteOn/NoteOff (TraceNoteOn) displays every note sent with its channel and date. It also displays the NoteOns found too close to the previous one of the same key (note 28), marked with ?. The regression test run-ctests.sh plays every grammar and data project of ctests_git/ and compares the MIDI files and Csound scores produced by two builds of the console. It should be run after every change of MakeSound(). It does not test real-time MIDI (STOP, PANIC, scripts waiting for MIDI input), which must be checked by hand.

Note: This page was entirely composed by Claude Opus 5.5 on 13 November 2026.

Leave a Reply

Your email address will not be published. Required fields are marked *

« Translate