-
Notifications
You must be signed in to change notification settings - Fork 0
ChipTuneEngine File Format
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 <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.
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
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
+ 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
; 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]
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.
filter 0 Butterworth LowPass 2 1.5 0 0.1 false
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>")," ...")"
<note> := <pitch> <duration_ms> <instrument> ["adsr:"<adsr_id>] ["flt:"<filter_id>] ["vol:"<volume>]
Where
<pitch> := [A-G](b|#)?[0-8]
A3 33 MyInstrument_3
Gb4 250 PIANO
E#5 30 trumpet ; Same pitch as F5.
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.
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.
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.
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
.
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