Skip to content

ChipTuneEngine File Format

Rasmus Anthin edited this page May 29, 2024 · 15 revisions

The crown of this header-only library is the ChipTuneEngine class. The ChipTuneEngine class plays a tune defined in a text file with file extension *.ct (for Chip Tune). This page outlines the format of such a tune file.

It is important to note that all commands such as instrument, adsr, filter and params must have all their operands on a single line for them to be properly parsed.

Comments starts with either of these characters: # or ;.

Instrument Definitions

Syntax

instrument <instrument_name> <waveform> ["params:"<params_id>]
 ["ffx:"<freq_preset_effect>] [afx:<ampl_preset_effect>] ["pfx:"<phase_preset_effect>] 
 ["adsr:"<adsr_id>] ["flt:"<filter_id>] ["vol:"<volume>]

instrument <instrument_name> "ring_mod_A:"<ring_mod_instr_A> "ring_mod_B:"<ring_mod_instr_B>
 ["adsr:"<adsr_id>] ["flt:"<filter_id>] ["vol:"<volume>]

instrument <instrument_name> "conv_A:"<conv_instr_A> "conv_B:"<conv_instr_B>
 ["adsr:"<adsr_id>] ["flt:"<filter_id>] ["vol:"<volume>]

instrument <instrument_name> "((" <w0> ", " <instrument_name0> "), (" <w1> ", " <instrument_name1> "), " ... ")"
 ["adsr:"<adsr_id>] ["flt:"<filter_id>] ["vol:"<volume>]

instrument <instrument_name> "&"<lib_instrument_name>
 ["ffx:"<freq_preset_effect>] ["afx:"<ampl_preset_effect>] ["pfx:"<phase_preset_effect>]
 ["adsr:"<adsr_id>] ["flt:"<filter_id>] ["vol:"<volume>]

Where

<instrument_name> := [A-Za-z][A-Za-z0-9]*
<waveform> := "SINE"|"SQUARE"|"TRIANGLE"|"SAWTOOTH"|"NOISE" ; SQUARE, TRIANGLE and SAWTOOTH waveform use the waveform generation params parameter `duty_cycle`. Default values of the duty cycle are 0.5 for SQUARE wave and TRIANGLE wave and 1 for SAWTOOTH wave.
In the case with the TRIANGLE wave, the duty cycle goes from reverse sawtooth wave at 0 to pure triangle wave at 0.5 and regular sawtooth wave at 1. In the case with the SAWTOOTH wave, the duty cycle goes from 1 (full regular sawtooth) towards 0 which makes the sawtooth teeth thinner and thinner until they basically become "Kronecker delta"-ish style pulses or just a straight line.
<duty_cycle> := 0..1
<volume> := 0..1
<lib_instrument_name> := "PIANO"|"VIOLIN"|"ORGAN"|"TRUMPET"|"FLUTE"|"GUITAR"|"KICKDRUM"|"SNAREDRUM"|"HIHAT"|"ANVIL" ; Predefined instruments.
<ffx> := "CONSTANT"|"JET_ENGINE_POWERUP"|"CHIRP_0"|"CHIRP_1"|"CHIRP_2" ; Frequency effect.
<afx> := "CONSTANT"|"JET_ENGINE_POWERUP"|"VIBRATO_0" ; Amplitude effect.
<pfx> := "ZERO" ; Phase effect.

Examples

instrument PIANO ring_mod_A:I0 ring_mod_B:I1 adsr:0
instrument I0 SQUARE adsr:1 flt:0 afx:VIBRATO_0
instrument I1 SQUARE params:0
instrument ORGAN ((0.5, PIANO), (0.2, I0)) flt:0
instrument FLUTE_VIB &FLUTE ffx:CONSTANT afx:VIBRATO_0 pfx:ZERO vol:0.15
instrument TRUMPET &TRUMPET ffx:CONSTANT afx:CONSTANT pfx:ZERO vol:0.6
instrument TRUMPET_VIB &TRUMPET ffx:CHIRP_2 afx:VIBRATO_0 pfx:ZERO vol:0.6
instrument KDRUM &KICKDRUM
instrument SDRUM &SNAREDRUM
instrument HIHAT &HIHAT
instrument STEEL_PAN conv_A:FLUTE_VIB conv_B:SDRUM adsr:1

Modulation Envelopes

Syntax

adsr <adsr_nr> "["<attack_mode> <attack_ms> [<level_begin>] [<level_end>]"]"
               "["<decay_mode> <decay_ms> [<level_begin>] [<level_end>]"]"
               "["<sustain_level> [<sustain_max_ms>]"]"
               "["<release_mode> <release_ms> [<level_begin>] [<level_end>]"]"
adsr <adsr_nr> "&"<lib_adsr_name>

Where

<adsr_nr> := [0-9]+
<attack_mode> := <adsr_mode>
<decay_mode> := <adsr_mode>
<release_mode> := <adsr_mode>
<adsr_mode> := "LIN"|"EXP"|"LOG"
<attack_ms> := [0-9]+
<decay_ms> := [0-9]+
<sustain_max_ms> := [0-9]+
<release_ms> := [0-9]+
<sustain_level> := 0..100
<level_begin> := 0..100
<level_end> := 0..100

Charts

+ 100
|     d0
|    .\        s
|    . \   ________  
|    .  \ .        . 
|   /    \.        \ r0
|a1/   d1           \
| /                  \
|/a0.                 \ r1
+-——-+-——-+--------+—--+---> t
|    |    |        |   |
ta   tad  tds      tsr tr

Where

  • Attack : t ∈ [ta = 0, tad].
  • Decay : t ∈ (tad, tds].
  • Sustain : t ∈ (tds, tsr].
  • Release : t ∈ (tsr, tr].

Here we can see that the pairs (a1,d0), (d1,s) and (s,r0) internally actually have separate values and they don’t have to be equal, although they normally are. E.g. Normally a1 = d0 but they can be different, in which case creating a discontinuous jump.

By default the levels are set to:

a0 = 0
a1 = d0 = 100
d1 = s = r0 = 80
r1 = 0

That would instead look like a normal ADSR envelope:

+ 100
|     a1=d0
|       /\   
|      /  \   
|     /    \   d1=s=r0
|    /      \___________           
|   /                   \
|  /                     \
| /                       \ r1
|/ a0                      \
+—————--+-—-+---——-——--+---+——---> t
|       |   |          |   |
ta      tad tds        tsr tr

What's worth to note is that these levels are actually arbitrary, but a normal ADSR is:

a0 = 0
a1 = d0 = 100
d0 > d1
d1 = s = r0
r0 > r1
r1 = 0

Compression and Expansion of Intervals:

Ex0:

t_a               t_ad               t_ds                   t_sr               t_r
|----- T_a_s ----->|----- T_d_s ----->|----- T_s_max_s ----->|----- T_r_s ----->|
|-------------------------------- T_dur_s ----------------------------------------------------->|

T_a_s = .10, T_d_s = .10, (T_s_s = *), T_r_s = .10
t_a = 0, t_ad = .10, t_ds = .20, t_sr = .35, t_r = .45
T_dur_s = .60
t_ad = T_a_s = .10
t_ds = T_a_s + T_d_s = 0.10 + 0.10 = 0.20
t_sr = T_a_s + T_d_s + T_s_max_s = 0.10 + 0.10 + 0.15 = 0.35
t_r = T_a_s + T_d_s + T_s_max_s + T_r_s = .10 + .10 + .15 + .10 = .45

Ex1:

t_a               t_ad               t_ds               t_sr               t_r
|----- T_a_s ----->|----- T_d_s ----->|----- T_s_s ----->|----- T_r_s ----->|
|-------------------------------- T_dur_s --------------------------------->|

T_a_s = .10, T_d_s = .10, (T_s_s = *), T_r_s = .10
t_a = 0, t_ad = .10, t_ds = .20, t_sr = .30, t_r = .40
T_dur_s = .40
T_ads_s = max(T_dur_s - T_r_s, 0) = max(.40 - .10, 0) = 0.30
t_ad = min(T_ads_s, T_a_s) = min(0.30, 0.10) = 0.10 = T_a_s (original)
t_ds = min(T_ads_s, T_a_s + T_d_s) = min(0.30, 0.10 + 0.10) = 0.20 = T_a_s + T_d_s (original)
t_sr = max(t_ds, T_ads_s) = max(0.20, 0.30) = 0.30
t_r = T_dur_s = 0.40

Ex2:

|----- T_a_s ----->|----- T_d_s ----->|- T_s_s ->|----- T_r_s ----->|
|---------------------------- T_dur_s ----------------------------->|

Ex3:

|----- T_a_s ----->|----- T_d_s ----->|----- T_r_s ----->|
|------------------------ T_dur_s ---------------------->|

T_a_s = .10, T_d_s = .10, (T_s_s = *), T_r_s = .10

Ex4:

t_a              t_ad      t_ds/t_sr            t_r
|----- T_a_s ----->|- T_d_s ->|----- T_r_s ----->|
|-------------------- T_dur_s ------------------>|

T_a_s = .10, T_d_s = .10, (T_s_s = *), T_r_s = .10
t_a = 0, t_ad = .10, t_ds = .15, t_sr = .15, t_r = .25
T_dur_s = .25
T_ads_s = max(T_dur_s - T_r_s, 0) = max(.25 - .10, 0) = 0.15
t_ad = min(T_ads_s, T_a_s) = min(0.15, 0.10) = 0.10 = T_a_s (original)
t_ds = min(T_ads_s, T_a_s + T_d_s) = min(0.15, 0.10 + 0.10) = 0.15 < 0.20 = T_a_s + T_d_s
t_sr = max(t_ds, T_ads_s) = max(0.15, 0.15) = 0.15
t_r = T_dur_s = 0.25

Ex5:

t_a          t_ad/t_ds/t_sr          t_r
|----- T_a_s ----->|----- T_r_s ----->|
|-------------- T_dur_s ------------->|

T_a_s = .10, T_d_s = .10, (T_s_s = *), T_r_s = .10
t_a = 0, t_ad = .10, t_ds = .10, t_sr = .10, t_r = .20
T_dur_s = .20
T_ads_s = max(T_dur_s - T_r_s, 0) = max(.20 - .10, 0) = 0.10
t_ad = min(T_ads_s, T_a_s) = min(0.10, 0.10) = 0.10 = T_a_s (original)
t_ds = min(T_ads_s, T_a_s + T_d_s) = min(0.10, 0.10 + 0.10) = 0.10 < 0.20 = T_a_s + T_d_s
t_sr = max(t_ds, T_ads_s) = max(0.10, 0.10) = 0.10
t_r = T_dur_s = 0.20

Ex6:

t_a  t_ad/t_ds/t_sr          t_r
|- T_a_s ->|----- T_r_s ----->|
|---------- T_dur_s --------->|

T_a_s = .10, T_d_s = .10, (T_s_s = *), T_r_s = .10
t_a = 0, t_ad = .05, t_ds = .05, t_sr = .05, t_r = .15
T_dur_s = .15
T_ads_s = max(T_dur_s - T_r_s, 0) = max(.15 - .10, 0) = 0.05
t_ad = min(T_ads_s, T_a_s) = min(0.05, 0.10) = 0.05 < 0.10 = T_a_s
t_ds = min(T_ads_s, T_a_s + T_d_s) = min(0.05, 0.10 + 0.10) = 0.05 < 0.20 = T_a_s + T_d_s
t_sr = max(t_ds, T_ads_s) = max(0.05, 0.05) = 0.05
t_r = T_dur_s = 0.15

Ex7:

t_a/t_ad/t_ds/t_sr        t_r
        |----- T_r_s ----->|
        |----- T_dur_s --->|

T_a_s = .10, T_d_s = .10, (T_s_s = *), T_r_s = .10
t_a = 0, t_ad = 0, t_ds = 0, t_sr = 0, t_r = .10
T_dur_s = .10
T_ads_s = max(T_dur_s - T_r_s, 0) = max(.10 - .10, 0) = 0
t_ad = min(T_ads_s, T_a_s) = min(0, 0.10) = 0 < 0.10 = T_a_s
t_ds = min(T_ads_s, T_a_s + T_d_s) = min(0, 0.10 + 0.10) = 0 < 0.20 = T_a_s + T_d_s
t_sr = max(t_ds, T_ads_s) = max(0, 0) = 0
t_r = T_dur_s = 0.10

Ex8:

t_a/t_ad/t_ds/t_sr       t_r
               |- T_r_s ->|
               |--------->| T_dur_s

T_a_s = .10, T_d_s = .10, (T_s_s = *), T_r_s = .10
t_a = 0, t_ad = 0, t_ds = 0, t_sr = 0, t_r = .05
T_dur_s = .05
T_ads_s = max(T_dur_s - T_r_s, 0) = max(.05 - .10, 0) = 0
t_ad = min(T_ads_s, T_a_s) = min(0, 0.10) = 0 < 0.10 = T_a_s
t_ds = min(T_ads_s, T_a_s + T_d_s) = min(0, 0.10 + 0.10) = 0 < 0.20 = T_a_s + T_d_s
t_sr = max(t_ds, T_ads_s) = max(0, 0) = 0
t_r = T_dur_s = 0.05

Ex9:

t_a/t_ad/t_ds/t_sr/t_r
                    |
                    | T_dur_s

T_a_s = .10, T_d_s = .10, (T_s_s = *), T_r_s = .10
t_a = 0, t_ad = 0, t_ds = 0, t_sr = 0, t_r = 0
T_dur_s = 0
T_ads_s = max(T_dur_s - T_r_s, 0) = max(0 - .10, 0) = 0
t_ad = min(T_ads_s, T_a_s) = min(0, 0.10) = 0 < 0.10 = T_a_s
t_ds = min(T_ads_s, T_a_s + T_d_s) = min(0, 0.10 + 0.10) = 0 < 0.20 = T_a_s + T_d_s
t_sr = max(t_ds, T_ads_s) = max(0, 0) = 0
t_r = T_dur_s = 0

Examples

; Linear attack during 15ms starting at level 0 ending at level 50 (vol: 0.5).
; Exponential decay during 10ms starting at level 100 (vol: 1) thus making a discontinuous jump from the attack.
; Sustain at 50 (vol: 0.5).
; Logarithmic release during 100ms.
adsr 0 [LIN 15 0 50] [EXP 10 100] 50 [LOG 100]

adsr 1 [EXP 16] [EXP 20] [80] [LIN 70]
adsr 2 &ORGAN_2
adsr 3 [LOG 60] [LOG 30] [50 15] [LOG 10]

Filters

Syntax

filter <filter_nr> <type> <op_type> <order> <cutoff_frq_mult> <bandwidth_frq_mult> <ripple> <normalize>

Where

<filter_nr> := [0-9]+
<type> := "Butterworth"|"ChebyshevTypeI"|"ChebyshevTypeII"
<op_type> := "LowPass"|"HighPass"|"BandPass"|"BandStop"
<order> := 1..
<normalize> := "false"|"true"

If bandwidth_frq_mult is not to be used, then set it to a value <= 0.

Examples

filter 0 Butterworth LowPass 2 1.5 0 0.1 false

Waveform Generation Params

Syntax

params <params_nr> ["sample_min:"<sample_range_min>] ["sample_max:"<sample_range_max>]
                   ["duty_cycle:"<duty_cycle>] ["duty_cycle_sweep:"<duty_cycle_sweep>]
                   ["min_freq_limit:"<min_frequency_limit>] ["max_freq_limit:"<max_frequency_limit>]
                   ["freq_slide_vel:"<freq_slide_vel>] ["freq_slide_acc:"<freq_slide_acc>]
                   ["vib_depth:"<vibrato_depth>] ["vib_freq:"<vibrato_freq>]
                   ["vib_freq_vel:"<vibrato_freq_vel>] ["vib_freq_acc:"<vibrato_freq_acc>]
                   ["vib_freq_acc_max_vel_lim:"<vibrato_freq_acc_max_vel_limit>]
                   ["noise_flt_order:"<noise_filter_order>] ["noise_flt_rel_bw:"<noise_filter_rel_bw>]
                   ["noise_flt_slot_dur_s:"<noise_filter_slot_dur_s>]
                   ["arpeggio:"<arpeggio>]

Where

<arpeggio> := "(("<t0_ms>, <freq_mult0>"), ("<t1_ms>, <freq_mult1>")," ...")"

Score

Notes

Syntax

<note> := <pitch> <duration_ms> <instrument> ["adsr:"<adsr_id>] ["flt:"<filter_id>] ["vol:"<volume>]

Where

<pitch> := [A-G](b|#)?[0-8]

Examples

A3 33 MyInstrument_3
Gb4 250 PIANO
E#5 30 trumpet ; Same pitch as F5.

Modifiers

Syntax

NUM_VOICES <num_voices>

Sets the number of voices. Can only be set once and before the score begins.


TIME_STEP_MS <time_step>

Time step (tempo) of the song. A shorter value will make the duration of each note row after it shorter. This can be set multiple times throughout the music.


VOLUME <volume>

Globally sets the (accumulated) volume. This volume is multiplied with every volume level of each note row that comes after it. This can be set multiple times throughout the music.


PRINT <mode>

Where <mode> is either ON or OFF. Toggles debug printing of notes played.

Branching

Syntax

LABEL <label>

Destination label for jump with GOTO or GOTO_TIMES.

GOTO <label>

When execution arrives at this command, it will jump to the LABEL that matches <label>.

GOTO_TIMES <label> <count>

Same as GOTO but will only jump <count> times.

ENDING <jump_no>

Allows for structures like this:

LABEL my_label
... ; Part A. Will be played every repeat.
ENDING 0
... ; Part B. Will be played the first time (0th jump).
ENDING 1
... ; Part C. Will be played the second time (1st jump).
ENDING 2
... ; Part D. Will be played the third time (2nd jump).
GOTO_TIMES my_label 2

In this example, it will play: A, B, A, C, A, D and then move on to the notes below the GOTO_TIMES command.


CODA

This is a muscial notation that denotes a passage that leads the music to an end. It works like a specialized label.


SEGNO

Segno means sign. It is a kind of a musical "goto label".


FINE

Marks the end of branching using DA_CAPO_AL_FINE or DAL_SEGNO_AL_FINE.


DA_CAPO_AL_FINE

When the tune reaches this command, it will branch to the beginning of the tune and then play all the way to the FINE command/sign.


DA_CAPO_AL_CODA

When the tune reaches this command, it will branch to the beginning of the tune, then play all the way to the first TO_CODA sign and then jumps to CODA.


DAL_SEGNO_AL_FINE

...


DAL_SEGNO_AL_CODA

TO_CODA

START

Tells the player to start the tune from here.


END

Tells the player to end the tune here.

Score Table

Syntax

Each row corresponds to one beat. There is no notion of bars here. A table row begins with the command:

TAB

The syntax for a table row is:

TAB -

Means a musical pause for all voices.

"TAB |" (<note>|"-") {"|" (<note>|"-")  }* "|"

See above for definition of <note>. This is the EBNF format for a one or multi voice table row. A dash between two pipes, i.e. | - | means a musical pause for that particular voice.

Examples

TAB | A6 20 HIHAT | - | Gb4 250 ORGAN |

Here we see that for VOICE 0 note A6 is played for a duration of 20 ms and with instrument HIHAT. Then VOICE 1 has a musical pause, and finally, for VOICE 2 a Gb4 note is played for 250 ms and with instrument ORGAN.

Example Score

TIME_STEP_MS 50
NUM_VOICES 3


#TAB | A4 400 STEEL_PAN |
#END
VOLUME 1
LABEL lbl0
TAB | A2 50 KDRUM | C4 250 PIANO adsr:3 |
TAB -
TAB -
TAB -
TAB | A6 20 HIHAT | - | G4 250 ORGAN adsr:3 |
TAB -
TAB -
TAB -
TAB | B4 50 SDRUM |
TAB -
TAB -
TAB -
ENDING 0
TAB | A6 20 HIHAT | E#4 250 PIANO | A4 600 FLUTE_VIB |
TAB -
TAB -
TAB -
TAB | A2 50 KDRUM |
TAB -
TAB -
TAB -
TAB | A6 20 HIHAT |
TAB -
TAB -
TAB -
ENDING 1
TAB | A6 20 HIHAT | F#4 250 PIANO | Db4 600 FLUTE_VIB vol:3.5 |
TAB -
TAB -
TAB -
TAB | A2 50 KDRUM |
TAB -
TAB -
TAB -
TAB | A6 20 HIHAT |
TAB -
TAB -
TAB -
GOTO_TIMES lbl0 1
TAB | B4 50 SDRUM | G4 500 PIANO |
TAB -
TAB -
TAB -
TAB | A6 20 HIHAT | - | Gb4 250 ORGAN |
TAB -
TAB -
TAB -
TAB | A2 50 KDRUM |
TAB -
TAB -
TAB -
SEGNO
TAB | A6 20 HIHAT | D#4 360 TRUMPET |
TAB -
TAB -
TAB -
TAB | B4 50 SDRUM | - | C4 200 TRUMPET |
TAB -
TAB -
TAB -
TAB | A6 20 HIHAT | - | A3 200 TRUMPET |
TAB -
TAB -
TAB | - | - | - |
TAB | A2 50 KDRUM | - | C4 200 TRUMPET |
TAB -
TAB -
TAB -
TAB | A6 20 HIHAT | C4 200 TRUMPET |
TAB -
TAB -
TAB -
TAB | B4 50 SDRUM | Eb4 650 TRUMPET_VIB |
TAB -
TAB -
TAB -
FINE
TO_CODA
TAB | A6 20 HIHAT |
TAB -
TAB -
TAB -
TAB | A2 50 KDRUM |
TAB -
TAB -
TAB -
TIME_STEP_MS 70
TAB | A6 20 HIHAT |
TAB -
TAB -
TAB -
TAB | B4 50 SDRUM |
TAB -
TAB -
TAB -
TIME_STEP_MS 80
TAB | A6 20 HIHAT |
TAB -
TAB -
TAB -
TAB | A2 50 KDRUM |
TAB -
TAB -
TAB -
TIME_STEP_MS 90
TAB | A6 20 HIHAT |
TAB -
TAB -
TAB -
TAB | B4 50 SDRUM |
TAB -
TAB -
TAB -
TIME_STEP_MS 100
TAB | A6 20 HIHAT |
TAB -
TAB -
TAB -
TAB | A2 50 KDRUM |
TAB -
TAB -
TAB -
#DA_CAPO_AL_CODA
#DAL_SEGNO_AL_CODA
VOLUME 0.8
TAB | A6 20 HIHAT | F#4 120 STEEL_PAN adsr:2 |
TAB -
TAB -
TAB -
VOLUME 0.6
TAB | B4 50 SDRUM |
TAB -
TAB -
TAB -
VOLUME 0.4
TAB | A6 20 HIHAT | G5 350 STEEL_PAN adsr:3 |
TAB -
TAB -
TAB -
CODA
TIME_STEP_MS 150
VOLUME 0.2
TAB | A2 50 KDRUM |
TAB -
TAB -
TAB -
TAB | A6 20 HIHAT |
TAB -
TAB -
TAB -
TAB | B4 50 SDRUM |
TAB -
TAB -
TAB -
TAB | A6 20 HIHAT | F4 350 STEEL_PAN |
TAB -
TAB -
TAB -
DA_CAPO_AL_FINE
#DAL_SEGNO_AL_FINE