diff --git a/README.md b/README.md index a6e49fd..3933e7d 100644 --- a/README.md +++ b/README.md @@ -2,75 +2,3 @@ A Python tool to convert a MIDI file to a MakeCode Arcade song! (Work in progress) - -Some bug squashing may be needed but otherwise this tool is complete. - -Web version will be available soon in a different repo. - -## Install - -1. Download and install Python. -2. Clone this repo. -3. Install all the requirements in [`requirements.txt`](requirements.txt) - -> You may need to edit commands listed in this repo to use `py` or `python3` if -> `python` doesn't work. - -## Usage - -Run [`src/main.py`](src/main.py) at the root of the repository in the terminal. -(It is a CLI app) - -### Example commands - -To convert the MIDI file `Never_Gonna_Give_You_Up.mid` and print the Arcade -song to standard output with the default track "dog", no divisor, (divisor of -1), and no character break. - -```commandline -python src/main.py -i "Never_Gonna_Give_You_Up.mid" -``` - -To convert the MIDI file at the absolute path -`E:\Arcade MIDI to Song\testing\Friend_Like_Me_Disneys_Aladdin.mid` and -write the output to `Friend_Like_Me_Disneys_Aladdin song.ts` in the current -directory with the "computer" track, a divisor of 2, a character break of 512, -and with debug messages on. - -```commandline -python src/main.py -i "E:\Arcade MIDI to Song\testing\Friend_Like_Me_Disneys_Aladdin.mid" -o "Friend_Like_Me_Disneys_Aladdin song.ts" -d 2 -t computer -b 512 --debug -``` - -### Help text - -```commandline -usage: ArcadeMIDItoSong [-h] --input INPUT [--output OUTPUT] [--track TRACK] - [--divisor DIVISOR] [--break CHAR_BREAK] [--debug] - -A program to convert MIDI files to the Arcade song format. - -options: - -h, --help show this help message and exit - --input INPUT, -i INPUT - Input MIDI file - --output OUTPUT, -o OUTPUT - Output text file path, otherwise we will output to - standard output. - --track TRACK, -t TRACK - A track to use, which changes the instrument. - Available tracks include ['dog', 'duck', 'cat', - 'fish', 'car', 'computer', 'burger', 'cherry', - 'lemon']. (You can also use indices 0-8) Defaults to - 'dog'. - --divisor DIVISOR, -d DIVISOR - A divisor to reduce the number of measures used. A - higher integer means a longer song can fit in the - maximum of 255 measures of a song, but with less - precision. Must be greater than or equal to 1, and - defaults to 1 for no division. - --break CHAR_BREAK, -b CHAR_BREAK - Break the hex string after so many characters. - Defaults to 0 for no breaking. - --debug Include debug messages. Defaults to info and greater - severity messages only. -``` diff --git a/example/blank_instrument_params.yaml b/example/blank_instrument_params.yaml new file mode 100644 index 0000000..6a3a3ca --- /dev/null +++ b/example/blank_instrument_params.yaml @@ -0,0 +1,450 @@ +_comment: " +Full GM1 support, minimal GM2 support (the extended GM2 drum notes, but only on +the standard kit), and very minimal Roland GS/Yamaha XG support (parsing very +specific SysEx messages) + +Units: + Attack, decay, release: milliseconds + Sustain and amplitudes: range 0-1024 + LFO frequencies: hz + +Waveforms map to as expected by MakeCode Arcade: + triangle = 1 + sawtooth = 2 + sine = 3 + noise_tunable = 4 + noise = 5 + square_10 = 11 + square_20 = 12 + square_30 = 13 + square_40 = 14 + square_50 (or square) = 15 + cycle_16 = 16 + cycle_32 = 17 + cycle_64 (or noise_tunable_2) = 18 + +https://arcade.makecode.com/developer/sound +> The noise is an actual white noise. It ignores the requested frequency or +> note. +> +> The tunable noise tone contains a pseudorandom sample. At high frequency it +> sounds similar to white noise, at low frequency it produces irregular +> crackling noises. +> +> The cycle 16, cycle 32, and cycle 64 waveforms are repeating pseudorandom +> patterns with a cycle length of 16/32/64 samples respectively. They sound +> like a noise-distorted square wave, with short cycles being more regular and +> long cycles being noisier. + +For testing instrument parameters, you can use this, by Richard +https://arcade.makecode.com/18336-29202-55655-79439 +" +melodic_instruments: + # Pianos + - instrument: 0 + _comment: Acoustic Grand Piano + waveform: triangle + amp_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 1 + _comment: Bright Acoustic Piano + - instrument: 2 + _comment: Electric Grand Piano + - instrument: 3 + _comment: Honky-tonk Piano + - instrument: 4 + _comment: Electric Piano 1 + - instrument: 5 + _comment: Electric Piano 2 + - instrument: 6 + _comment: Harpsichord + - instrument: 7 + _comment: Clavinet + # Chromatic percussion + - instrument: 8 + _comment: Celesta + - instrument: 9 + _comment: Glockenspiel + - instrument: 10 + _comment: Music Box + - instrument: 11 + _comment: Vibraphone + - instrument: 12 + _comment: Marimba + - instrument: 13 + _comment: Xylophone + - instrument: 14 + _comment: Tubular Bells + - instrument: 15 + _comment: Dulcimer + # Organs + - instrument: 16 + _comment: Drawbar Organ + - instrument: 17 + _comment: Percussive Organ + - instrument: 18 + _comment: Rock Organ + - instrument: 19 + _comment: Church Organ + - instrument: 20 + _comment: Reed Organ + - instrument: 21 + _comment: Accordion + - instrument: 22 + _comment: Harmonica + - instrument: 23 + _comment: Tango Accordion + # Guitars + - instrument: 24 + _comment: Acoustic Guitar (nylon) + - instrument: 25 + _comment: Acoustic Guitar (steel) + - instrument: 26 + _comment: Electric Guitar (jazz) + - instrument: 27 + _comment: Electric Guitar (clean) + - instrument: 28 + _comment: Electric Guitar (muted) + - instrument: 29 + _comment: Overdriven Guitar + - instrument: 30 + _comment: Distortion Guitar + - instrument: 31 + _comment: Guitar Harmonics + # Basses + - instrument: 32 + _comment: Acoustic Bass + - instrument: 33 + _comment: Electric Bass (finger) + - instrument: 34 + _comment: Electric Bass (pick) + - instrument: 35 + _comment: Fretless Bass + - instrument: 36 + _comment: Slap Bass 1 + - instrument: 37 + _comment: Slap Bass 2 + - instrument: 38 + _comment: Synth Bass 1 + - instrument: 39 + _comment: Synth Bass 2 + # Strings + - instrument: 40 + _comment: Violin + - instrument: 41 + _comment: Viola + - instrument: 42 + _comment: Cello + - instrument: 43 + _comment: Contrabass + - instrument: 44 + _comment: Tremolo Strings + - instrument: 45 + _comment: Pizzicato Strings + - instrument: 46 + _comment: Orchestral Harp + - instrument: 47 + _comment: Timpani + # Strings continued + - instrument: 48 + _comment: String Ensemble 1 + - instrument: 49 + _comment: String Ensemble 2 + - instrument: 50 + _comment: Synth Strings 1 + - instrument: 51 + _comment: Synth Strings 2 + - instrument: 52 + _comment: Choir Aahs + - instrument: 53 + _comment: Voice Oohs + - instrument: 54 + _comment: Synth Voice + - instrument: 55 + _comment: Orchestra Hit + # Brass + - instrument: 56 + _comment: Trumpet + - instrument: 57 + _comment: Trombone + - instrument: 58 + _comment: Tuba + - instrument: 59 + _comment: Muted Trumpet + - instrument: 60 + _comment: French Horn + - instrument: 61 + _comment: Brass Section + - instrument: 62 + _comment: Synth Brass 1 + - instrument: 63 + _comment: Synth Brass 2 + # Reeds + - instrument: 64 + _comment: Soprano Sax + - instrument: 65 + _comment: Alto Sax + - instrument: 66 + _comment: Tenor Sax + - instrument: 67 + _comment: Baritone Sax + - instrument: 68 + _comment: Oboe + - instrument: 69 + _comment: English Horn + - instrument: 70 + _comment: Bassoon + - instrument: 71 + _comment: Clarinet + # Pipes + - instrument: 72 + _comment: Piccolo + - instrument: 73 + _comment: Flute + - instrument: 74 + _comment: Recorder + - instrument: 75 + _comment: Pan Flute + - instrument: 76 + _comment: Blown Bottle + - instrument: 77 + _comment: Shakuhachi + - instrument: 78 + _comment: Whistle + - instrument: 79 + _comment: Ocarina + # Synth leads + - instrument: 80 + _comment: Lead 1 (square) + - instrument: 81 + _comment: Lead 2 (sawtooth) + - instrument: 82 + _comment: Lead 3 (calliope) + - instrument: 83 + _comment: Lead 4 (chiff) + - instrument: 84 + _comment: Lead 5 (charang) + - instrument: 85 + _comment: Lead 6 (voice) + - instrument: 86 + _comment: Lead 7 (fifths) + - instrument: 87 + _comment: Lead 8 (bass+lead) + # Synth pads + - instrument: 88 + _comment: Pad 1 (new age) + - instrument: 89 + _comment: Pad 2 (warm) + - instrument: 90 + _comment: Pad 3 (polysynth) + - instrument: 91 + _comment: Pad 4 (choir) + - instrument: 92 + _comment: Pad 5 (bowed) + - instrument: 93 + _comment: Pad 6 (metallic) + - instrument: 94 + _comment: Pad 7 (halo) + - instrument: 95 + _comment: Pad 8 (sweep) + # Synth sfx + - instrument: 96 + _comment: FX 1 (rain) + - instrument: 97 + _comment: FX 2 (soundtrack) + - instrument: 98 + _comment: FX 3 (crystal) + - instrument: 99 + _comment: FX 4 (atmosphere) + - instrument: 100 + _comment: FX 5 (brightness) + - instrument: 101 + _comment: FX 6 (goblins) + - instrument: 102 + _comment: FX 7 (echoes) + - instrument: 103 + _comment: FX 8 (sci-fi) + # Ethnic + - instrument: 104 + _comment: Sitar + - instrument: 105 + _comment: Banjo + - instrument: 106 + _comment: Shamisen + - instrument: 107 + _comment: Koto + - instrument: 108 + _comment: Kalimba + - instrument: 109 + _comment: Bag Pipe + - instrument: 110 + _comment: Fiddle + - instrument: 111 + _comment: Shanai + # Percussive + - instrument: 112 + _comment: Tinkle Bell + - instrument: 113 + _comment: Agogo + - instrument: 114 + _comment: Steel Drums + - instrument: 115 + _comment: Woodblock + - instrument: 116 + _comment: Taiko Drum + - instrument: 117 + _comment: Melodic Tom + - instrument: 118 + _comment: Synth Drum + # Sfx + - instrument: 119 + _comment: Reverse Cymbal + - instrument: 120 + _comment: Guitar Fret Noise + - instrument: 121 + _comment: Breath Noise + - instrument: 122 + _comment: Seashore + - instrument: 123 + _comment: Bird Tweet + - instrument: 124 + _comment: Telephone Ring + - instrument: 125 + _comment: Helicopter + - instrument: 126 + _comment: Applause + - instrument: 127 + _comment: Gunshot +drum_instruments: + - # GM2 start + - note: 27 + _comment: High Q + start_freq: 0 + start_vol: 0 + steps: + - { waveform: noise_tunable, target_freq: 0, target_vol: 0, duration: 0 } + - note: 28 + _comment: Slap + - note: 29 + _comment: Scratch Push + - note: 30 + _comment: Scratch Pull + - note: 31 + _comment: Sticks + - note: 32 + _comment: Square Click + - note: 33 + _comment: Metronome Click + - note: 34 + _comment: Metronome Bell + # GM2 end + - note: 35 + _comment: Bass Drum 2 + - note: 36 + _comment: Bass Drum 1 + - note: 37 + _comment: Side Stick + - note: 38 + _comment: Snare Drum 1 + - note: 39 + _comment: Hand Clap + - note: 40 + _comment: Snare Drum 2 + - note: 41 + _comment: Low Tom 2 + - note: 42 + _comment: Closed Hi-Hat + - note: 43 + _comment: Low Tom 1 + - note: 44 + _comment: Pedal Hi-Hat + - note: 45 + _comment: Mid Tom 2 + - note: 46 + _comment: Open Hi-Hat + - note: 47 + _comment: Mid Tom 1 + - note: 48 + _comment: High Tom 2 + - note: 49 + _comment: Crash Cymbal 1 + - note: 50 + _comment: High Tom 1 + - note: 51 + _comment: Ride Cymbal 1 + - note: 52 + _comment: Chinese Cymbal + - note: 53 + _comment: Ride Bell + - note: 54 + _comment: Tambourine + - note: 55 + _comment: Splash Cymbal + - note: 56 + _comment: Cowbell + - note: 57 + _comment: Crash Cymbal 2 + - note: 58 + _comment: Vibraslap + - note: 59 + _comment: Ride Cymbal 2 + - note: 60 + _comment: High Bongo + - note: 61 + _comment: Low Bongo + - note: 62 + _comment: Mute High Conga + - note: 63 + _comment: Open High Conga + - note: 64 + _comment: Low Conga + - note: 65 + _comment: High Timbale + - note: 66 + _comment: Low Timbale + - note: 67 + _comment: High Agogo + - note: 68 + _comment: Low Agogo + - note: 69 + _comment: Cabasa + - note: 70 + _comment: Maracas + - note: 71 + _comment: Short Whistle + - note: 72 + _comment: Long Whistle + - note: 73 + _comment: Short Guiro + - note: 74 + _comment: Long Guiro + - note: 75 + _comment: Claves + - note: 76 + _comment: High Wood Block + - note: 77 + _comment: Low Wood Block + - note: 78 + _comment: Mute Cuica + - note: 79 + _comment: Open Cuica + - note: 80 + _comment: Mute Triangle + - note: 81 + _comment: Open Triangle + # GM2 start + - note: 82 + _comment: Shaker + - note: 83 + _comment: Jingle Bell + - note: 84 + _comment: Belltree + - note: 85 + _comment: Castanets + - note: 86 + _comment: Mute Surdo + - note: 87 + _comment: Open Surdo + # GM2 end diff --git a/example/instrument_params_2.yaml b/example/instrument_params_2.yaml new file mode 100644 index 0000000..17c7e61 --- /dev/null +++ b/example/instrument_params_2.yaml @@ -0,0 +1,1378 @@ +_comment: " +This file was generated by Claude for testing purposes. + +Units: + Attack, decay, release: milliseconds + Sustain and amplitudes: range 0-1024 + LFO frequencies: hz + +Waveforms map to as expected by MakeCode Arcade: + triangle = 1 + sawtooth = 2 + sine = 3 + noise_tunable = 4 + noise = 5 + square_10 = 11 + square_20 = 12 + square_30 = 13 + square_40 = 14 + square_50 (or square) = 15 + cycle_16 = 16 + cycle_32 = 17 + cycle_64 (or noise_tunable_2) = 18 + +https://arcade.makecode.com/developer/sound +> The noise is an actual white noise. It ignores the requested frequency or +> note. +> +> The tunable noise tone contains a pseudorandom sample. At high frequency it +> sounds similar to white noise, at low frequency it produces irregular +> crackling noises. +> +> The cycle 16, cycle 32, and cycle 64 waveforms are repeating pseudorandom +> patterns with a cycle length of 16/32/64 samples respectively. They sound +> like a noise-distorted square wave, with short cycles being more regular and +> long cycles being noisier. +" +_comment2: 'MakeCode Arcade GM1/GM2 instrument parameter map. All envelope times in ms. sustain/amplitude values 0-1024. LFO + frequency in Hz. Waveforms: triangle=1 sawtooth=2 sine=3 noise_tunable=4 noise=5 square_10=11 square_20=12 square_30=13 + square_40=14 square_50=15 cycle_16=16 cycle_32=17 cycle_64=18. noise ignores pitch. noise_tunable is pitch-dependent (high=white + noise, low=crackling). cycle_N are periodic pseudo-random patterns (shorter = more regular). Drum notes: GM2 extended range + 27-87 (GM1 was 35-81). Notes 71-74 and 78-79 are unassigned in both GM1 and GM2.' +melodic_instruments: + - instrument: 0 + _comment: Acoustic Grand Piano + waveform: triangle + amp_envelope: { attack: 5, decay: 400, sustain: 300, release: 200, amplitude: 900 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 1 + _comment: Bright Acoustic Piano + waveform: triangle + amp_envelope: { attack: 5, decay: 280, sustain: 250, release: 150, amplitude: 950 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 2 + _comment: Electric Grand Piano + waveform: triangle + amp_envelope: { attack: 5, decay: 350, sustain: 350, release: 180, amplitude: 880 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 3 + _comment: Honky-tonk Piano + waveform: triangle + amp_envelope: { attack: 5, decay: 250, sustain: 250, release: 100, amplitude: 850 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 4, amplitude: 6 } + - instrument: 4 + _comment: Electric Piano 1 + waveform: triangle + amp_envelope: { attack: 8, decay: 500, sustain: 300, release: 250, amplitude: 850 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 5 + _comment: Electric Piano 2 + waveform: sine + amp_envelope: { attack: 5, decay: 400, sustain: 400, release: 200, amplitude: 870 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 6 + _comment: Harpsichord + waveform: sawtooth + amp_envelope: { attack: 2, decay: 400, sustain: 0, release: 50, amplitude: 900 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 7 + _comment: Clavinet + waveform: sawtooth + amp_envelope: { attack: 2, decay: 180, sustain: 0, release: 60, amplitude: 920 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 8 + _comment: Celesta + waveform: sine + amp_envelope: { attack: 5, decay: 900, sustain: 0, release: 200, amplitude: 800 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 9 + _comment: Glockenspiel + waveform: sine + amp_envelope: { attack: 2, decay: 1100, sustain: 0, release: 300, amplitude: 850 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 10 + _comment: Music Box + waveform: sine + amp_envelope: { attack: 2, decay: 700, sustain: 0, release: 150, amplitude: 750 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 11 + _comment: Vibraphone + waveform: sine + amp_envelope: { attack: 10, decay: 900, sustain: 50, release: 500, amplitude: 830 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 5, amplitude: 50 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 12 + _comment: Marimba + waveform: triangle + amp_envelope: { attack: 2, decay: 350, sustain: 0, release: 80, amplitude: 870 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 13 + _comment: Xylophone + waveform: triangle + amp_envelope: { attack: 2, decay: 280, sustain: 0, release: 70, amplitude: 900 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 14 + _comment: Tubular Bells + waveform: sine + amp_envelope: { attack: 5, decay: 1400, sustain: 30, release: 500, amplitude: 820 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 15 + _comment: Dulcimer + waveform: triangle + amp_envelope: { attack: 5, decay: 500, sustain: 80, release: 180, amplitude: 800 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 16 + _comment: Drawbar Organ + waveform: square_50 + amp_envelope: { attack: 5, decay: 0, sustain: 900, release: 50, amplitude: 900 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 5, amplitude: 3 } + - instrument: 17 + _comment: Percussive Organ + waveform: square_50 + amp_envelope: { attack: 5, decay: 180, sustain: 700, release: 40, amplitude: 880 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 18 + _comment: Rock Organ + waveform: square_50 + amp_envelope: { attack: 5, decay: 0, sustain: 900, release: 50, amplitude: 920 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 4, amplitude: 30 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 19 + _comment: Church Organ + waveform: square_50 + amp_envelope: { attack: 30, decay: 0, sustain: 900, release: 100, amplitude: 900 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 20 + _comment: Reed Organ + waveform: square_30 + amp_envelope: { attack: 20, decay: 0, sustain: 850, release: 80, amplitude: 880 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 21 + _comment: Accordion + waveform: square_30 + amp_envelope: { attack: 15, decay: 0, sustain: 850, release: 60, amplitude: 870 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 5, amplitude: 30 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 22 + _comment: Harmonica + waveform: square_20 + amp_envelope: { attack: 15, decay: 50, sustain: 800, release: 80, amplitude: 860 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 5, amplitude: 40 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 23 + _comment: Tango Accordion + waveform: square_30 + amp_envelope: { attack: 10, decay: 50, sustain: 850, release: 60, amplitude: 860 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 6, amplitude: 30 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 24 + _comment: Acoustic Guitar (nylon) + waveform: triangle + amp_envelope: { attack: 5, decay: 380, sustain: 80, release: 280, amplitude: 830 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 25 + _comment: Acoustic Guitar (steel) + waveform: triangle + amp_envelope: { attack: 3, decay: 320, sustain: 120, release: 220, amplitude: 860 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 26 + _comment: Electric Guitar (jazz) + waveform: triangle + amp_envelope: { attack: 5, decay: 380, sustain: 300, release: 180, amplitude: 820 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 27 + _comment: Electric Guitar (clean) + waveform: triangle + amp_envelope: { attack: 3, decay: 280, sustain: 400, release: 180, amplitude: 850 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 28 + _comment: Electric Guitar (muted) + waveform: triangle + amp_envelope: { attack: 2, decay: 120, sustain: 0, release: 60, amplitude: 870 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 29 + _comment: Overdriven Guitar + waveform: cycle_16 + amp_envelope: { attack: 3, decay: 180, sustain: 700, release: 120, amplitude: 920 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 30 + _comment: Distortion Guitar + waveform: cycle_32 + amp_envelope: { attack: 2, decay: 100, sustain: 800, release: 100, amplitude: 950 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 31 + _comment: Guitar Harmonics + waveform: sine + amp_envelope: { attack: 5, decay: 700, sustain: 150, release: 300, amplitude: 800 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 32 + _comment: Acoustic Bass + waveform: triangle + amp_envelope: { attack: 5, decay: 280, sustain: 450, release: 130, amplitude: 900 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 33 + _comment: Electric Bass (finger) + waveform: triangle + amp_envelope: { attack: 5, decay: 230, sustain: 520, release: 120, amplitude: 900 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 34 + _comment: Electric Bass (pick) + waveform: sawtooth + amp_envelope: { attack: 2, decay: 180, sustain: 520, release: 90, amplitude: 920 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 35 + _comment: Fretless Bass + waveform: triangle + amp_envelope: { attack: 10, decay: 200, sustain: 600, release: 200, amplitude: 880 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 3, amplitude: 6 } + - instrument: 36 + _comment: Slap Bass 1 + waveform: triangle + amp_envelope: { attack: 2, decay: 140, sustain: 300, release: 90, amplitude: 940 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 37 + _comment: Slap Bass 2 + waveform: sawtooth + amp_envelope: { attack: 2, decay: 140, sustain: 350, release: 90, amplitude: 950 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 38 + _comment: Synth Bass 1 + waveform: square_50 + amp_envelope: { attack: 5, decay: 180, sustain: 620, release: 90, amplitude: 900 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 39 + _comment: Synth Bass 2 + waveform: sawtooth + amp_envelope: { attack: 5, decay: 180, sustain: 620, release: 90, amplitude: 920 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 40 + _comment: Violin + waveform: sawtooth + amp_envelope: { attack: 50, decay: 0, sustain: 900, release: 150, amplitude: 870 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 5, amplitude: 9 } + - instrument: 41 + _comment: Viola + waveform: sawtooth + amp_envelope: { attack: 60, decay: 0, sustain: 880, release: 150, amplitude: 860 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 5, amplitude: 8 } + - instrument: 42 + _comment: Cello + waveform: sawtooth + amp_envelope: { attack: 70, decay: 0, sustain: 880, release: 200, amplitude: 870 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 4, amplitude: 8 } + - instrument: 43 + _comment: Contrabass + waveform: sawtooth + amp_envelope: { attack: 80, decay: 0, sustain: 870, release: 200, amplitude: 860 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 3, amplitude: 7 } + - instrument: 44 + _comment: Tremolo Strings + waveform: sawtooth + amp_envelope: { attack: 30, decay: 0, sustain: 800, release: 150, amplitude: 840 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 8, amplitude: 100 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 45 + _comment: Pizzicato Strings + waveform: triangle + amp_envelope: { attack: 3, decay: 380, sustain: 0, release: 130, amplitude: 850 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 46 + _comment: Orchestral Harp + waveform: triangle + amp_envelope: { attack: 5, decay: 480, sustain: 80, release: 280, amplitude: 840 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 47 + _comment: Timpani + waveform: triangle + amp_envelope: { attack: 3, decay: 600, sustain: 40, release: 280, amplitude: 900 } + pitch_envelope: { attack: 5, decay: 300, sustain: 0, release: 150, amplitude: 120 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 48 + _comment: String Ensemble 1 + waveform: sawtooth + amp_envelope: { attack: 80, decay: 0, sustain: 880, release: 200, amplitude: 870 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 5, amplitude: 10 } + - instrument: 49 + _comment: String Ensemble 2 + waveform: sawtooth + amp_envelope: { attack: 100, decay: 0, sustain: 860, release: 250, amplitude: 860 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 5, amplitude: 8 } + - instrument: 50 + _comment: Synth Strings 1 + waveform: sawtooth + amp_envelope: { attack: 60, decay: 100, sustain: 800, release: 300, amplitude: 860 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 51 + _comment: Synth Strings 2 + waveform: square_40 + amp_envelope: { attack: 80, decay: 100, sustain: 780, release: 300, amplitude: 840 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 52 + _comment: Choir Aahs + waveform: sine + amp_envelope: { attack: 120, decay: 50, sustain: 900, release: 300, amplitude: 850 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 4, amplitude: 6 } + - instrument: 53 + _comment: Voice Oohs + waveform: sine + amp_envelope: { attack: 140, decay: 50, sustain: 900, release: 300, amplitude: 840 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 4, amplitude: 5 } + - instrument: 54 + _comment: Synth Voice + waveform: square_20 + amp_envelope: { attack: 90, decay: 50, sustain: 870, release: 250, amplitude: 830 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 4, amplitude: 7 } + - instrument: 55 + _comment: Orchestra Hit + waveform: sawtooth + amp_envelope: { attack: 2, decay: 300, sustain: 0, release: 100, amplitude: 960 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 56 + _comment: Trumpet + waveform: sawtooth + amp_envelope: { attack: 20, decay: 50, sustain: 900, release: 100, amplitude: 930 } + pitch_envelope: { attack: 12, decay: 60, sustain: 0, release: 50, amplitude: 40 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 57 + _comment: Trombone + waveform: sawtooth + amp_envelope: { attack: 30, decay: 50, sustain: 880, release: 150, amplitude: 920 } + pitch_envelope: { attack: 15, decay: 60, sustain: 0, release: 60, amplitude: 35 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 58 + _comment: Tuba + waveform: sawtooth + amp_envelope: { attack: 30, decay: 50, sustain: 870, release: 150, amplitude: 910 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 59 + _comment: Muted Trumpet + waveform: square_20 + amp_envelope: { attack: 15, decay: 50, sustain: 800, release: 80, amplitude: 870 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 60 + _comment: French Horn + waveform: sawtooth + amp_envelope: { attack: 55, decay: 50, sustain: 880, release: 200, amplitude: 900 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 4, amplitude: 6 } + - instrument: 61 + _comment: Brass Section + waveform: sawtooth + amp_envelope: { attack: 15, decay: 50, sustain: 900, release: 100, amplitude: 940 } + pitch_envelope: { attack: 10, decay: 50, sustain: 0, release: 40, amplitude: 30 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 62 + _comment: Synth Brass 1 + waveform: square_50 + amp_envelope: { attack: 10, decay: 100, sustain: 850, release: 150, amplitude: 920 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 63 + _comment: Synth Brass 2 + waveform: sawtooth + amp_envelope: { attack: 10, decay: 100, sustain: 850, release: 150, amplitude: 920 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 64 + _comment: Soprano Sax + waveform: square_30 + amp_envelope: { attack: 20, decay: 50, sustain: 850, release: 100, amplitude: 890 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 5, amplitude: 9 } + - instrument: 65 + _comment: Alto Sax + waveform: square_30 + amp_envelope: { attack: 20, decay: 50, sustain: 850, release: 100, amplitude: 880 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 5, amplitude: 9 } + - instrument: 66 + _comment: Tenor Sax + waveform: square_30 + amp_envelope: { attack: 25, decay: 50, sustain: 840, release: 120, amplitude: 880 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 5, amplitude: 8 } + - instrument: 67 + _comment: Baritone Sax + waveform: square_30 + amp_envelope: { attack: 30, decay: 50, sustain: 830, release: 150, amplitude: 870 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 4, amplitude: 7 } + - instrument: 68 + _comment: Oboe + waveform: square_10 + amp_envelope: { attack: 20, decay: 30, sustain: 870, release: 80, amplitude: 870 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 5, amplitude: 7 } + - instrument: 69 + _comment: English Horn + waveform: square_20 + amp_envelope: { attack: 25, decay: 30, sustain: 860, release: 100, amplitude: 860 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 5, amplitude: 6 } + - instrument: 70 + _comment: Bassoon + waveform: square_20 + amp_envelope: { attack: 30, decay: 30, sustain: 850, release: 120, amplitude: 860 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 4, amplitude: 5 } + - instrument: 71 + _comment: Clarinet + waveform: square_50 + amp_envelope: { attack: 20, decay: 30, sustain: 870, release: 100, amplitude: 870 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 5, amplitude: 7 } + - instrument: 72 + _comment: Piccolo + waveform: triangle + amp_envelope: { attack: 15, decay: 20, sustain: 880, release: 80, amplitude: 850 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 5, amplitude: 6 } + - instrument: 73 + _comment: Flute + waveform: sine + amp_envelope: { attack: 20, decay: 20, sustain: 880, release: 100, amplitude: 840 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 5, amplitude: 20 } + pitch_lfo: { frequency: 5, amplitude: 9 } + - instrument: 74 + _comment: Recorder + waveform: triangle + amp_envelope: { attack: 15, decay: 20, sustain: 870, release: 80, amplitude: 840 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 4, amplitude: 6 } + - instrument: 75 + _comment: Pan Flute + waveform: sine + amp_envelope: { attack: 35, decay: 20, sustain: 870, release: 120, amplitude: 830 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 4, amplitude: 8 } + - instrument: 76 + _comment: Blown Bottle + waveform: noise_tunable + amp_envelope: { attack: 25, decay: 120, sustain: 650, release: 180, amplitude: 820 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 77 + _comment: Shakuhachi + waveform: noise_tunable + amp_envelope: { attack: 60, decay: 60, sustain: 750, release: 220, amplitude: 820 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 4, amplitude: 12 } + - instrument: 78 + _comment: Whistle + waveform: sine + amp_envelope: { attack: 15, decay: 0, sustain: 900, release: 80, amplitude: 830 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 5, amplitude: 10 } + - instrument: 79 + _comment: Ocarina + waveform: sine + amp_envelope: { attack: 20, decay: 20, sustain: 880, release: 100, amplitude: 830 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 4, amplitude: 8 } + - instrument: 80 + _comment: Lead 1 (square) + waveform: square_50 + amp_envelope: { attack: 5, decay: 0, sustain: 900, release: 50, amplitude: 900 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 81 + _comment: Lead 2 (sawtooth) + waveform: sawtooth + amp_envelope: { attack: 5, decay: 0, sustain: 900, release: 50, amplitude: 920 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 82 + _comment: Lead 3 (calliope) + waveform: triangle + amp_envelope: { attack: 10, decay: 50, sustain: 800, release: 100, amplitude: 870 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 83 + _comment: Lead 4 (chiff) + waveform: cycle_16 + amp_envelope: { attack: 5, decay: 120, sustain: 700, release: 100, amplitude: 880 } + pitch_envelope: { attack: 6, decay: 80, sustain: 0, release: 50, amplitude: 30 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 84 + _comment: Lead 5 (charang) + waveform: cycle_32 + amp_envelope: { attack: 5, decay: 100, sustain: 800, release: 100, amplitude: 900 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 3, amplitude: 6 } + - instrument: 85 + _comment: Lead 6 (voice) + waveform: square_20 + amp_envelope: { attack: 30, decay: 50, sustain: 850, release: 150, amplitude: 860 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 4, amplitude: 8 } + - instrument: 86 + _comment: Lead 7 (fifths) + waveform: square_50 + amp_envelope: { attack: 5, decay: 0, sustain: 900, release: 50, amplitude: 910 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 87 + _comment: Lead 8 (bass+lead) + waveform: sawtooth + amp_envelope: { attack: 5, decay: 100, sustain: 800, release: 100, amplitude: 920 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 88 + _comment: Pad 1 (new age) + waveform: sine + amp_envelope: { attack: 220, decay: 100, sustain: 800, release: 450, amplitude: 820 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 89 + _comment: Pad 2 (warm) + waveform: triangle + amp_envelope: { attack: 170, decay: 100, sustain: 850, release: 450, amplitude: 840 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 90 + _comment: Pad 3 (polysynth) + waveform: sawtooth + amp_envelope: { attack: 110, decay: 100, sustain: 800, release: 380, amplitude: 850 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 91 + _comment: Pad 4 (choir) + waveform: sine + amp_envelope: { attack: 160, decay: 50, sustain: 900, release: 430, amplitude: 830 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 3, amplitude: 6 } + - instrument: 92 + _comment: Pad 5 (bowed) + waveform: triangle + amp_envelope: { attack: 220, decay: 0, sustain: 900, release: 520, amplitude: 820 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 4, amplitude: 5 } + - instrument: 93 + _comment: Pad 6 (metallic) + waveform: cycle_16 + amp_envelope: { attack: 110, decay: 200, sustain: 700, release: 420, amplitude: 840 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 94 + _comment: Pad 7 (halo) + waveform: sine + amp_envelope: { attack: 270, decay: 100, sustain: 800, release: 520, amplitude: 820 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 3, amplitude: 9 } + - instrument: 95 + _comment: Pad 8 (sweep) + waveform: sawtooth + amp_envelope: { attack: 220, decay: 200, sustain: 700, release: 520, amplitude: 830 } + pitch_envelope: { attack: 60, decay: 350, sustain: 0, release: 220, amplitude: 60 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 96 + _comment: FX 1 (rain) + waveform: noise_tunable + amp_envelope: { attack: 55, decay: 200, sustain: 500, release: 420, amplitude: 800 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 97 + _comment: FX 2 (soundtrack) + waveform: sawtooth + amp_envelope: { attack: 110, decay: 100, sustain: 800, release: 420, amplitude: 830 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 98 + _comment: FX 3 (crystal) + waveform: sine + amp_envelope: { attack: 20, decay: 550, sustain: 80, release: 420, amplitude: 820 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 99 + _comment: FX 4 (atmosphere) + waveform: triangle + amp_envelope: { attack: 160, decay: 200, sustain: 700, release: 520, amplitude: 810 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 100 + _comment: FX 5 (brightness) + waveform: sawtooth + amp_envelope: { attack: 30, decay: 100, sustain: 800, release: 320, amplitude: 870 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 101 + _comment: FX 6 (goblins) + waveform: cycle_32 + amp_envelope: { attack: 110, decay: 100, sustain: 700, release: 420, amplitude: 840 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 3, amplitude: 18 } + - instrument: 102 + _comment: FX 7 (echoes) + waveform: sine + amp_envelope: { attack: 55, decay: 300, sustain: 400, release: 520, amplitude: 820 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 103 + _comment: FX 8 (sci-fi) + waveform: cycle_64 + amp_envelope: { attack: 85, decay: 200, sustain: 600, release: 420, amplitude: 840 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 2, amplitude: 22 } + - instrument: 104 + _comment: Sitar + waveform: sawtooth + amp_envelope: { attack: 5, decay: 550, sustain: 180, release: 280, amplitude: 860 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 5, amplitude: 12 } + - instrument: 105 + _comment: Banjo + waveform: triangle + amp_envelope: { attack: 3, decay: 380, sustain: 80, release: 180, amplitude: 870 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 106 + _comment: Shamisen + waveform: sawtooth + amp_envelope: { attack: 3, decay: 320, sustain: 80, release: 130, amplitude: 880 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 107 + _comment: Koto + waveform: triangle + amp_envelope: { attack: 5, decay: 580, sustain: 80, release: 280, amplitude: 850 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 108 + _comment: Kalimba + waveform: sine + amp_envelope: { attack: 3, decay: 650, sustain: 0, release: 180, amplitude: 840 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 109 + _comment: Bag Pipe + waveform: square_50 + amp_envelope: { attack: 50, decay: 0, sustain: 900, release: 100, amplitude: 870 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 4, amplitude: 6 } + - instrument: 110 + _comment: Fiddle + waveform: sawtooth + amp_envelope: { attack: 30, decay: 0, sustain: 900, release: 150, amplitude: 870 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 5, amplitude: 9 } + - instrument: 111 + _comment: Shanai + waveform: square_10 + amp_envelope: { attack: 20, decay: 50, sustain: 860, release: 100, amplitude: 860 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 5, amplitude: 9 } + - instrument: 112 + _comment: Tinkle Bell + waveform: sine + amp_envelope: { attack: 2, decay: 850, sustain: 0, release: 180, amplitude: 820 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 113 + _comment: Agogo + waveform: triangle + amp_envelope: { attack: 2, decay: 480, sustain: 0, release: 130, amplitude: 870 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 114 + _comment: Steel Drums + waveform: sine + amp_envelope: { attack: 5, decay: 620, sustain: 80, release: 180, amplitude: 850 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 115 + _comment: Woodblock + waveform: cycle_16 + amp_envelope: { attack: 2, decay: 90, sustain: 0, release: 40, amplitude: 900 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 116 + _comment: Taiko Drum + waveform: triangle + amp_envelope: { attack: 2, decay: 380, sustain: 0, release: 180, amplitude: 920 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 117 + _comment: Melodic Tom + waveform: triangle + amp_envelope: { attack: 3, decay: 280, sustain: 80, release: 130, amplitude: 880 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 118 + _comment: Synth Drum + waveform: square_50 + amp_envelope: { attack: 3, decay: 280, sustain: 0, release: 90, amplitude: 900 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 119 + _comment: Reverse Cymbal + waveform: noise + amp_envelope: { attack: 900, decay: 0, sustain: 900, release: 50, amplitude: 850 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 120 + _comment: Guitar Fret Noise + waveform: noise_tunable + amp_envelope: { attack: 2, decay: 90, sustain: 0, release: 25, amplitude: 600 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 121 + _comment: Breath Noise + waveform: noise_tunable + amp_envelope: { attack: 10, decay: 50, sustain: 400, release: 100, amplitude: 600 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 122 + _comment: Seashore + waveform: noise + amp_envelope: { attack: 200, decay: 0, sustain: 600, release: 400, amplitude: 550 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 123 + _comment: Bird Tweet + waveform: sine + amp_envelope: { attack: 5, decay: 100, sustain: 700, release: 100, amplitude: 700 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 9, amplitude: 35 } + - instrument: 124 + _comment: Telephone Ring + waveform: square_50 + amp_envelope: { attack: 5, decay: 0, sustain: 900, release: 50, amplitude: 850 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 2, amplitude: 900 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 125 + _comment: Helicopter + waveform: cycle_16 + amp_envelope: { attack: 100, decay: 0, sustain: 700, release: 200, amplitude: 700 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 4, amplitude: 20 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 126 + _comment: Applause + waveform: noise + amp_envelope: { attack: 220, decay: 100, sustain: 600, release: 400, amplitude: 650 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } + - instrument: 127 + _comment: Gunshot + waveform: noise + amp_envelope: { attack: 2, decay: 200, sustain: 0, release: 100, amplitude: 950 } + pitch_envelope: { attack: 0, decay: 0, sustain: 0, release: 0, amplitude: 0 } + amp_lfo: { frequency: 0, amplitude: 0 } + pitch_lfo: { frequency: 0, amplitude: 0 } +drum_instruments: + - note: 27 + _comment: High Q + start_freq: 3500 + start_vol: 900 + steps: + - { waveform: cycle_16, target_freq: 2800, target_vol: 500, duration: 5 } + - { waveform: cycle_16, target_freq: 2000, target_vol: 0, duration: 8 } + - note: 28 + _comment: Slap + start_freq: 300 + start_vol: 1000 + steps: + - { waveform: noise_tunable, target_freq: 400, target_vol: 700, duration: 12 } + - { waveform: noise_tunable, target_freq: 250, target_vol: 200, duration: 30 } + - { waveform: noise_tunable, target_freq: 150, target_vol: 0, duration: 35 } + - note: 29 + _comment: Scratch Push + start_freq: 200 + start_vol: 950 + steps: + - { waveform: noise_tunable, target_freq: 600, target_vol: 800, duration: 30 } + - { waveform: noise_tunable, target_freq: 1200, target_vol: 500, duration: 40 } + - { waveform: noise_tunable, target_freq: 2000, target_vol: 0, duration: 30 } + - note: 30 + _comment: Scratch Pull + start_freq: 2000 + start_vol: 950 + steps: + - { waveform: noise_tunable, target_freq: 1200, target_vol: 800, duration: 30 } + - { waveform: noise_tunable, target_freq: 500, target_vol: 500, duration: 40 } + - { waveform: noise_tunable, target_freq: 200, target_vol: 0, duration: 30 } + - note: 31 + _comment: Sticks + start_freq: 4000 + start_vol: 850 + steps: + - { waveform: cycle_16, target_freq: 3200, target_vol: 400, duration: 6 } + - { waveform: cycle_16, target_freq: 2400, target_vol: 0, duration: 10 } + - note: 32 + _comment: Square Click + start_freq: 1800 + start_vol: 900 + steps: + - { waveform: cycle_16, target_freq: 1400, target_vol: 500, duration: 8 } + - { waveform: cycle_16, target_freq: 1000, target_vol: 0, duration: 12 } + - note: 33 + _comment: Metronome Click + start_freq: 1400 + start_vol: 870 + steps: + - { waveform: cycle_16, target_freq: 1100, target_vol: 450, duration: 8 } + - { waveform: cycle_16, target_freq: 800, target_vol: 0, duration: 14 } + - note: 34 + _comment: Metronome Bell + start_freq: 2800 + start_vol: 870 + steps: + - { waveform: sine, target_freq: 2600, target_vol: 500, duration: 40 } + - { waveform: sine, target_freq: 2400, target_vol: 150, duration: 150 } + - { waveform: sine, target_freq: 2200, target_vol: 0, duration: 150 } + - note: 35 + _comment: Acoustic Bass Drum + start_freq: 150 + start_vol: 1024 + steps: + - { waveform: sine, target_freq: 80, target_vol: 900, duration: 30 } + - { waveform: sine, target_freq: 50, target_vol: 400, duration: 40 } + - { waveform: sine, target_freq: 40, target_vol: 0, duration: 60 } + - note: 36 + _comment: Bass Drum 1 + start_freq: 120 + start_vol: 1024 + steps: + - { waveform: sine, target_freq: 70, target_vol: 850, duration: 25 } + - { waveform: sine, target_freq: 45, target_vol: 350, duration: 35 } + - { waveform: sine, target_freq: 35, target_vol: 0, duration: 50 } + - note: 37 + _comment: Side Stick + start_freq: 1200 + start_vol: 900 + steps: + - { waveform: cycle_16, target_freq: 900, target_vol: 400, duration: 15 } + - { waveform: cycle_16, target_freq: 500, target_vol: 0, duration: 25 } + - note: 38 + _comment: Acoustic Snare + start_freq: 800 + start_vol: 1024 + steps: + - { waveform: noise, target_freq: 800, target_vol: 700, duration: 18 } + - { waveform: noise, target_freq: 800, target_vol: 300, duration: 35 } + - { waveform: noise, target_freq: 800, target_vol: 0, duration: 40 } + - note: 39 + _comment: Hand Clap + start_freq: 2000 + start_vol: 1024 + steps: + - { waveform: noise, target_freq: 2000, target_vol: 600, duration: 12 } + - { waveform: noise, target_freq: 2000, target_vol: 0, duration: 25 } + - note: 40 + _comment: Electric Snare + start_freq: 1000 + start_vol: 1000 + steps: + - { waveform: noise, target_freq: 1000, target_vol: 600, duration: 14 } + - { waveform: noise, target_freq: 1000, target_vol: 200, duration: 28 } + - { waveform: noise, target_freq: 1000, target_vol: 0, duration: 30 } + - note: 41 + _comment: Low Floor Tom + start_freq: 120 + start_vol: 1000 + steps: + - { waveform: triangle, target_freq: 80, target_vol: 700, duration: 40 } + - { waveform: triangle, target_freq: 60, target_vol: 200, duration: 80 } + - { waveform: triangle, target_freq: 50, target_vol: 0, duration: 80 } + - note: 42 + _comment: Closed Hi-Hat + start_freq: 8000 + start_vol: 800 + steps: + - { waveform: noise, target_freq: 8000, target_vol: 400, duration: 8 } + - { waveform: noise, target_freq: 8000, target_vol: 0, duration: 12 } + - note: 43 + _comment: High Floor Tom + start_freq: 150 + start_vol: 1000 + steps: + - { waveform: triangle, target_freq: 100, target_vol: 700, duration: 35 } + - { waveform: triangle, target_freq: 75, target_vol: 200, duration: 70 } + - { waveform: triangle, target_freq: 60, target_vol: 0, duration: 70 } + - note: 44 + _comment: Pedal Hi-Hat + start_freq: 7000 + start_vol: 700 + steps: + - { waveform: noise, target_freq: 7000, target_vol: 280, duration: 7 } + - { waveform: noise, target_freq: 7000, target_vol: 0, duration: 10 } + - note: 45 + _comment: Low Tom + start_freq: 180 + start_vol: 1000 + steps: + - { waveform: triangle, target_freq: 120, target_vol: 700, duration: 30 } + - { waveform: triangle, target_freq: 90, target_vol: 200, duration: 60 } + - { waveform: triangle, target_freq: 70, target_vol: 0, duration: 60 } + - note: 46 + _comment: Open Hi-Hat + start_freq: 8000 + start_vol: 800 + steps: + - { waveform: noise, target_freq: 8000, target_vol: 600, duration: 45 } + - { waveform: noise, target_freq: 8000, target_vol: 200, duration: 90 } + - { waveform: noise, target_freq: 8000, target_vol: 0, duration: 100 } + - note: 47 + _comment: Low-Mid Tom + start_freq: 220 + start_vol: 1000 + steps: + - { waveform: triangle, target_freq: 150, target_vol: 700, duration: 28 } + - { waveform: triangle, target_freq: 110, target_vol: 200, duration: 55 } + - { waveform: triangle, target_freq: 85, target_vol: 0, duration: 55 } + - note: 48 + _comment: Hi-Mid Tom + start_freq: 280 + start_vol: 1000 + steps: + - { waveform: triangle, target_freq: 190, target_vol: 700, duration: 25 } + - { waveform: triangle, target_freq: 140, target_vol: 200, duration: 50 } + - { waveform: triangle, target_freq: 110, target_vol: 0, duration: 50 } + - note: 49 + _comment: Crash Cymbal 1 + start_freq: 6000 + start_vol: 1000 + steps: + - { waveform: noise, target_freq: 6000, target_vol: 700, duration: 70 } + - { waveform: noise, target_freq: 6000, target_vol: 300, duration: 200 } + - { waveform: noise, target_freq: 6000, target_vol: 0, duration: 300 } + - note: 50 + _comment: High Tom + start_freq: 350 + start_vol: 1000 + steps: + - { waveform: triangle, target_freq: 240, target_vol: 700, duration: 20 } + - { waveform: triangle, target_freq: 180, target_vol: 200, duration: 40 } + - { waveform: triangle, target_freq: 140, target_vol: 0, duration: 40 } + - note: 51 + _comment: Ride Cymbal 1 + start_freq: 5000 + start_vol: 700 + steps: + - { waveform: noise, target_freq: 5000, target_vol: 380, duration: 35 } + - { waveform: noise, target_freq: 5000, target_vol: 100, duration: 75 } + - { waveform: noise, target_freq: 5000, target_vol: 0, duration: 100 } + - note: 52 + _comment: Chinese Cymbal + start_freq: 7000 + start_vol: 950 + steps: + - { waveform: noise, target_freq: 7000, target_vol: 700, duration: 45 } + - { waveform: noise, target_freq: 7000, target_vol: 400, duration: 150 } + - { waveform: noise, target_freq: 7000, target_vol: 0, duration: 200 } + - note: 53 + _comment: Ride Bell + start_freq: 3000 + start_vol: 800 + steps: + - { waveform: sine, target_freq: 2800, target_vol: 500, duration: 70 } + - { waveform: sine, target_freq: 2600, target_vol: 100, duration: 180 } + - { waveform: sine, target_freq: 2400, target_vol: 0, duration: 200 } + - note: 54 + _comment: Tambourine + start_freq: 5000 + start_vol: 800 + steps: + - { waveform: noise, target_freq: 5000, target_vol: 400, duration: 18 } + - { waveform: noise, target_freq: 5000, target_vol: 0, duration: 28 } + - note: 55 + _comment: Splash Cymbal + start_freq: 6500 + start_vol: 900 + steps: + - { waveform: noise, target_freq: 6500, target_vol: 600, duration: 25 } + - { waveform: noise, target_freq: 6500, target_vol: 200, duration: 70 } + - { waveform: noise, target_freq: 6500, target_vol: 0, duration: 80 } + - note: 56 + _comment: Cowbell + start_freq: 800 + start_vol: 900 + steps: + - { waveform: cycle_16, target_freq: 700, target_vol: 600, duration: 45 } + - { waveform: cycle_16, target_freq: 600, target_vol: 200, duration: 90 } + - { waveform: cycle_16, target_freq: 550, target_vol: 0, duration: 100 } + - note: 57 + _comment: Crash Cymbal 2 + start_freq: 5500 + start_vol: 1000 + steps: + - { waveform: noise, target_freq: 5500, target_vol: 700, duration: 70 } + - { waveform: noise, target_freq: 5500, target_vol: 300, duration: 200 } + - { waveform: noise, target_freq: 5500, target_vol: 0, duration: 300 } + - note: 58 + _comment: Vibraslap + start_freq: 400 + start_vol: 900 + steps: + - { waveform: triangle, target_freq: 300, target_vol: 600, duration: 25 } + - { waveform: noise_tunable, target_freq: 300, target_vol: 300, duration: 90 } + - { waveform: noise_tunable, target_freq: 200, target_vol: 0, duration: 100 } + - note: 59 + _comment: Ride Cymbal 2 + start_freq: 4500 + start_vol: 700 + steps: + - { waveform: noise, target_freq: 4500, target_vol: 380, duration: 38 } + - { waveform: noise, target_freq: 4500, target_vol: 100, duration: 75 } + - { waveform: noise, target_freq: 4500, target_vol: 0, duration: 100 } + - note: 60 + _comment: Hi Bongo + start_freq: 500 + start_vol: 950 + steps: + - { waveform: triangle, target_freq: 380, target_vol: 600, duration: 18 } + - { waveform: triangle, target_freq: 280, target_vol: 200, duration: 38 } + - { waveform: triangle, target_freq: 220, target_vol: 0, duration: 40 } + - note: 61 + _comment: Low Bongo + start_freq: 350 + start_vol: 950 + steps: + - { waveform: triangle, target_freq: 260, target_vol: 600, duration: 22 } + - { waveform: triangle, target_freq: 190, target_vol: 200, duration: 45 } + - { waveform: triangle, target_freq: 150, target_vol: 0, duration: 50 } + - note: 62 + _comment: Mute Hi Conga + start_freq: 600 + start_vol: 900 + steps: + - { waveform: triangle, target_freq: 420, target_vol: 400, duration: 12 } + - { waveform: triangle, target_freq: 320, target_vol: 0, duration: 22 } + - note: 63 + _comment: Open Hi Conga + start_freq: 600 + start_vol: 900 + steps: + - { waveform: triangle, target_freq: 520, target_vol: 600, duration: 28 } + - { waveform: triangle, target_freq: 420, target_vol: 200, duration: 55 } + - { waveform: triangle, target_freq: 320, target_vol: 0, duration: 60 } + - note: 64 + _comment: Low Conga + start_freq: 400 + start_vol: 900 + steps: + - { waveform: triangle, target_freq: 330, target_vol: 600, duration: 32 } + - { waveform: triangle, target_freq: 270, target_vol: 200, duration: 65 } + - { waveform: triangle, target_freq: 210, target_vol: 0, duration: 70 } + - note: 65 + _comment: High Timbale + start_freq: 450 + start_vol: 900 + steps: + - { waveform: triangle, target_freq: 320, target_vol: 500, duration: 18 } + - { waveform: noise_tunable, target_freq: 300, target_vol: 200, duration: 38 } + - { waveform: noise_tunable, target_freq: 200, target_vol: 0, duration: 40 } + - note: 66 + _comment: Low Timbale + start_freq: 280 + start_vol: 900 + steps: + - { waveform: triangle, target_freq: 210, target_vol: 500, duration: 22 } + - { waveform: noise_tunable, target_freq: 200, target_vol: 200, duration: 48 } + - { waveform: noise_tunable, target_freq: 120, target_vol: 0, duration: 50 } + - note: 67 + _comment: High Agogo + start_freq: 1200 + start_vol: 850 + steps: + - { waveform: triangle, target_freq: 1050, target_vol: 400, duration: 28 } + - { waveform: triangle, target_freq: 850, target_vol: 0, duration: 55 } + - note: 68 + _comment: Low Agogo + start_freq: 900 + start_vol: 850 + steps: + - { waveform: triangle, target_freq: 780, target_vol: 400, duration: 32 } + - { waveform: triangle, target_freq: 620, target_vol: 0, duration: 65 } + - note: 69 + _comment: Cabasa + start_freq: 4000 + start_vol: 700 + steps: + - { waveform: noise, target_freq: 4000, target_vol: 300, duration: 14 } + - { waveform: noise, target_freq: 4000, target_vol: 0, duration: 28 } + - note: 70 + _comment: Maracas + start_freq: 5000 + start_vol: 700 + steps: + - { waveform: noise, target_freq: 5000, target_vol: 280, duration: 9 } + - { waveform: noise, target_freq: 5000, target_vol: 0, duration: 18 } + - note: 75 + _comment: Claves + start_freq: 1500 + start_vol: 900 + steps: + - { waveform: cycle_16, target_freq: 1300, target_vol: 400, duration: 12 } + - { waveform: cycle_16, target_freq: 1000, target_vol: 0, duration: 25 } + - note: 76 + _comment: Hi Wood Block + start_freq: 1800 + start_vol: 900 + steps: + - { waveform: cycle_16, target_freq: 1500, target_vol: 400, duration: 12 } + - { waveform: cycle_16, target_freq: 1100, target_vol: 0, duration: 22 } + - note: 77 + _comment: Low Wood Block + start_freq: 1200 + start_vol: 900 + steps: + - { waveform: cycle_16, target_freq: 1000, target_vol: 400, duration: 15 } + - { waveform: cycle_16, target_freq: 750, target_vol: 0, duration: 28 } + - note: 78 + _comment: Mute Cuica + start_freq: 600 + start_vol: 950 + steps: + - { waveform: noise_tunable, target_freq: 450, target_vol: 700, duration: 20 } + - { waveform: noise_tunable, target_freq: 300, target_vol: 200, duration: 40 } + - { waveform: noise_tunable, target_freq: 200, target_vol: 0, duration: 35 } + - note: 79 + _comment: Open Cuica + start_freq: 500 + start_vol: 950 + steps: + - { waveform: noise_tunable, target_freq: 900, target_vol: 800, duration: 35 } + - { waveform: noise_tunable, target_freq: 700, target_vol: 500, duration: 80 } + - { waveform: noise_tunable, target_freq: 500, target_vol: 100, duration: 100 } + - { waveform: noise_tunable, target_freq: 350, target_vol: 0, duration: 80 } + - note: 80 + _comment: Mute Triangle + start_freq: 4000 + start_vol: 800 + steps: + - { waveform: sine, target_freq: 3800, target_vol: 300, duration: 18 } + - { waveform: sine, target_freq: 3600, target_vol: 0, duration: 28 } + - note: 81 + _comment: Open Triangle + start_freq: 4000 + start_vol: 800 + steps: + - { waveform: sine, target_freq: 3900, target_vol: 500, duration: 90 } + - { waveform: sine, target_freq: 3750, target_vol: 200, duration: 280 } + - { waveform: sine, target_freq: 3600, target_vol: 0, duration: 300 } + - note: 82 + _comment: Shaker + start_freq: 5000 + start_vol: 750 + steps: + - { waveform: noise, target_freq: 5000, target_vol: 550, duration: 18 } + - { waveform: noise, target_freq: 5000, target_vol: 200, duration: 35 } + - { waveform: noise, target_freq: 5000, target_vol: 0, duration: 30 } + - note: 83 + _comment: Hi Half-Open Hi-Hat + start_freq: 8000 + start_vol: 800 + steps: + - { waveform: noise, target_freq: 8000, target_vol: 500, duration: 18 } + - { waveform: noise, target_freq: 8000, target_vol: 150, duration: 45 } + - { waveform: noise, target_freq: 8000, target_vol: 0, duration: 45 } + - note: 84 + _comment: High Hi-Hat + start_freq: 9000 + start_vol: 780 + steps: + - { waveform: noise, target_freq: 9000, target_vol: 550, duration: 40 } + - { waveform: noise, target_freq: 9000, target_vol: 180, duration: 80 } + - { waveform: noise, target_freq: 9000, target_vol: 0, duration: 90 } + - note: 85 + _comment: Crash Cymbal 3 + start_freq: 6500 + start_vol: 1000 + steps: + - { waveform: noise, target_freq: 6500, target_vol: 700, duration: 70 } + - { waveform: noise, target_freq: 6500, target_vol: 300, duration: 200 } + - { waveform: noise, target_freq: 6500, target_vol: 0, duration: 300 } + - note: 86 + _comment: Ride Cymbal 3 + start_freq: 5500 + start_vol: 720 + steps: + - { waveform: noise, target_freq: 5500, target_vol: 420, duration: 40 } + - { waveform: noise, target_freq: 5500, target_vol: 120, duration: 80 } + - { waveform: noise, target_freq: 5500, target_vol: 0, duration: 110 } + - note: 87 + _comment: Open Hi-Hat 2 + start_freq: 8000 + start_vol: 820 + steps: + - { waveform: noise, target_freq: 8000, target_vol: 650, duration: 60 } + - { waveform: noise, target_freq: 8000, target_vol: 250, duration: 130 } + - { waveform: noise, target_freq: 8000, target_vol: 0, duration: 140 } diff --git a/requirements.txt b/requirements.txt index eb8fd82..e2e3a44 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ mido -tqdm +pyyaml diff --git a/src/arcade/__init__.py b/src/arcade/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/arcade/music.py b/src/arcade/music.py index c7a571d..b27b1d0 100644 --- a/src/arcade/music.py +++ b/src/arcade/music.py @@ -1,246 +1,995 @@ # https://github.com/microsoft/pxt/blob/master/pxtlib/music.ts +import struct +from typing import Tuple -import logging -from dataclasses import dataclass -from enum import Enum -from struct import pack -from typing import List, Optional - +from arcade.music_types import * from utils.logger import create_logger logger = create_logger(name=__name__, level=logging.INFO) -@dataclass -class Envelope: - attack: int - decay: int - sustain: int - release: int - amplitude: int +def set_8_bit_number(buf: bytearray, offset: int, value: int): + struct.pack_into(" int: + return struct.unpack_from(" int: + return struct.unpack_from(" str: + """ + Encode a MakeCode Arcade song into a hex string. Surround this hex string with + hex` and ` to create a valid MakeCode Arcade TypeScript buffer for MakeCode Arcade + to use. + :param song: The MakeCode Arcade song. + :return: A hex representation of the bytearray that is generated by + `encode_song(song)`. + """ + encoded = encode_song(song) + return "".join(f"{byte:02X}" for byte in encoded) -@dataclass -class Note: - note: int - enharmonicSpelling: EnharmonicSpelling +def encode_song(song: Song) -> bytearray: + """ + Encode a MakeCode Arcade song into a bytearray. Ported from + https://github.com/microsoft/pxt/blob/master/pxtlib/music.ts#L96 -@dataclass -class NoteEvent: - notes: List[Note] - startTick: int - endTick: int + :param song: The MakeCode Arcade song. + :return: A bytearray, convert this to hex to use in a MakeCode Arcade program. + """ + encoded_tracks = [encode_track(track) for track in song.tracks if + len(track.notes) > 0] + encoded_track_velocities: List[bytearray] = list(filter(lambda v: v is not None, + [encode_track_velocity( + track) + for track in + song.tracks])) + track_length = sum(len(c) for c in (encoded_tracks + encoded_track_velocities)) -@dataclass -class DrumSoundStep: - waveform: int - frequency: int - volume: int - duration: int + out = bytearray(7 + track_length) + set_8_bit_number(out, 0, 0) # encoding version + set_16_bit_number(out, 1, song.beats_per_minute) + set_8_bit_number(out, 3, song.beats_per_measure) + set_8_bit_number(out, 4, song.ticks_per_beat) + set_8_bit_number(out, 5, song.measures) + set_8_bit_number(out, 6, len(encoded_tracks)) + current = 7 + for track in encoded_tracks: + out[current:current + len(track)] = track + current += len(track) -@dataclass -class DrumInstrument: - startFrequency: int - startVolume: int - steps: List[DrumSoundStep] + for trackVelocity in encoded_track_velocities: + out[current:current + len(trackVelocity)] = trackVelocity + current += len(trackVelocity) + return out -@dataclass -class Track: - instrument: Instrument - id: int - notes: List[NoteEvent] - name: Optional[str] = None - iconURI: Optional[str] = None - drums: Optional[List[DrumInstrument]] = None +def encode_instrument(instrument: Instrument) -> bytearray: + """ + Encode a MakeCode Arcade instrument into a bytearray. Ported from + https://github.com/microsoft/pxt/blob/master/pxtlib/music.ts#L127 + + :param instrument: The MakeCode Arcade instrument. + :return: A bytearray. + """ + out = bytearray(28) + set_8_bit_number(out, 0, instrument.waveform) + set_16_bit_number(out, 1, instrument.amp_envelope.attack) + set_16_bit_number(out, 3, instrument.amp_envelope.decay) + set_16_bit_number(out, 5, instrument.amp_envelope.sustain) + set_16_bit_number(out, 7, instrument.amp_envelope.release) + set_16_bit_number(out, 9, instrument.amp_envelope.amplitude) + if instrument.pitch_envelope is not None: + set_16_bit_number(out, 11, instrument.pitch_envelope.attack) + set_16_bit_number(out, 13, instrument.pitch_envelope.decay) + set_16_bit_number(out, 15, instrument.pitch_envelope.sustain) + set_16_bit_number(out, 17, instrument.pitch_envelope.release) + set_16_bit_number(out, 19, instrument.pitch_envelope.amplitude) + else: + set_16_bit_number(out, 11, 0) + set_16_bit_number(out, 13, 0) + set_16_bit_number(out, 15, 0) + set_16_bit_number(out, 17, 0) + set_16_bit_number(out, 19, 0) + if instrument.amp_lfo is not None: + set_8_bit_number(out, 21, instrument.amp_lfo.frequency) + set_16_bit_number(out, 22, instrument.amp_lfo.amplitude) + else: + set_8_bit_number(out, 21, 0) + set_16_bit_number(out, 22, 0) + if instrument.pitch_lfo is not None: + set_8_bit_number(out, 24, instrument.pitch_lfo.frequency) + set_16_bit_number(out, 25, instrument.pitch_lfo.amplitude) + else: + set_8_bit_number(out, 24, 0) + set_16_bit_number(out, 25, 0) + set_8_bit_number(out, 27, instrument.octave if instrument.octave is not None else 0) + return out -@dataclass -class Song(SongInfo): - tracks: List[Track] +def encode_drum_instrument(drum: DrumInstrument) -> bytearray: + """ + Encode a MakeCode Arcade drum instrument into a bytearray. Ported from + https://github.com/microsoft/pxt/blob/master/pxtlib/music.ts#L149 + + :param drum: The MakeCode Arcade drum instrument. + :return: A bytearray. + """ + out = bytearray(5 + 7 * len(drum.steps)) + set_8_bit_number(out, 0, len(drum.steps)) + set_16_bit_number(out, 1, drum.start_frequency) + set_16_bit_number(out, 3, drum.start_volume) + for i, step in enumerate(drum.steps): + start = 5 + i * 7 + set_8_bit_number(out, start, step.waveform) + set_16_bit_number(out, start + 1, step.frequency) + set_16_bit_number(out, start + 3, step.volume) + set_16_bit_number(out, start + 5, step.duration) + return out -def get8BitNumber(num: Optional[int]) -> bytes: - return bytes([0 if num is None else num]) +def encode_note_event(event: NoteEvent, instrument_octave: int, + is_drum_track: bool) -> bytearray: + """ + Encode a MakeCode Arcade note event into a bytearray. Ported from + https://github.com/microsoft/pxt/blob/master/pxtlib/music.ts#L166 -def get16BitNumber(num: Optional[int]) -> bytes: - return pack(" bytes: - if isDrumTrack: - return bytes([note.note]) + return out + + +def encode_note(note: Note, instrument_octave: int, is_drum_track: bool) -> int: + """ + Encode a MakeCode Arcade note into a single byte. Ported from + https://github.com/microsoft/pxt/blob/master/pxtlib/music.ts#L179 + + :param note: The MakeCode Arcade `Note`. + :param instrument_octave: The instrument octave offset used for the note. + :param is_drum_track: Whether this note is for a drum track or not. + :return: An int, which will fit into a single byte. + """ + if is_drum_track: + return note.note flags = 0 - if note.enharmonicSpelling == EnharmonicSpelling.FLAT: + if note.enharmonic_spelling == EnharmonicSpelling.FLAT: flags = 1 - elif note.enharmonicSpelling == EnharmonicSpelling.SHARP: + elif note.enharmonic_spelling == EnharmonicSpelling.SHARP: flags = 2 - note_val = (note.note - (instrumentOctave - 2) * 12) - note_val += 1 - 12 - byte_val = note_val | (flags << 6) + return (note.note - (instrument_octave - 2) * 12) | (flags << 6) + - if note_val > 63: - logger.warning( - f"Note {note.note} exceeds track range, skipping note!") - return bytes([]) +def encode_track(track: Track) -> bytearray: + """ + Encode a MakeCode Arcade track into a bytearray. Ported from + https://github.com/microsoft/pxt/blob/master/pxtlib/music.ts#L195 + + :param track: The MakeCode Arcade `Track`. + :return: A bytearray. + """ + if track.drums: + return encode_drum_track(track) else: - try: - return bytes([byte_val]) - except ValueError: - logger.warning(f"Note {note.note} generates invalid byte value " - f"{byte_val}, skipping note!") - return bytes([]) - - -def encodeNoteEvent(event: NoteEvent, instrumentOctave: int, - isDrumTrack: bool) -> bytes: - out = bytearray() - out += get16BitNumber(event.startTick) - out += get16BitNumber(event.endTick) - out.append(len(event.notes)) - for note in event.notes: - out += encodeNote(note, instrumentOctave, isDrumTrack) + return encode_melodic_track(track) + + +def encode_track_velocity(track: Track) -> Optional[bytearray]: + """ + Encode a MakeCode Arcade track's velocity data into a bytearray. Ported from + https://github.com/microsoft/pxt/blob/master/pxtlib/music.ts#L200 + + :param track: The MakeCode Arcade `Track`. + :return: A bytearray. + """ + if not any([note.velocity is not None and note.velocity < 128 for note in + track.notes]): + return None + + out = bytearray(1 + len(track.notes)) + set_8_bit_number(out, 0, track.id) + for i, note in enumerate(track.notes): + set_8_bit_number(out, 1 + i, note.velocity if note.velocity is not None else 0) return out -def encodeInstrument(instrument: Instrument) -> bytes: - out = bytearray() - out.append(instrument.waveform) - out += get16BitNumber(instrument.ampEnvelope.attack) - out += get16BitNumber(instrument.ampEnvelope.decay) - out += get16BitNumber(instrument.ampEnvelope.sustain) - out += get16BitNumber(instrument.ampEnvelope.release) - out += get16BitNumber(instrument.ampEnvelope.amplitude) - if instrument.pitchEnvelope is None: - for _ in range(5): - out += get16BitNumber(0) - else: - out += get16BitNumber(instrument.pitchEnvelope.attack) - out += get16BitNumber(instrument.pitchEnvelope.decay) - out += get16BitNumber(instrument.pitchEnvelope.sustain) - out += get16BitNumber(instrument.pitchEnvelope.release) - out += get16BitNumber(instrument.pitchEnvelope.amplitude) - out.append(0 if instrument.ampLFO is None else instrument.ampLFO.frequency) - out += get16BitNumber( - 0 if instrument.ampLFO is None else instrument.ampLFO.amplitude) - out.append( - 0 if instrument.pitchLFO is None else instrument.pitchLFO.frequency) - out += get16BitNumber( - 0 if instrument.pitchLFO is None else instrument.pitchLFO.amplitude) - out.append(instrument.octave) +def encode_melodic_track(track: Track) -> bytearray: + """ + Encode a MakeCode Arcade melodic track into a bytearray. Ported from + https://github.com/microsoft/pxt/blob/master/pxtlib/music.ts#L211 + + :param track: The MakeCode Arcade `Track`, must be melodic. + :return: A bytearray. + """ + encoded_instrument = encode_instrument(track.instrument) + encoded_notes = [encode_note_event(note, track.instrument.octave, False) for note in + track.notes] + note_length = sum(len(c) for c in encoded_notes) + + out = bytearray(6 + len(encoded_instrument) + note_length) + set_8_bit_number(out, 0, track.id) + set_8_bit_number(out, 1, 0) + + set_16_bit_number(out, 2, len(encoded_instrument)) + current = 4 + out[current:current + len(encoded_instrument)] = encoded_instrument + current += len(encoded_instrument) + + set_16_bit_number(out, current, note_length) + current += 2 + for note in encoded_notes: + out[current:current + len(note)] = note + current += len(note) + return out -def encodeMelodicTrack(track: Track) -> bytes: - encodedInstrument = encodeInstrument(track.instrument) - encodedNotes = [ - encodeNoteEvent(n, track.instrument.octave, False) for n in track.notes - ] - noteLength = sum([len(e) for e in encodedNotes]) - - out = bytearray() - out.append(track.id) - out.append(0) - out += get16BitNumber(len(encodedInstrument)) - out += encodedInstrument - out += get16BitNumber(noteLength) - for note in encodedNotes: - out += note +def encode_drum_track(track: Track) -> bytearray: + """ + Encode a MakeCode Arcade drum track into a bytearray. Ported from + https://github.com/microsoft/pxt/blob/master/pxtlib/music.ts#L235 + + :param track: The MakeCode Arcade `Track`, must be a drum track. + :return: A bytearray. + """ + assert track.drums is not None + encoded_drums = [encode_drum_instrument(drum) for drum in track.drums] + drum_length = sum(len(c) for c in encoded_drums) + + encoded_notes = [encode_note_event(note, 0, True) for note in track.notes] + note_length = sum(len(c) for c in encoded_notes) + + out = bytearray(6 + drum_length + note_length) + set_8_bit_number(out, 0, track.id) + set_8_bit_number(out, 1, 1) + set_16_bit_number(out, 2, drum_length) + current = 4 + + for drum in encoded_drums: + out[current:current + len(drum)] = drum + current += len(drum) + + set_16_bit_number(out, current, note_length) + current += 2 + for note in encoded_notes: + out[current:current + len(note)] = note + current += len(note) + return out -def encodeTrack(track: Track) -> bytes: - if track.drums is not None: - raise NotImplementedError - # return encodeDrumTrack(track) +def decode_song_from_hex(hex: str) -> Song: + """ + Decode a MakeCode Arcade song from a hex string. The parts "hex`" and "`" before and + after a typical MakeCode Arcade TypeScript buffer must have been removed before + using this function. + + :param hex: The bare hex buffer of the MakeCode Arcade song. + :return: A MakeCode Arcade `Song` that is generated by `decode_song(buf)`. + """ + buf = bytearray.fromhex(hex) + return decode_song(buf) + + +def decode_song(buf: bytearray) -> Song: + """ + Decode a MakeCode Arcade song from a bytearray. Ported from + https://github.com/microsoft/pxt/blob/master/pxtlib/music.ts#L269 + + :param buf: A bytearray of an entire song. + :return: A MakeCode Arcade `Song`. + """ + res = Song(beats_per_minute=get_16_bit_number(buf, 1), + beats_per_measure=get_8_bit_number(buf, 3), + ticks_per_beat=get_8_bit_number(buf, 4), + measures=get_8_bit_number(buf, 5), + tracks=[]) + + num_tracks = get_8_bit_number(buf, 6) + current = 7 + + for _ in range(num_tracks): + track, pointer = decode_track(buf, current) + current = pointer + res.tracks.append(track) + + while current < len(buf): + current = decode_track_velocity(buf, res.tracks, current) + + return res + + +def decode_instrument(buf: bytearray, offset: int) -> Instrument: + """ + Decode a MakeCode Arcade instrument from a bytearray. Ported from + https://github.com/microsoft/pxt/blob/master/pxtlib/music.ts#L294 + + :param buf: A bytearray of an entire song. + :param offset: The offset in the bytearray which to start reading the instrument + data from. + :return: A MakeCode Arcade `Instrument`. + """ + return Instrument( + waveform=get_8_bit_number(buf, offset), + amp_envelope=Envelope( + attack=get_16_bit_number(buf, offset + 1), + decay=get_16_bit_number(buf, offset + 3), + sustain=get_16_bit_number(buf, offset + 5), + release=get_16_bit_number(buf, offset + 7), + amplitude=get_16_bit_number(buf, offset + 9), + ), + pitch_envelope=Envelope( + attack=get_16_bit_number(buf, offset + 11), + decay=get_16_bit_number(buf, offset + 13), + sustain=get_16_bit_number(buf, offset + 15), + release=get_16_bit_number(buf, offset + 17), + amplitude=get_16_bit_number(buf, offset + 19), + ), + amp_lfo=LFO( + frequency=get_8_bit_number(buf, offset + 21), + amplitude=get_16_bit_number(buf, offset + 22), + # the original implementation is 22 instead of offset + 22 + ), + pitch_lfo=LFO( + frequency=get_8_bit_number(buf, offset + 24), + amplitude=get_16_bit_number(buf, offset + 25), + # the original implementation is 25 instead of offset + 25 + ), + octave=get_8_bit_number(buf, offset + 27), + ) + + +def decode_track(buf: bytearray, offset: int) -> Tuple[Track, int]: + """ + Decode a MakeCode Arcade track from a bytearray. Ported from + https://github.com/microsoft/pxt/blob/master/pxtlib/music.ts#L323 + + :param buf: A bytearray of an entire song. + :param offset: The offset in the bytearray which to start reading the track + data from. + :return: A MakeCode Arcade `Track`. + """ + if get_8_bit_number(buf, offset + 1) != 0: + return decode_drum_track(buf, offset) else: - return encodeMelodicTrack(track) + return decode_melodic_track(buf, offset) + + +def decode_track_velocity(buf: bytearray, tracks: List[Track], offset: int) -> int: + """ + Decode a MakeCode Arcade track velocity from a bytearray. Ported from + https://github.com/microsoft/pxt/blob/master/pxtlib/music.ts#L331 + + :param buf: A bytearray of an entire song. + :param tracks: A list of `Track` objects which have already been decoded from the + bytearray, the one with the matching ID will have the note velocities set. + :param offset: The offset in the bytearray which to start reading the track velocity + data from. + :return: The next offset after the end of this track velocity data. + """ + track_id = get_8_bit_number(buf, offset) + track = next((t for t in tracks if t.id == track_id), None) + if track is None: + raise ValueError(f"Track with {track_id} not found") + for i in range(len(track.notes)): + track.notes[i].velocity = get_8_bit_number(buf, offset + i + 1) + return offset + len(track.notes) + 1 + + +def decode_drum_instrument(buf: bytearray, offset: int) -> DrumInstrument: + """ + Decode a MakeCode Arcade drum instrument from a bytearray. Ported from + https://github.com/microsoft/pxt/blob/master/pxtlib/music.ts#L341 + + :param buf: A bytearray of an entire song. + :param offset: The offset in the bytearray which to start reading the drum + instrument data from. + :return: A MakeCode Arcade `DrumInstrument`. + """ + res = DrumInstrument( + start_frequency=get_16_bit_number(buf, offset + 1), + start_volume=get_16_bit_number(buf, offset + 3), + steps=[], + ) + for i in range(get_8_bit_number(buf, offset)): + start = offset + 5 + i * 7 + res.steps.append(DrumSoundStep( + waveform=get_8_bit_number(buf, start), + frequency=get_16_bit_number(buf, start + 1), + volume=get_16_bit_number(buf, start + 3), + duration=get_16_bit_number(buf, start + 5) + )) + + return res + + +def decode_note_event(buf: bytearray, offset: int, instrument_octave: int, + is_drum_track: bool) -> NoteEvent: + """ + Decode a MakeCode Arcade note event from a bytearray. Ported from + https://github.com/microsoft/pxt/blob/master/pxtlib/music.ts#L361 + + :param buf: A bytearray of an entire song. + :param offset: The offset in the bytearray which to start reading the note event + data from. + :param instrument_octave: The instrument's octave offset used for the note event, + needed to decode the note value itself. + :param is_drum_track: Whether this note event is for a drum track or not, needed to + decode the note. + :return: A MakeCode Arcade `NoteEvent`. + """ + res = NoteEvent( + start_tick=get_16_bit_number(buf, offset), + end_tick=get_16_bit_number(buf, offset + 2), + notes=[], + ) -def encodeSong(song: Song) -> bytes: - encodedTracks = list( - map( - encodeTrack, - filter( - lambda track: len(track.notes) > 0, - song.tracks + for i in range(get_8_bit_number(buf, offset + 4)): + res.notes.append( + decode_note( + get_8_bit_number(buf, offset + 5 + i), + instrument_octave, + is_drum_track ) ) + + return res + + +def decode_note(note: int, instrument_octave: int, is_drum_track: bool) -> Note: + """ + Construct a MakeCode Arcade note from specified parameters. Ported from + https://github.com/microsoft/pxt/blob/master/pxtlib/music.ts#L374 + + :param note: The note number itself, between 0 and 63. For melodic tracks, the + instrument octave is taken into account. + :param instrument_octave: The track's instrument's octave offset. + :param is_drum_track: Whether this note is for a drum track or not. + :return: A MakeCode Arcade `Note`. + """ + flags = note >> 6 + res = Note( + note=note if is_drum_track else ((note & 0x3F) + (instrument_octave - 2) * 12), + enharmonic_spelling=EnharmonicSpelling.NORMAL ) - out = bytearray() - out.append(0) - out += get16BitNumber(song.beatsPerMinute) - out.append(song.beatsPerMeasure) - out.append(song.ticksPerBeat) - out.append(song.measures) - out.append(len(encodedTracks)) - for track in encodedTracks: - out += track - return out + if flags == 1: + res.enharmonic_spelling = EnharmonicSpelling.FLAT + elif flags == 2: + res.enharmonic_spelling = EnharmonicSpelling.SHARP + + return res + + +def decode_melodic_track(buf: bytearray, offset: int) -> Tuple[Track, int]: + """ + Decode a MakeCode Arcade melodic track from a bytearray. Ported from + https://github.com/microsoft/pxt/blob/master/pxtlib/music.ts#L392 + + :param buf: A bytearray of an entire song. + :param offset: The offset in the bytearray which to start reading the melodic track + data from. + :return: A tuple of a MakeCode Arcade `Track` and the next offset after the end of + this track data. + """ + res = Track( + id=get_8_bit_number(buf, offset), + instrument=decode_instrument(buf, offset + 4), + notes=[] + ) + + note_start = offset + 4 + get_16_bit_number(buf, offset + 2) + note_length = get_16_bit_number(buf, note_start) + current_offset = note_start + 2 + + while current_offset < note_start + 2 + note_length: + res.notes.append( + decode_note_event(buf, current_offset, res.instrument.octave, False) + ) + current_offset += 5 + len(res.notes[-1].notes) + + return res, current_offset + + +def decode_drum_track(buf: bytearray, offset: int) -> Tuple[Track, int]: + """ + Decode a MakeCode Arcade drum track from a bytearray. Ported from + https://github.com/microsoft/pxt/blob/master/pxtlib/music.ts#L412 + + :param buf: A bytearray of an entire song. + :param offset: The offset in the bytearray which to start reading the drum track + data from. + :return: A tuple of a MakeCode Arcade `Track` and the next offset after the end of + this track data. + """ + res = Track( + id=get_8_bit_number(buf, offset), + instrument=Instrument( + amp_envelope=Envelope(attack=0, decay=0, sustain=0, release=0, amplitude=0), + waveform=0, octave=0), + notes=[], + drums=[] + ) + + drum_byte_length = get_16_bit_number(buf, offset + 2) + current_offset = offset + 4 + + while current_offset < (offset + 4 + drum_byte_length): + res.drums.append(decode_drum_instrument(buf, current_offset)) + current_offset += 5 + 7 * len(res.drums[-1].steps) + + note_length = get_16_bit_number(buf, current_offset) + current_offset += 2 + + while current_offset < (offset + 4 + drum_byte_length + note_length): + res.notes.append(decode_note_event(buf, current_offset, 0, True)) + current_offset += 5 + len(res.notes[-1].notes) + + return res, current_offset + + +def get_empty_song(measures: int) -> Song: + """ + Generate an empty MakeCode Arcade song with the specified number of measures, + including the default instruments. + + :param measures: The number of measures to include. + :return: A MakeCode Arcade `Song`. + """ + tracks: List[Track] = [ + Track( + id=0, + name="Dog", + notes=[], + icon_uri="music-editor/dog.png", + instrument=Instrument( + waveform=1, + octave=4, + amp_envelope=Envelope(attack=10, decay=100, sustain=500, release=100, + amplitude=1024), + pitch_envelope=None, + amp_lfo=None, + pitch_lfo=LFO(frequency=5, amplitude=0) + ) + ), + Track( + id=1, + name="Duck", + notes=[], + icon_uri="music-editor/duck.png", + instrument=Instrument( + waveform=15, + octave=4, + amp_envelope=Envelope(attack=5, decay=530, sustain=705, release=450, + amplitude=1024), + pitch_envelope=Envelope(attack=5, decay=40, sustain=0, release=100, + amplitude=40), + amp_lfo=LFO(frequency=3, amplitude=20), + pitch_lfo=LFO(frequency=6, amplitude=2) + ) + ), + Track( + id=2, + name="Cat", + notes=[], + icon_uri="music-editor/cat.png", + instrument=Instrument( + waveform=12, + octave=5, + amp_envelope=Envelope(attack=150, decay=100, sustain=365, release=400, + amplitude=1024), + pitch_envelope=Envelope(attack=120, decay=300, sustain=0, release=100, + amplitude=50), + amp_lfo=None, + pitch_lfo=LFO(frequency=10, amplitude=6) + ) + ), + Track( + id=3, + name="Fish", + notes=[], + icon_uri="music-editor/fish.png", + instrument=Instrument( + waveform=1, + octave=3, + amp_envelope=Envelope(attack=220, decay=105, sustain=1024, release=350, + amplitude=1024), + amp_lfo=LFO(frequency=5, amplitude=100), + pitch_lfo=LFO(frequency=1, amplitude=4), + pitch_envelope=None + ) + ), + Track( + id=4, + name="Car", + notes=[], + icon_uri="music-editor/car.png", + instrument=Instrument( + waveform=16, + octave=4, + amp_envelope=Envelope(attack=5, decay=100, sustain=1024, release=30, + amplitude=1024), + pitch_lfo=LFO(frequency=10, amplitude=4), + pitch_envelope=None, + amp_lfo=None + ) + ), + Track( + id=5, + name="Computer", + notes=[], + icon_uri="music-editor/computer.png", + instrument=Instrument( + waveform=15, + octave=2, + amp_envelope=Envelope(attack=10, decay=100, sustain=500, release=10, + amplitude=1024) + ) + ), + Track( + id=6, + name="Burger", + notes=[], + icon_uri="music-editor/burger.png", + instrument=Instrument( + waveform=1, + octave=2, + amp_envelope=Envelope(attack=10, decay=100, sustain=500, release=100, + amplitude=1024) + ) + ), + Track( + id=7, + name="Cherry", + notes=[], + icon_uri="music-editor/cherry.png", + instrument=Instrument( + waveform=2, + octave=3, + amp_envelope=Envelope(attack=10, decay=100, sustain=500, release=100, + amplitude=1024) + ) + ), + Track( + id=8, + name="Lemon", + notes=[], + icon_uri="music-editor/lemon.png", + instrument=Instrument( + waveform=14, + octave=2, + amp_envelope=Envelope(attack=5, decay=70, sustain=870, release=50, + amplitude=1024), + pitch_envelope=Envelope(attack=10, decay=45, sustain=0, release=100, + amplitude=20), + amp_lfo=LFO(frequency=1, amplitude=50), + pitch_lfo=LFO(frequency=2, amplitude=1) + ) + ), + Track( + id=9, + name="Drums", + notes=[], + icon_uri="music-editor/explosion.png", + instrument=Instrument( + waveform=11, + octave=4, + amp_envelope=Envelope(attack=10, decay=100, sustain=500, release=100, + amplitude=1024) + ), + drums=[ + DrumInstrument( + name="neutral kick", + start_frequency=100, + start_volume=1024, + steps=[ + DrumSoundStep(waveform=3, frequency=120, duration=10, + volume=1024), + DrumSoundStep(waveform=3, frequency=1, duration=100, volume=0), + ] + ), + DrumInstrument( + name="punchy kick", + start_frequency=200, + start_volume=1024, + steps=[ + DrumSoundStep(waveform=1, frequency=0, duration=100, volume=0) + ] + ), + DrumInstrument( + name="booming kick", + start_frequency=100, + start_volume=1024, + steps=[ + DrumSoundStep(waveform=1, frequency=0, duration=250, volume=0) + ] + ), + DrumInstrument( + name="snare 1", + start_frequency=175, + start_volume=1024, + steps=[ + DrumSoundStep(waveform=1, frequency=200, duration=10, + volume=1024), + DrumSoundStep(waveform=1, frequency=150, duration=20, + volume=1024), + DrumSoundStep(waveform=5, frequency=1, duration=20, volume=100), + DrumSoundStep(waveform=5, frequency=1, duration=300, volume=0), + ] + ), + DrumInstrument( + name="snare 2", + start_frequency=220, + start_volume=1024, + steps=[ + DrumSoundStep(waveform=1, frequency=250, duration=10, + volume=1024), + DrumSoundStep(waveform=1, frequency=200, duration=20, + volume=1024), + DrumSoundStep(waveform=5, frequency=2000, duration=20, + volume=100), + DrumSoundStep(waveform=5, frequency=2000, duration=200, + volume=0), + ] + ), + DrumInstrument( + name="hat 1", + start_frequency=400, + start_volume=500, + steps=[ + DrumSoundStep(waveform=5, frequency=450, duration=10, + volume=500), + DrumSoundStep(waveform=5, frequency=400, duration=20, + volume=20), + ] + ), + DrumInstrument( + name="hat 2", + start_frequency=400, + start_volume=0, + steps=[ + DrumSoundStep(waveform=5, frequency=450, duration=5, + volume=500), + DrumSoundStep(waveform=5, frequency=900, duration=50, volume=5), + DrumSoundStep(waveform=5, frequency=900, duration=250, + volume=0), + ] + ), + DrumInstrument( + name="hat 3", + start_frequency=400, + start_volume=0, + steps=[ + DrumSoundStep(waveform=5, frequency=450, duration=5, + volume=500), + DrumSoundStep(waveform=5, frequency=900, duration=50, + volume=200), + DrumSoundStep(waveform=5, frequency=900, duration=100, + volume=5), + DrumSoundStep(waveform=5, frequency=900, duration=400, + volume=0), + ] + ), + DrumInstrument( + name="hat 4", + start_frequency=400, + start_volume=0, + steps=[ + DrumSoundStep(waveform=5, frequency=450, duration=5, + volume=500), + DrumSoundStep(waveform=5, frequency=900, duration=100, + volume=200), + DrumSoundStep(waveform=5, frequency=900, duration=200, + volume=5), + DrumSoundStep(waveform=5, frequency=900, duration=500, + volume=0), + ] + ), + DrumInstrument( + name="double hat", + start_frequency=3500, + start_volume=1024, + steps=[ + DrumSoundStep(waveform=4, frequency=4000, duration=10, + volume=0), + DrumSoundStep(waveform=4, frequency=3500, duration=1, + volume=800), + DrumSoundStep(waveform=4, frequency=4000, duration=40, + volume=0), + DrumSoundStep(waveform=4, frequency=3500, duration=1, + volume=400), + DrumSoundStep(waveform=4, frequency=4000, duration=40, + volume=0), + ] + ), + DrumInstrument( + name="metallic", + start_frequency=2000, + start_volume=1024, + steps=[ + DrumSoundStep(waveform=4, frequency=1800, duration=100, + volume=15), + DrumSoundStep(waveform=4, frequency=1800, duration=200, + volume=0), + ] + ), + DrumInstrument( + name="low tom", + start_frequency=200, + start_volume=200, + steps=[ + DrumSoundStep(waveform=14, frequency=125, duration=25, + volume=200), + DrumSoundStep(waveform=14, frequency=100, duration=50, + volume=15), + DrumSoundStep(waveform=14, frequency=120, duration=250, + volume=0), + ] + ), + DrumInstrument( + name="mid tom", + start_frequency=300, + start_volume=200, + steps=[ + DrumSoundStep(waveform=14, frequency=225, duration=25, + volume=200), + DrumSoundStep(waveform=14, frequency=200, duration=50, + volume=15), + DrumSoundStep(waveform=14, frequency=220, duration=250, + volume=0), + ] + ), + DrumInstrument( + name="hi tom", + start_frequency=500, + start_volume=200, + steps=[ + DrumSoundStep(waveform=14, frequency=425, duration=25, + volume=200), + DrumSoundStep(waveform=14, frequency=400, duration=50, + volume=15), + DrumSoundStep(waveform=14, frequency=420, duration=250, + volume=0), + ] + ), + DrumInstrument( + name="lo tom 2", + start_frequency=200, + start_volume=1024, + steps=[ + DrumSoundStep(waveform=1, frequency=75, duration=200, volume=0), + ] + ), + DrumInstrument( + name="mid tom 2", + start_frequency=300, + start_volume=1024, + steps=[ + DrumSoundStep(waveform=1, frequency=200, duration=200, + volume=0), + ] + ), + DrumInstrument( + name="hi tom 2", + start_frequency=400, + start_volume=1024, + steps=[ + DrumSoundStep(waveform=1, frequency=300, duration=200, + volume=0), + ] + ), + DrumInstrument( + name="thump 1", + start_frequency=200, + start_volume=1024, + steps=[ + DrumSoundStep(waveform=4, frequency=200, duration=100, + volume=15), + DrumSoundStep(waveform=4, frequency=150, duration=200, + volume=0), + ] + ), + DrumInstrument( + name="thump 2", + start_frequency=450, + start_volume=1024, + steps=[ + DrumSoundStep(waveform=4, frequency=350, duration=100, + volume=15), + DrumSoundStep(waveform=4, frequency=300, duration=100, + volume=0), + ] + ), + DrumInstrument( + name="cymbal", + start_frequency=2500, + start_volume=1024, + steps=[ + DrumSoundStep(waveform=4, frequency=2500, duration=150, + volume=100), + DrumSoundStep(waveform=4, frequency=2550, duration=500, + volume=0), + ] + ), + DrumInstrument( + name="crash 1", + start_frequency=3000, + start_volume=1024, + steps=[ + DrumSoundStep(waveform=4, frequency=3000, duration=300, + volume=100), + DrumSoundStep(waveform=4, frequency=3060, duration=500, + volume=0), + ] + ), + DrumInstrument( + name="crash 2", + start_frequency=800, + start_volume=0, + steps=[ + DrumSoundStep(waveform=4, frequency=800, duration=10, + volume=1024), + DrumSoundStep(waveform=4, frequency=800, duration=490, + volume=0), + ] + ), + DrumInstrument( + name="crash 3", + start_frequency=400, + start_volume=0, + steps=[ + DrumSoundStep(waveform=4, frequency=400, duration=10, + volume=1024), + DrumSoundStep(waveform=4, frequency=400, duration=400, + volume=0), + ] + ), + DrumInstrument( + name="buzzer", + start_frequency=2000, + start_volume=1024, + steps=[ + DrumSoundStep(waveform=16, frequency=2000, duration=150, + volume=100), + DrumSoundStep(waveform=16, frequency=2000, duration=200, + volume=0), + ] + ), + ] + ), + ] -def getEmptySong(measures: int) -> Song: return Song( - ticksPerBeat=8, - beatsPerMeasure=4, - beatsPerMinute=120, measures=measures, - tracks=[ - Track( - id=0, name="Dog", notes=[], - iconURI="/static/music-editor/dog.png", - instrument=Instrument( - waveform=1, - octave=4, - ampEnvelope=Envelope( - attack=10, - decay=100, - sustain=500, - release=100, - amplitude=1024 - ), - pitchLFO=LFO( - frequency=5, - amplitude=0 - ) - ) - ) - ] + beats_per_measure=4, + beats_per_minute=120, + ticks_per_beat=8, + tracks=tracks ) diff --git a/src/arcade/music_types.py b/src/arcade/music_types.py new file mode 100644 index 0000000..a1f1db3 --- /dev/null +++ b/src/arcade/music_types.py @@ -0,0 +1,169 @@ +# https://github.com/microsoft/pxt/blob/master/localtypings/pxtmusic.d.ts +from __future__ import annotations + +import logging +from dataclasses import dataclass +from enum import Enum +from typing import List, Optional + +from utils.logger import create_logger + +logger = create_logger(name=__name__, level=logging.INFO) + + +# /** +# * Byte encoding format for songs +# * FIXME: should this all be word aligned? +# * +# * song(7 + length of all tracks bytes) +# * 0 version +# * 1 beats per minute +# * 3 beats per measure +# * 4 ticks per beat +# * 5 measures +# * 6 number of tracks +# * ...tracks +# * ...track velocities +# * +# * track(6 + instrument length + note length bytes) +# * 0 id +# * 1 flags +# * 2 instruments byte length +# * 4...instrument +# * notes byte length +# * ...note events +# * +# * instrument(28 bytes) +# * 0 waveform +# * 1 amp attack +# * 3 amp decay +# * 5 amp sustain +# * 7 amp release +# * 9 amp amp +# * 11 pitch attack +# * 13 pitch decay +# * 15 pitch sustain +# * 17 pitch release +# * 19 pitch amp +# * 21 amp lfo freq +# * 22 amp lfo amp +# * 24 pitch lfo freq +# * 25 pitch lfo amp +# * 27 octave +# * +# * drum(5 + 7 * steps bytes) +# * 0 steps +# * 1 start freq +# * 3 start amp +# * 5...steps +# * +# * drum step(7 bytes) +# * 0 waveform +# * 1 freq +# * 3 volume +# * 5 duration +# * +# * note event(5 + 1 * polyphony bytes) +# * 0 start tick +# * 2 end tick +# * 4 polyphony +# * 5...notes(1 byte each) +# * +# * note (1 byte) +# * lower six bits = note - (instrumentOctave - 2) * 12 +# * upper two bits are the enharmonic spelling: +# * 0 = normal +# * 1 = flat +# * 2 = sharp +# * +# * track velocity +# * 0 track id +# * 1...velocities +# * +# * velocty +# * 1 byte +# */ + + +@dataclass +class Instrument: + waveform: int + octave: int + amp_envelope: Envelope + pitch_envelope: Optional[Envelope] = None + amp_lfo: Optional[LFO] = None + pitch_lfo: Optional[LFO] = None + + +@dataclass +class Envelope: + attack: int + decay: int + sustain: int + release: int + amplitude: int + + +@dataclass +class LFO: + frequency: int + amplitude: int + + +@dataclass +class SongInfo: + measures: int + beats_per_measure: int + beats_per_minute: int + ticks_per_beat: int + + +@dataclass +class Song(SongInfo): + tracks: List[Track] + + +@dataclass +class Track: + id: int + instrument: Instrument + notes: List[NoteEvent] + drums: Optional[List[DrumInstrument]] = None + name: Optional[str] = None + icon_uri: Optional[str] = None + + +@dataclass +class NoteEvent: + notes: List[Note] + start_tick: int + end_tick: int + velocity: Optional[int] = None + + +class EnharmonicSpelling(Enum): + NORMAL = "normal" + FLAT = "flat" + SHARP = "sharp" + + +@dataclass +class Note: + note: int + enharmonic_spelling: EnharmonicSpelling + + +@dataclass +class DrumSoundStep: + waveform: int + frequency: int + volume: int + duration: int + + +@dataclass +class DrumInstrument: + start_frequency: int + start_volume: int + steps: List[DrumSoundStep] + name: Optional[str] = None diff --git a/src/arcade/tracks.py b/src/arcade/tracks.py deleted file mode 100644 index f3f3274..0000000 --- a/src/arcade/tracks.py +++ /dev/null @@ -1,213 +0,0 @@ -from .music import Envelope, Instrument, LFO, Track - - -def get_available_tracks() -> list[Track]: - return [ - Track( - id=0, name="Dog", notes=[], - iconURI="/static/music-editor/dog.png", - instrument=Instrument( - waveform=1, - octave=4, - ampEnvelope=Envelope( - attack=10, - decay=100, - sustain=500, - release=100, - amplitude=1024 - ), - pitchLFO=LFO( - frequency=5, - amplitude=0 - ) - ) - ), - Track( - id=1, - name="Duck", notes=[], - iconURI="music-editor/duck.png", - instrument=Instrument( - waveform=15, - octave=4, - ampEnvelope=Envelope( - attack=5, - decay=530, - sustain=705, - release=450, - amplitude=1024 - ), - pitchEnvelope=Envelope( - attack=5, - decay=40, - sustain=0, - release=100, - amplitude=40 - ), - ampLFO=LFO( - frequency=3, - amplitude=20 - ), - pitchLFO=LFO( - frequency=6, - amplitude=2 - ) - ) - ), - Track( - id=2, - name="Cat", - notes=[], - iconURI="music-editor/cat.png", - instrument=Instrument( - waveform=12, - octave=5, - ampEnvelope=Envelope( - attack=150, - decay=100, - sustain=365, - release=400, - amplitude=1024 - ), - pitchEnvelope=Envelope( - attack=120, - decay=300, - sustain=0, - release=100, - amplitude=50 - ), - pitchLFO=LFO( - frequency=10, - amplitude=6 - ) - ) - ), - Track( - id=3, - name="Fish", - notes=[], - iconURI="music-editor/fish.png", - instrument=Instrument( - waveform=1, - octave=3, - ampEnvelope=Envelope( - attack=220, - decay=105, - sustain=1024, - release=350, - amplitude=1024 - ), - ampLFO=LFO( - frequency=5, - amplitude=100 - ), - pitchLFO=LFO( - frequency=1, - amplitude=4 - ) - ) - ), - Track( - id=4, - name="Car", - notes=[], - iconURI="music-editor/car.png", - instrument=Instrument( - waveform=16, - octave=4, - ampEnvelope=Envelope( - attack=5, - decay=100, - sustain=1024, - release=30, - amplitude=1024 - ), - pitchLFO=LFO( - frequency=10, - amplitude=4 - ) - ) - ), - Track( - id=5, - name="Computer", - notes=[], - iconURI="music-editor/computer.png", - instrument=Instrument( - waveform=15, - octave=2, - ampEnvelope=Envelope( - attack=10, - decay=100, - sustain=500, - release=10, - amplitude=1024 - ) - ) - ), - Track( - id=6, - name="Burger", - notes=[], - iconURI="music-editor/burger.png", - instrument=Instrument( - waveform=1, - octave=2, - ampEnvelope=Envelope( - attack=10, - decay=100, - sustain=500, - release=100, - amplitude=1024 - ) - ) - ), - Track( - id=7, - name="Cherry", - notes=[], - iconURI="music-editor/cherry.png", - instrument=Instrument( - waveform=2, - octave=3, - ampEnvelope=Envelope( - attack=10, - decay=100, - sustain=500, - release=100, - amplitude=1024 - ) - ) - ), - Track( - id=8, - name="Lemon", - notes=[], - iconURI="music-editor/lemon.png", - instrument=Instrument( - waveform=14, - octave=2, - ampEnvelope=Envelope( - attack=5, - decay=70, - sustain=870, - release=50, - amplitude=1024 - ), - pitchEnvelope=Envelope( - attack=10, - decay=45, - sustain=0, - release=100, - amplitude=20 - ), - ampLFO=LFO( - frequency=1, - amplitude=50 - ), - pitchLFO=LFO( - frequency=2, - amplitude=1 - ) - ) - ) - ] diff --git a/src/converter.py b/src/converter.py deleted file mode 100644 index c7869f0..0000000 --- a/src/converter.py +++ /dev/null @@ -1,55 +0,0 @@ -import logging -from enum import Enum -from typing import Optional, Union - -from mido import MidiFile - -from arcade.music import encodeSong -from midi_to_song import midi_to_song -from utils.logger import create_logger - -logger = create_logger(name=__name__, level=logging.DEBUG) - - -class OutputOptions(Enum): - MAKECODE_ARCADE_STRING = "makecode_arcade_string" - - -def convert(input: MidiFile, output: OutputOptions, - track: Optional[Union[int, str]] = None, - divisor: Optional[float] = 1, - char_break: Optional[int] = 0) -> Union[str]: - """ - Converts a MIDI file to a MakeCode Arcade song. - - :param input: Input MIDI file. - :param output: An OutputOptions enum value. Currently only supports - MAKECODE_ARCADE_STRING. - :param track: The track to use. Can be an index or a string. Defaults to - the first track. - :param divisor: A float to divide the number of measures used, to fit big songs - into the maximum number of measures being 255. Defaults to 1. - :param char_break: An integer to break the hex string after so many characters. - Defaults to 0. (No breaking) - :return: A string which is a MakeCode Arcade song. - """ - song = midi_to_song(input, int(track) if track.isnumeric() else track, - divisor) - bin_result = encodeSong(song) - - logger.debug(f"Generated {len(bin_result)} bytes, converting to text") - - logger.debug(f"Using character break of {char_break}") - - hex_result = map(lambda v: format(v, "02x"), bin_result) - result = "hex`" - for i, hex_num in enumerate(hex_result): - if char_break != 0 and i % char_break == 0: - result += "\n " - result += hex_num - if char_break != 0: - result += "\n" - result += "`" - logger.debug(f"Hex string result is {len(result)} characters long") - - return result diff --git a/src/main.py b/src/main.py index 256bd50..a7b251c 100644 --- a/src/main.py +++ b/src/main.py @@ -4,73 +4,128 @@ from mido import MidiFile -from arcade.tracks import get_available_tracks -from converter import OutputOptions, convert +from arcade.music import encode_song_to_hex +from midi_to_song import TestingOptionsForMIDIToSong, convert_midi_to_song +from midi_to_song.instruments import load_instrument_params +from midi_to_song.models import TestingOptionsForLoadInstrumentParams from utils.logger import create_logger, set_all_stdout_logger_levels +from utils.strings import parse_range -tracks = get_available_tracks() -track_names = [t.name.lower() for t in tracks] -track_ids = [str(t.id) for t in tracks] - -parser = ArgumentParser(prog="ArcadeMIDItoSong", - description="A program to convert MIDI files to the " - "Arcade song format. ") -parser.add_argument("--input", "-i", required=True, type=Path, - help="Input MIDI file") +parser = ArgumentParser(description="Convert a MIDI file to a MakeCode Arcade song.") +parser.add_argument("--input", "-i", type=Path, required=True, + help="Input MIDI file.") +parser.add_argument("--input-instrument-params", "-p", type=Path, + required=True, help="Input instrument parameter mapping file.") parser.add_argument("--output", "-o", type=Path, - help="Output text file path, otherwise we will output to " - "standard output.") -parser.add_argument("--track", "-t", metavar="TRACK", - choices=track_ids + track_names, - default=track_names[0], - help=f"A track to use, which changes the instrument. " - f"Available tracks include {track_names}. (You can " - f"also use indices 0-{len(track_ids) - 1}) Defaults " - f"to '{track_names[0]}'.") -parser.add_argument("--divisor", "-d", type=float, - default=1, - help="A divisor to reduce (or increase!) the number of " - "measures used. A higher float means a longer song " - "can fit in the maximum of 255 measures of a song, " - "but with less precision. Must be greater than 0, " - "defaults to 1 for no division.") -parser.add_argument("--break", "-b", type=int, dest="char_break", - default=0, - help="Break the hex string after so many characters. " - "Defaults to 0 for no breaking.") + help="Output TypeScript file path, otherwise will write to stdout. " + "If specified, this will overwrite the file if it already " + "exists, and the parent directories must exist.") parser.add_argument("--debug", action="store_const", const=logging.DEBUG, default=logging.INFO, help="Include debug messages. Defaults to info and " "greater severity messages only.") + +testing_group = parser.add_argument_group("Testing options") +testing_group.add_argument("--test-replace-all-melodics-with", type=int, + default=None, help="Replace all melodic tracks in a song " + "with the specific MIDI instrument.") +testing_group.add_argument("--test-ask-to-replace-all-melodics-with", + action="store_true", help="Prompt the user to replace all " + "melodic tracks in a song with a " + "specific MIDI instrument.") +testing_group.add_argument("--test-generate-code", action="store_true", + help="Generate the MakeCode Arcade code to play the song.") +testing_group.add_argument("--test-force-instrument-param-load", action="store_true", + help="Forcibly load the instrument parameter mapping file, " + "even if it would normally cause errors.") +testing_group.add_argument("--test-sample-melodic-instruments", type=parse_range, + help="Pass in a range of MIDI instruments to sample, such " + "as \"0,2,4-10\" to replicate the song several times " + "and replace all melodic tracks in the song with the " + "specific MIDI instrument. Basically does what " + "`--test-replace-all-melodics-with` and " + "`--test-generate-code` but with a bunch of specified " + "instruments.") + args = parser.parse_args() logger = create_logger(name=__name__, level=logging.INFO) set_all_stdout_logger_levels(args.debug) logger.debug(f"Received arguments: {args}") +testing_opts_midi_to_song = TestingOptionsForMIDIToSong() +testing_opts_load_instrument_params = TestingOptionsForLoadInstrumentParams() + +testing_opts_midi_to_song.replace_all_melodics_with = args.test_replace_all_melodics_with +testing_opts_load_instrument_params.force_load = args.test_force_instrument_param_load +if args.test_ask_to_replace_all_melodics_with: + testing_opts_midi_to_song.replace_all_melodics_with = int( + input("Replace all melodics with MIDI " + "instrument: ")) +testing_opts_midi_to_song.generate_code = args.test_generate_code +if testing_opts_midi_to_song.replace_all_melodics_with is not None: + logger.info(f"Replacing all melodic instruments with MIDI instrument " + f"{testing_opts_midi_to_song.replace_all_melodics_with} in final output") +if testing_opts_midi_to_song.generate_code: + logger.info(f"Final result will be valid MakeCode Arcade TypeScript code to play " + f"the song") + input_path = Path(args.input) -logger.debug(f"Input path is {input_path}") +logger.info(f"Reading MIDI file from {input_path}") -midi = MidiFile(input_path) -logger.debug(f"MIDI is {midi.length}s long") +mid = MidiFile(input_path) +logger.debug(f"Found {len(mid.tracks)} tracks, length of {mid.length}s") -divisor = float(args.divisor) -if not divisor > 0: - raise ValueError(f"divisor must be a float greater than 0, " - f"not {divisor}!") -logger.debug(f"Using divisor of {divisor}") +input_instrument_param_path = Path(args.input_instrument_params) +if testing_opts_load_instrument_params.force_load: + logger.info(f"Forcibly reading instrument params from " + f"{input_instrument_param_path}") +else: + logger.info( + f"Reading instrument parameter mapping from {input_instrument_param_path}") + +mapping = load_instrument_params(input_instrument_param_path.read_text(), + testing_opts_load_instrument_params) +logger.debug(f"Mapped {len(mapping.melodic_instruments)} melodic instruments and " + f"{len(mapping.drum_instruments)} drum instruments") + +melodic_sample = args.test_sample_melodic_instruments +if melodic_sample is not None: + logger.info(f"Sampling {melodic_sample} melodic instruments") -char_break = int(args.char_break) -if char_break < 0: - raise ValueError(f"break must be an integer greater than or equal to 0, " - f"not {char_break}!") + final_output = "\n// generated melodic instrument sample\n\n" + + for instrument in melodic_sample: + logger.info(f"Generating code for melodic instrument {instrument}") + testing_opts_midi_to_song.replace_all_melodics_with = instrument + song = convert_midi_to_song(mid, mapping, testing_opts_midi_to_song) + h = encode_song_to_hex(song) + final_output += f"""// melodics replaced with MIDI instrument {instrument} +info.setScore({instrument}); +music.play(music.createSong( + hex`{h}` +), music.PlaybackMode.UntilDone); +""" +else: + song = convert_midi_to_song(mid, mapping, testing_opts_midi_to_song) + h = encode_song_to_hex(song) + final_output = f"hex`{h}`" + logger.info("Finished converting MIDI file") -result = convert(midi, OutputOptions.MAKECODE_ARCADE_STRING, args.track, divisor, - char_break) + if testing_opts_midi_to_song.generate_code: + final_output = f"""music.play(music.createSong( + {final_output} +), music.PlaybackMode.UntilDone); +""" + if testing_opts_midi_to_song.replace_all_melodics_with is not None: + final_output = (f"// melodics replaced with MIDI instrument " + f"{testing_opts_midi_to_song.replace_all_melodics_with}\n{final_output}") -output_path = args.output -if output_path is None: - logger.debug("No output path provided, printing to standard output") - print(result) +output_path = Path(args.output) if args.output is not None else None +if output_path is not None: + logger.info(f"Writing result to {output_path}") + output_path.write_text(final_output) else: - logger.debug(f"Writing to {output_path}") - Path(output_path).write_text(result) + if testing_opts_midi_to_song.generate_code: + final_output = "\n" + final_output + logger.info(f"Writing result to stdout") + print(final_output) diff --git a/src/midi_to_song.py b/src/midi_to_song.py deleted file mode 100644 index 86344c5..0000000 --- a/src/midi_to_song.py +++ /dev/null @@ -1,186 +0,0 @@ -import logging -from collections import namedtuple -from math import ceil -from sys import stdout -from typing import Union - -from mido import Message, MidiFile -from tqdm import tqdm - -from arcade.music import EnharmonicSpelling, Note, NoteEvent, Song, Track, \ - getEmptySong -from arcade.tracks import get_available_tracks -from utils.logger import create_logger - -logger = create_logger(name=__name__, level=logging.INFO) - - -def midi_to_song(midi: MidiFile, track_id: Union[str, int], - divisor: float) -> Song: - def get_track_from_name_or_id(name_or_id: Union[int, str]) -> Track: - logger.debug(f"Finding track {name_or_id}") - for track in get_available_tracks(): - if name_or_id == track.name.lower() or name_or_id == track.id: - selected_track = track - break - else: - raise ValueError(f"Unknown track ID or name {name_or_id}!") - logger.debug(f"Found track '{selected_track.name}' ({selected_track})") - return selected_track - - def find_note_time(start_index: int, note: int, - msgs: list[Message]) -> float: - time = 0 - for i in range(start_index, len(msgs)): - msg = msgs[i] - if msg.type not in ("note_on", "note_off"): - continue - time += msg.time - if ((msg.type == "note_on" and msg.velocity == 0) or - msg.type == "note_off") and msg.note == note: - break - return time - - NoteInfo = namedtuple("NoteInfo", - "note_value note_time start_tick end_tick") - - def gather_note_info(index: int, msgs: list[Message], - current_time: int) -> NoteInfo: - msg = msgs[index] - if msg.type != "note_on": - return NoteInfo( - note_value=-1, - note_time=-1, - start_tick=-1, - end_tick=-1 - ) - note_time = round(find_note_time(index + 1, msg.note, msgs) * 1000) - start_tick = round(current_time / 10) - end_tick = start_tick + round(note_time / 10) - return NoteInfo( - note_value=msg.note, - note_time=note_time, - start_tick=start_tick, - end_tick=end_tick - ) - - NoteSimpleEvent = namedtuple("NoteSimpleEvent", - "note start_tick end_tick") - ChordSimpleEvent = namedtuple("ChordSimpleEvent", - "notes start_tick end_tick") - - def find_chord_with_start_tick(chords: list[ChordSimpleEvent], - start_tick: int) -> int: - for i, chord in enumerate(chords): - if chord.start_tick == start_tick: - return i - return -1 - - def add_tracks_for_piano(song: Song, track_id: Union[int, str]): - selected_track = get_track_from_name_or_id(track_id) - selected_higher_track = get_track_from_name_or_id(track_id) - song.tracks.append(selected_track) - song.tracks[-1].instrument.octave = 2 - song.tracks.append(selected_higher_track) - song.tracks[-1].instrument.octave = 7 - logger.debug(f"Added 2 piano tracks") - - msgs = list(midi) - simple_notes = [] - - curr_time = 0 - ending_tick = 0 - for i, msg in tqdm(enumerate(msgs), desc="Processing (stage 1/3)", - file=stdout): - curr_time += round(msg.time * 1000) - if msg.type not in ("note_on", "note_off"): - continue - # logger.debug(f"{i}: {msg} (current time: {curr_time})") - if msg.type == "note_off" or ( - msg.type == "note_on" and msg.velocity == 0): - pass - else: - note_info = gather_note_info(i, msgs, curr_time) - note_simple_event = NoteSimpleEvent(note_info.note_value, - note_info.start_tick, - note_info.end_tick) - ending_tick = max(ending_tick, note_info.end_tick) - # duration = (note_simple_event.end_tick - - # note_simple_event.start_tick) - # logger.debug(f"{i}: * {note_simple_event} " - # f"(duration: {duration})") - simple_notes.append(note_simple_event) - - logger.debug(f"Last tick is {ending_tick} ({round(ending_tick / divisor)} " - f"after divisor)") - - ending_tick = round(ending_tick / divisor) - - simple_chords = [] - for note in tqdm(simple_notes, desc="Processing (stage 2/3)", file=stdout): - chord_index = find_chord_with_start_tick(simple_chords, - note.start_tick) - if chord_index == -1: - simple_chords.append( - ChordSimpleEvent([note.note], note.start_tick, note.end_tick) - ) - else: - simple_chords[chord_index].notes.append(note.note) - - ticks_per_beat = 100 - beats_per_measure = 10 - beats_per_minute = round(60 / divisor) - measure_count = ceil(ending_tick / ticks_per_beat / beats_per_measure) - - logger.debug( - f"{measure_count=} {ticks_per_beat=} {beats_per_measure=} {beats_per_minute=}") - logger.debug(f"Maximum number of ticks is " - f"{measure_count * ticks_per_beat * beats_per_measure} ticks") - - song = getEmptySong(measure_count) - song.ticksPerBeat = ticks_per_beat - song.beatsPerMeasure = beats_per_measure - song.beatsPerMinute = beats_per_minute - song.tracks.clear() - add_tracks_for_piano(song, track_id) - - for i, chord in tqdm(enumerate(simple_chords), desc="Processing (stage 3/3)", - file=stdout): - # logger.debug(f"Chord {i}: {chord}") - all_notes = [Note(note=n, enharmonicSpelling=EnharmonicSpelling.NORMAL) - for n in chord.notes] - notes = [] - higher_notes = [] - for note in all_notes: - instrumentOctave = song.tracks[-2].instrument.octave - note_val = (note.note - (instrumentOctave - 2) * 12) - note_val += 1 - 12 - if note_val > 63: - higher_notes.append(note) - else: - notes.append(note) - event = NoteEvent( - notes=notes, - startTick=round(chord.start_tick / divisor), - endTick=round(chord.end_tick / divisor) - ) - higher_event = NoteEvent( - notes=higher_notes, - startTick=round(chord.start_tick / divisor), - endTick=round(chord.end_tick / divisor) - ) - # logger.debug(f"Note event {i}: {event}") - if len(event.notes) > 0: - song.tracks[-2].notes.append(event) - if len(higher_event.notes) > 0: - song.tracks[-1].notes.append(higher_event) - ending_tick = chord.end_tick - - for i, track in enumerate(song.tracks): - logger.debug( - f"Created {len(song.tracks[i].notes)} note events in track {i}") - logger.debug( - f"Total of {sum([len(t.notes) for t in song.tracks])} note events") - logger.debug(f"Last tick is {ending_tick}") - - return song diff --git a/src/midi_to_song/__init__.py b/src/midi_to_song/__init__.py new file mode 100644 index 0000000..fdee2ed --- /dev/null +++ b/src/midi_to_song/__init__.py @@ -0,0 +1,175 @@ +import logging +from copy import deepcopy +from math import ceil +from typing import Dict, List, Optional + +from mido import MidiFile + +from arcade.music_types import EnharmonicSpelling, Envelope, Instrument, Note, \ + NoteEvent, Song, Track +from midi_to_song.instruments import InstrumentParameterMapping +from midi_to_song.models import AbsoluteCompleteChordWithTick, AbsoluteCompleteNote, \ + AbsoluteCompleteNoteWithTick, AbsoluteTickMessage, AbsoluteTimeMessage, \ + AbsoluteTimeMessageWithInstrument, ChannelState, DrumDeterminationSource, \ + TestingOptionsForMIDIToSong +from midi_to_song.timeline.parser import timeline_build, timeline_find_instrument_data, \ + timeline_group_messages +from midi_to_song.timeline.processor import find_all_drum_chords_used, \ + timeline_fix_gate_lens, timeline_group_by_instrument, \ + timeline_group_into_perfect_chords, \ + timeline_quantize_to_song_ticks, timeline_resolve_overlapping_chords, \ + timeline_split_into_two_tracks_if_needed +from midi_to_song.timeline.validation import timeline_checks +from utils.logger import create_logger + +logger = create_logger(name=__name__, level=logging.INFO) + + +def convert_midi_to_song(midi_song: MidiFile, + mapping: InstrumentParameterMapping, + testing_opts: Optional[ + TestingOptionsForMIDIToSong] = None) -> Song: + """ + Convert a MIDI file into a MakeCode Arcade song. + + :param midi_song: A `MidiFile` object. + :param mapping: An `InstrumentParameterMapping` object, loaded from + `load_instrument_params`. + :param testing_opts: Extra options used for testing, passed from the CLI. + :return: MakeCode Arcade `Song` object. + """ + logger.debug("Converting MIDI file into MakeCode Arcade song") + + if testing_opts is None: + testing_opts = TestingOptionsForMIDIToSong() + + song = Song( + measures=1, + beats_per_measure=4, + beats_per_minute=120, # beat every 1/2 seconds + ticks_per_beat=24, # each tick is 1/48 seconds long + tracks=[] + ) + + logger.debug("Resolving timeline") + + global_timeline: List[AbsoluteTimeMessage] = timeline_build(midi_song) + global_timeline: List[ + AbsoluteTimeMessageWithInstrument] = timeline_find_instrument_data( + global_timeline) + global_timeline: List[AbsoluteCompleteNote] = timeline_group_messages( + global_timeline) + + if testing_opts.replace_all_melodics_with is not None: + logger.debug(f"Testing option enabled to replace all melodic instruments with " + f"MIDI instrument {testing_opts.replace_all_melodics_with}") + for m in global_timeline: + if not m.is_drum: + m.instrument = testing_opts.replace_all_melodics_with + + # MIDI file with C4 (MIDI 60) plays at B5 (MIDI 83) + # This is because MakeCode Arcade defines C4 as 49 instead of 60 + # And now I have no idea why I need to shift down another octave but then it works + # Drums don't need this because we already map from MIDI drum notes to an index into + # a list of drum instruments in a track, which we control + for note in global_timeline: + if not note.is_drum: + note.note -= 11 # MIDI 60 (C4) maps to Arcade's C4 of 49 + note.note -= 12 # another octave down makes it correct + + global_timeline = timeline_fix_gate_lens(global_timeline, song, mapping) + global_timeline: List[ + AbsoluteCompleteNoteWithTick] = timeline_quantize_to_song_ticks(global_timeline, + song) + global_timeline: List[ + List[AbsoluteCompleteNoteWithTick]] = timeline_group_by_instrument( + global_timeline) + global_timeline = timeline_split_into_two_tracks_if_needed(global_timeline) + global_timeline: List[ + List[AbsoluteCompleteChordWithTick]] = timeline_group_into_perfect_chords( + global_timeline) + + # Raises exceptions on check failures + timeline_checks(song, global_timeline, mapping) + + # With all this pitch checks and timing manipulations done to fit MakeCode Arcade's + # song's constraints, we should be able to basically map 1-1 to the MakeCode Arcade + # dataclasses + logger.debug("Timeline resolved, mapping to MakeCode Arcade song") + + next_id = 0 + highest_tick = 0 + + for old_track in global_timeline: + this_track_is_drum = old_track[0].is_drum + highest_tick = max([highest_tick] + [c.end_tick for c in old_track]) + + # for drums + midi_drum_to_drum_idx: Dict[int, int] = {} + if this_track_is_drum: + # shouldn't matter, copied from get_empty_song to satisfy types and song + # packing + instrument = Instrument( + waveform=11, + octave=4, + amp_envelope=Envelope(attack=10, decay=100, sustain=500, release=100, + amplitude=1024) + ) + # actually load the drums in + # and keep what midi note to what sample index they should go to + drums = [] + used_drum_notes = find_all_drum_chords_used(old_track) + for i, drum_note in enumerate(used_drum_notes): + drums.append(mapping.drum_instruments[drum_note]) + midi_drum_to_drum_idx[drum_note] = i + else: + instrument = deepcopy(mapping.melodic_instruments[old_track[0].instrument]) + # determine the optimal octave offset + highest_note = max([max(chord.notes) for chord in old_track]) + lowest_note = min([min(chord.notes) for chord in old_track]) + + def octave_offset_work(octave: int) -> bool: + return ((((octave - 2) * 12) <= lowest_note) and + (highest_note <= ((octave - 2) * 12 + 63))) + + for potential_offset in range(0, 10): # find the first offset that works + if octave_offset_work(potential_offset): + instrument.octave = potential_offset + break + else: + raise ValueError(f"Track range too big to fit! (please report)") + # none for melodic instrument + drums = None + new_track = Track( + id=next_id, + instrument=instrument, + drums=drums, + notes=[], + ) + for chord in old_track: + if this_track_is_drum: + notes = [midi_drum_to_drum_idx[note] for note in + chord.notes] + else: + notes = chord.notes + new_track.notes.append(NoteEvent( + notes=[Note(note=n, enharmonic_spelling=EnharmonicSpelling.NORMAL) for n + in notes], + start_tick=chord.start_tick, + end_tick=chord.end_tick, + velocity=chord.velocity + )) + + song.tracks.append(new_track) + next_id += 1 + + # fix the ending measure count + ticks_per_measure = song.beats_per_measure * song.ticks_per_beat + song.measures = ceil(highest_tick / ticks_per_measure) + + time_for_tick = (60 / song.beats_per_minute) / song.ticks_per_beat + logger.debug(f"Finished mapping to MakeCode Arcade song with {len(song.tracks)} " + f"tracks, length of {highest_tick} ticks which is " + f"{highest_tick * time_for_tick} seconds") + + return song diff --git a/src/midi_to_song/instruments.py b/src/midi_to_song/instruments.py new file mode 100644 index 0000000..36b9fd3 --- /dev/null +++ b/src/midi_to_song/instruments.py @@ -0,0 +1,144 @@ +import logging +from dataclasses import dataclass +from enum import IntEnum +from typing import Dict, Optional + +from yaml import safe_load + +from arcade.music_types import DrumInstrument, DrumSoundStep, Envelope, Instrument, LFO +from midi_to_song.models import TestingOptionsForLoadInstrumentParams +from utils.logger import create_logger + +logger = create_logger(name=__name__, level=logging.INFO) + + +@dataclass +class InstrumentParameterMapping: + # The integer is the general MIDI instrument + melodic_instruments: Dict[int, Instrument] + # The integer is the MIDI drum kit note + drum_instruments: Dict[int, DrumInstrument] + + +class WaveForm(IntEnum): + TRIANGLE = 1 + SAWTOOTH = 2 + SINE = 3 + NOISE_TUNABLE = 4 + NOISE = 5 + SQUARE_10 = 11 + SQUARE_20 = 12 + SQUARE_30 = 13 + SQUARE_40 = 14 + SQUARE_50 = 15 # aka square + CYCLE_16 = 16 + CYCLE_32 = 17 + CYCLE_64 = 18 # aka noise_tunable_2 + + +def waveform_from_str(w: str) -> WaveForm: + """ + Takes a string and returns the correct waveform integer. + + :param w: A string like "triangle" or "sine". + :return: A `WaveForm` enum value. + """ + w = w.lower() + if w == "square": + w = "square_50" + if w == "noise_tunable_2": + w = "cycle_64" + return { + "triangle": WaveForm.TRIANGLE, + "sawtooth": WaveForm.SAWTOOTH, + "sine": WaveForm.SINE, + "noise_tunable": WaveForm.NOISE_TUNABLE, + "noise": WaveForm.NOISE, + "square_10": WaveForm.SQUARE_10, + "square_20": WaveForm.SQUARE_20, + "square_30": WaveForm.SQUARE_30, + "square_40": WaveForm.SQUARE_40, + "square_50": WaveForm.SQUARE_50, + "cycle_16": WaveForm.CYCLE_16, + "cycle_32": WaveForm.CYCLE_32, + "cycle_64": WaveForm.CYCLE_64, + }[w] + + +def load_instrument_params(yaml_text: str, + testing_opts: Optional[ + TestingOptionsForLoadInstrumentParams] = None) -> InstrumentParameterMapping: + """ + Take a YAML file specifying instrument data and return an instrument parameter + mapping. For melodic instruments, the octave has been set to 0, duplicate as + necessary for the full MIDI range (use octave offset 2 and 7 for full range) + + :param yaml_text: String holding the YAML file's text. + :param testing_opts: Extra options used for testing, passed from the CLI. + :return: An `InstrumentParameterMapping` object. + """ + logger.debug(f"Loading instrument parameters from {len(yaml_text)} characters of " + f"YAML text") + data = safe_load(yaml_text) + + mapping = InstrumentParameterMapping(melodic_instruments={}, drum_instruments={}) + + logger.debug(f"Creating mappings for {len(data["melodic_instruments"])} melodic " + f"instruments") + for instr in data["melodic_instruments"]: + try: + # TODO: If pitch envelope or LFOs aren't defined in the YAML don't error out + mapping.melodic_instruments[instr["instrument"]] = Instrument( + waveform=waveform_from_str(instr["waveform"]), + # Each note can range from 0-63, so we'll have two tracks, each with the + # same two instruments but at different octave offsets + octave=0, + amp_envelope=Envelope( + attack=instr["amp_envelope"]["attack"], + decay=instr["amp_envelope"]["decay"], + sustain=instr["amp_envelope"]["sustain"], + release=instr["amp_envelope"]["release"], + amplitude=instr["amp_envelope"]["amplitude"], + ), + pitch_envelope=Envelope( + attack=instr["pitch_envelope"]["attack"], + decay=instr["pitch_envelope"]["decay"], + sustain=instr["pitch_envelope"]["sustain"], + release=instr["pitch_envelope"]["release"], + amplitude=instr["pitch_envelope"]["amplitude"], + ), + amp_lfo=LFO( + frequency=instr["amp_lfo"]["frequency"], + amplitude=instr["amp_lfo"]["amplitude"], + ), + pitch_lfo=LFO( + frequency=instr["pitch_lfo"]["frequency"], + amplitude=instr["pitch_lfo"]["amplitude"], + ) + ) + except Exception as e: + if not testing_opts.force_load: + raise e + + logger.debug(f"Creating mappings for {len(data["drum_instruments"])} drum " + f"instruments") + for sample in data["drum_instruments"]: + try: + mapping.drum_instruments[sample["note"]] = DrumInstrument( + name=sample["_comment"], + start_frequency=sample["start_freq"], + start_volume=sample["start_vol"], + steps=[ + DrumSoundStep( + waveform=waveform_from_str(step["waveform"]), + frequency=step["target_freq"], + volume=step["target_vol"], + duration=step["duration"], + ) for step in sample["steps"] + ] + ) + except Exception as e: + if not testing_opts.force_load: + raise e + + return mapping diff --git a/src/midi_to_song/models.py b/src/midi_to_song/models.py new file mode 100644 index 0000000..ec11ce7 --- /dev/null +++ b/src/midi_to_song/models.py @@ -0,0 +1,99 @@ +import logging +from dataclasses import dataclass +from enum import IntEnum +from typing import List, Optional + +from mido import Message + +from utils.logger import create_logger + +logger = create_logger(name=__name__, level=logging.INFO) + + +# All intermediate representations used during conversion + + +@dataclass +class AbsoluteTickMessage: + tick: int # absolute MIDI tick + track: int + msg: Message + msg_idx: int + + +@dataclass +class AbsoluteTimeMessage: + time: float # absolute time in seconds + port: int + msg: Message # note_on, note_off, program_change, control_change (where control = 0) + + +@dataclass +class ChannelState: + program: int # instrument + bank_select_msb: int + bank_select_lsb: int + is_drum: bool + drum_determined_by: DrumDeterminationSource + + +class DrumDeterminationSource(IntEnum): + DEFAULT = 0 + CC = 1 # control change + SYSEX = 2 + + +@dataclass +class AbsoluteTimeMessageWithInstrument: + time: float + port: int + instrument: int # midi instrument + is_drum: bool + msg: Message # note_on and note_off + + +@dataclass +class AbsoluteCompleteNote: + start_time: float + end_time: float + + note: int + velocity: int + + instrument: int + is_drum: bool + + +@dataclass +class AbsoluteCompleteNoteWithTick: + start_tick: int + end_tick: int + + note: int + velocity: int + + instrument: int + is_drum: bool + + +@dataclass +class AbsoluteCompleteChordWithTick: + start_tick: int + end_tick: int + + notes: List[int] + velocity: int + + instrument: int + is_drum: bool + + +@dataclass +class TestingOptionsForLoadInstrumentParams: + force_load: Optional[bool] = False + + +@dataclass +class TestingOptionsForMIDIToSong: + replace_all_melodics_with: Optional[int] = None + generate_code: Optional[bool] = False diff --git a/src/midi_to_song/timeline/parser.py b/src/midi_to_song/timeline/parser.py new file mode 100644 index 0000000..11ad2c2 --- /dev/null +++ b/src/midi_to_song/timeline/parser.py @@ -0,0 +1,305 @@ +import logging +from typing import Dict, List, Tuple + +from mido import MidiFile, tick2second + +from midi_to_song.models import AbsoluteCompleteNote, \ + AbsoluteTickMessage, AbsoluteTimeMessage, \ + AbsoluteTimeMessageWithInstrument, ChannelState, DrumDeterminationSource +from utils.logger import create_logger + +logger = create_logger(name=__name__, level=logging.INFO) + + +def timeline_build(midi_song: MidiFile) -> List[AbsoluteTimeMessage]: + """ + Look through all tracks and convert MIDI's delta tick time to absolute time in + seconds while keeping track of tempo and port changes. + + :param midi_song: `MidiFile` object. + :return: A list of `AbsoluteTimeMessage` objects, which hold time, their original + track, and the MIDI message itself. It is sorted by time in ascending order and + filters out unneeded messages. + """ + # Gather tracks and convert to absolute time + # Unfortunately we can't simply just merge all the tracks because if channel 0 on + # track 0 was a piano and channel 0 on track 1 was a flute, then they collide + # We must keep them separate and build our own global absolute timeline afterward + # Additionally, we must keep track of tempo changes and port changes + + logger.debug("Building global absolute timeline of MIDI messages") + + # First gather all messages with their relative ticks, and convert to absolute ticks + # We need to do this in passes because some messages are globally effective while + # some others are restricted to a track only + logger.debug("Convert relative ticks to absolute ticks and sort") + all_msgs_with_abs_ticks: List[AbsoluteTickMessage] = [] + for i, track in enumerate(midi_song.tracks): + abs_tick = 0 + for j, msg in enumerate(track): + abs_tick += msg.time + all_msgs_with_abs_ticks.append( + AbsoluteTickMessage(tick=abs_tick, track=i, msg=msg, msg_idx=j)) + # Update the sort, time first, then track, then order within the track + all_msgs_with_abs_ticks.sort(key=lambda m: (m.tick, m.track, m.msg_idx)) + + # Now we can convert absolute ticks to absolute time, but we need to keep track of + # tempo changes and MIDI port changes as well (they are also chronological) + logger.debug("Convert absolute ticks to absolute time and track tempo and port " + "changes") + global_timeline: List[AbsoluteTimeMessage] = [] + ticks_per_beat = midi_song.ticks_per_beat + msgs_skipped = 0 + + current_tempo = 500000 # the default + current_abs_time = 0.0 # in seconds (*_time is seconds) + last_abs_ticks = 0 # in MIDI ticks (*_ticks is MIDI ticks) + + track_ports = {i: 0 for i in range(len(midi_song.tracks))} + # Prescan each track for the first meta midi_port message + # Although technically we shouldn't need to do this, some notation software (notably + # MuseScore in my testing) seem to skip midi_port for the first batch of CCs and PC + # in every track, and so CCs and PCs go to port 0 while the note data goes to + # another port + # This sets up a default port that usually works + for i, track in enumerate(midi_song.tracks): + for msg in track: + if msg.type == "midi_port": + track_ports[i] = msg.port + break + + for item in all_msgs_with_abs_ticks: + msg = item.msg + track_idx = item.track + abs_ticks = item.tick + + delta_ticks = abs_ticks - last_abs_ticks + if delta_ticks > 0: + delta_time = tick2second(delta_ticks, ticks_per_beat, current_tempo) + current_abs_time += delta_time + last_abs_ticks = abs_ticks + + if msg.type == "set_tempo": + current_tempo = msg.tempo + elif msg.type == "midi_port": + track_ports[track_idx] = msg.port + # Keep note on/off, program change, sysex, and control change (only if control + # is 0 or 32 which is the bank select MSB/LSB) + elif msg.type in ("note_on", "note_off", "program_change", "sysex") or ( + msg.type == "control_change" and msg.control in (0, 32)): + global_timeline.append( + AbsoluteTimeMessage(time=current_abs_time, port=track_ports[track_idx], + msg=msg)) + else: + msgs_skipped += 1 + logger.debug(f"Global timeline has {len(global_timeline)} messages (skipped " + f"{msgs_skipped}), total song length of {global_timeline[-1].time}s") + + return global_timeline + + +def timeline_find_instrument_data(timeline: List[AbsoluteTimeMessage]) -> List[ + AbsoluteTimeMessageWithInstrument]: + """ + Parse the timeline for program_change and control_change (control = 0) messages to + determine what instrument each message has. + + :param timeline: A list of `AbsoluteTimeMessage` objects. + :return: A list of `AbsoluteTimeMessageWithInstrument` objects. + """ + # Read the timeline sequentially and keep track of program_change and + # control_change (control = 0) to update the current instrument for each channel on + # a port + logger.debug("Finding instrument data for each message in the timeline") + + # Initialize channel states, which keep track of the bank_select and program per + # channel and port + channel_states = {} + highest_port = max([m.port for m in timeline], default=0) + for port in range(highest_port + 1): + for channel in range(16): + # By default, channel 10 starts as drum + channel_states[(port, channel)] = ChannelState(program=0, + bank_select_msb=0, + bank_select_lsb=0, + is_drum=channel == 9, + drum_determined_by=DrumDeterminationSource.DEFAULT) + + # Now search through the timeline and apply control_change (control = 0) and + # program_change messages to the channel states + timeline_with_instrument: List[AbsoluteTimeMessageWithInstrument] = [] + + instr_msgs_processed = 0 + sysex_msgs_processed = 0 + + for item in timeline: + msg = item.msg + port = item.port + channel = getattr(msg, "channel", -1) + + if msg.type == "control_change": + if msg.control == 0: + channel_states[(port, channel)].bank_select_msb = msg.value + elif msg.control == 32: + channel_states[(port, channel)].bank_select_lsb = msg.value + is_drum_bank = ( + channel_states[(port, channel)].bank_select_msb in (120, 121, + 126, + 127) or + channel == 9 + ) + # Only override if last drum determination was weaker than CC + if DrumDeterminationSource.CC >= channel_states[ + (port, channel)].drum_determined_by: + channel_states[(port, channel)].is_drum = is_drum_bank + channel_states[ + (port, channel)].drum_determined_by = DrumDeterminationSource.CC + instr_msgs_processed += 1 + elif msg.type == "program_change": + channel_states[(port, channel)].program = msg.program + instr_msgs_processed += 1 + elif msg.type == "sysex": + data = msg.data + # check for Roland GS + if (len(data) >= 8 and + data[0] == 0x41 and # Roland ID + data[1] == 0x10 and # device ID + data[2] == 0x42 and # GS standard layouts + data[3] == 0x12 and + data[4] == 0x40 and # parameter 1 + 0x10 <= data[5] <= 0x1F and # 0x1n, channel byte, see below + data[6] == 0x15 and # part address + data[7] in (0, 1, 2)): # map byte + # 0x1n is the channel byte, where: + # n=0 is channel 9 (midi channel 10) + # n=1 through 9 is channels 0 through 8 + # n=A through F is channels 10 through 15 + def gs_byte_to_channel(val: int) -> int: + n = val & 0x0F + if n == 0: + return 9 + if n < 10: + return n - 1 + return n + + channel = gs_byte_to_channel(data[5]) + is_drum = data[7] in (1, 2) + # Only override if last drum determination was weaker than sysex + if DrumDeterminationSource.SYSEX >= channel_states[ + (port, channel)].drum_determined_by: + channel_states[(port, channel)].is_drum = is_drum + channel_states[ + (port, + channel)].drum_determined_by = DrumDeterminationSource.SYSEX + # print(f"Roland GS channel {channel} drum: {is_drum}") + instr_msgs_processed += 1 + sysex_msgs_processed += 1 + # check for Yamaha XG + elif (len(data) >= 7 and + data[0] == 0x43 and # Yamaha ID + data[1] == 0x10 and # device ID + data[2] == 0x4C and # XG model ID + data[3] == 0x08 and # multi part params + 0x00 <= data[4] <= 0x0F and # channel, direct mapping + data[5] == 0x07 and # part address + data[6] >= 0): # map byte, technically redundant but + channel = data[4] + is_drum = data[6] > 0 + # Only override if last drum determination was weaker than sysex + if DrumDeterminationSource.SYSEX >= channel_states[ + (port, channel)].drum_determined_by: + channel_states[(port, channel)].is_drum = is_drum + channel_states[ + (port, + channel)].drum_determined_by = DrumDeterminationSource.SYSEX + # print(f"Yamaha XG channel {channel} drum: {is_drum}") + instr_msgs_processed += 1 + sysex_msgs_processed += 1 + elif msg.type in ("note_on", "note_off"): + timeline_with_instrument.append(AbsoluteTimeMessageWithInstrument( + time=item.time, + port=item.port, + instrument=channel_states[(port, channel)].program, + is_drum=channel_states[(port, channel)].is_drum, + msg=msg + )) + logger.debug(f"Global timeline has {len(timeline_with_instrument)} note messages (" + f"processed {instr_msgs_processed} instrument messages, " + f"{sysex_msgs_processed} of which were recognized SysEx messages)") + + return timeline_with_instrument + + +def timeline_group_messages(timeline: List[AbsoluteTimeMessageWithInstrument]) -> List[ + AbsoluteCompleteNote]: + """ + Parse the timeline for note_on and note_off messages to determine the start and end + times of each note. + + :param timeline: A list of `AbsoluteTimeMessageWithInstrument` objects. + :return: A list of `AbsoluteCompleteNote` objects. + """ + # Find all note_on and note_on (velocity = 0) and note_off messages, and pair them + # up + logger.debug("Grouping messages into complete notes in the timeline") + + timeline_with_complete_notes = [] + highest_port = max([0] + [m.port for m in timeline]) + active_notes: Dict[Tuple[int, int], List[AbsoluteCompleteNote]] = { + (port, channel): [] + for port in range(highest_port + 1) + for channel in range(16) + } + last_time = 0 + + current_poly = 0 + max_poly = 0 + + for item in timeline: + msg = item.msg + port_and_channel = (item.port, msg.channel) + last_time = max(last_time, item.time) + + if msg.type == "note_on" and msg.velocity > 0: + # add to the list of playing notes + active_notes[port_and_channel].append( + AbsoluteCompleteNote( + start_time=item.time, + end_time=item.time, # will be updated when note_off found + note=msg.note, + velocity=msg.velocity, + instrument=item.instrument, + is_drum=item.is_drum, + ) + ) + current_poly += 1 + elif msg.type == "note_off" or (msg.type == "note_on" and msg.velocity == 0): + # find the playing note and finish it + for playing_note in active_notes[port_and_channel]: + if playing_note.note == msg.note: + playing_note.end_time = item.time + timeline_with_complete_notes.append(playing_note) + active_notes[port_and_channel].remove(playing_note) + current_poly -= 1 + break + else: + logger.warning(f"Could not find start of note for {item}") + max_poly = max(max_poly, current_poly) + + # handle hanging notes + hanging_count = 0 + for group_key in active_notes: + for playing_note in active_notes[group_key]: + playing_note.end_time = last_time + timeline_with_complete_notes.append(playing_note) + hanging_count += 1 + # no need to remove we're cleaning up + + # sort by start instead of when they ended + timeline_with_complete_notes.sort(key=lambda m: m.start_time) + + logger.debug(f"Global timeline has {len(timeline_with_complete_notes)} note events " + f"(maximum polyphony across all channels and ports was {max_poly} " + f"notes and had to clean up {hanging_count} hanging notes)") + + return timeline_with_complete_notes diff --git a/src/midi_to_song/timeline/processor.py b/src/midi_to_song/timeline/processor.py new file mode 100644 index 0000000..e62114f --- /dev/null +++ b/src/midi_to_song/timeline/processor.py @@ -0,0 +1,318 @@ +import logging +from collections import defaultdict +from copy import deepcopy +from math import ceil +from typing import Dict, List, Tuple + +from arcade.music_types import Song +from midi_to_song import InstrumentParameterMapping +from midi_to_song.models import AbsoluteCompleteChordWithTick, AbsoluteCompleteNote, \ + AbsoluteCompleteNoteWithTick +from utils.logger import create_logger + +logger = create_logger(name=__name__, level=logging.INFO) + + +def timeline_fix_gate_lens(timeline: List[AbsoluteCompleteNote], + song: Song, + mapping: InstrumentParameterMapping) -> List[ + AbsoluteCompleteNote]: + """ + Increase all the durations of the notes to ensure that attack < gate len + release, + so a playback bug is avoided. See https://github.com/microsoft/pxt/pull/11352. + + :param timeline: A list of `AbsoluteCompleteNote` objects. + :param song: The `Song` object to reference the TPM and BPM. + :param mapping: An `InstrumentParameterMapping` object, loaded from + `load_instrument_params`. + :return: A list of `AbsoluteCompleteNote` objects. + """ + logger.debug("Fixing gate lengths of notes in the timeline to avoid playback bug") + + seconds_per_tick = (60 / song.beats_per_measure) / song.ticks_per_beat + + res = [] + + durations_extended = 0 + + for old_note in timeline: + if old_note.is_drum: + # drum instruments don't have gate lengths, so we can skip + res.append(old_note) + continue + instrument_params = mapping.melodic_instruments[old_note.instrument] + attack = instrument_params.amp_envelope.attack / 1000 + release = instrument_params.amp_envelope.release / 1000 + old_duration = old_note.end_time - old_note.start_time + min_duration = attack - release + if old_duration <= min_duration: + min_ticks = min_duration / seconds_per_tick + required_ticks = ceil(min_ticks) + if min_ticks == required_ticks: + required_ticks += 1 + new_note = deepcopy(old_note) + new_duration = required_ticks * seconds_per_tick + new_note.end_time = new_note.start_time + new_duration + durations_extended += 1 + else: + new_note = old_note + res.append(new_note) + + logger.debug(f"Extended {durations_extended} note durations to ensure duration > " + f"attack - release") + + return res + + +def timeline_quantize_to_song_ticks(timeline: List[AbsoluteCompleteNote], + song: Song) -> List[AbsoluteCompleteNoteWithTick]: + """ + Given the song's BPM and TPB, quantize the timeline's start and end times to ticks. + + :param timeline: A list of `AbsoluteCompleteNote` objects. + :param song: The `Song` object to use. + :return: A list of `AbsoluteCompleteNoteWithTick` objects. + """ + tick_time = (60 / song.beats_per_minute) / song.ticks_per_beat # in secs + logger.debug(f"Quantizing note times to ticks based of song BPM of " + f"{song.beats_per_minute} and TPB of {song.ticks_per_beat} - one tick " + f"is 1/{1 / tick_time} ({tick_time}) seconds long") + + res = [] + + for old_note in timeline: + new_start_tick = round(old_note.start_time / tick_time) + # Ensure all notes last for one tick + new_end_tick = max(round(old_note.end_time / tick_time), new_start_tick + 1) + res.append(AbsoluteCompleteNoteWithTick( + start_tick=new_start_tick, + end_tick=new_end_tick, + note=old_note.note, + velocity=old_note.velocity, + instrument=old_note.instrument, + is_drum=old_note.is_drum + )) + + return res + + +def find_all_melodic_instruments(timeline: List[AbsoluteCompleteNoteWithTick]) -> List[ + int]: + """ + Search the timeline for all unique melodic instruments. + + :param timeline: A list of `AbsoluteTimeMessage` objects. + :return: A list of ints, representing what general MIDI instruments are in the song. + """ + logger.debug("Finding all melodic instruments in the timeline") + + return list(sorted(set([m.instrument for m in timeline if not m.is_drum]))) + + +def find_all_drum_notes_used(timeline: List[AbsoluteCompleteNoteWithTick]) -> List[int]: + """ + Search the timeline for all unique drum notes. + + :param timeline: A list of `AbsoluteTimeMessage` objects. + :return: A list of ints, representing what general MIDI drum notes are in the song. + """ + logger.debug("Finding all drum notes in the timeline") + + return list(sorted(set([m.note for m in timeline if m.is_drum]))) + + +def find_all_drum_chords_used(timeline: List[AbsoluteCompleteChordWithTick]) -> List[ + int]: + """ + Search the timeline for all unique drum notes, for a list of chords. + + :param timeline: A list of `AbsoluteCompleteChordWithTick` objects. + :return: A list of ints, representing what general MIDI drum notes are in the song. + """ + logger.debug(f"Finding all drum chords in the timeline") + + return list( + sorted({note for chord in timeline if chord.is_drum for note in chord.notes})) + + +def timeline_group_by_instrument(timeline: List[AbsoluteCompleteNoteWithTick]) -> List[ + List[AbsoluteCompleteNoteWithTick]]: + """ + Split up the timeline by instrument. + + :param timeline: A list of `AbsoluteCompleteNote` objects. + :return: A list of lists of `AbsoluteCompleteNoteWithTick` objects. (Each list of + notes within the list have the same instrument) + """ + logger.debug("Splitting up the timeline by instruments") + + used_melodics = find_all_melodic_instruments(timeline) + used_drums = find_all_drum_notes_used(timeline) + logger.debug(f"Song used {len(used_melodics)} melodic instruments and " + f"{len(used_drums)} unique drum notes") + + tracks = [] + + for melodic in used_melodics: + tracks.append([note for note in timeline if + note.instrument == melodic and not note.is_drum]) + + if len(used_drums) > 0: + tracks.append([note for note in timeline if note.is_drum]) + + logger.debug(f"Split up global timeline into {len(tracks)} tracks") + + return tracks + + +def timeline_split_into_two_tracks_if_needed( + timeline: List[List[AbsoluteCompleteNoteWithTick]]) -> List[ + List[AbsoluteCompleteNoteWithTick]]: + """ + Go through the melodic tracks in the timeline and check the highest and lowest note + in each track. If it can't fit into one track (which has a range limit of 64 notes + from an octave offset) then we use two tracks and move notes as necessary. + + :param timeline: A list of lists of `AbsoluteCompleteNote` objects. + :return: A list of lists of `AbsoluteCompleteNoteWithTick` objects. + """ + logger.debug("Checking necessity to split track into two tracks for range") + + new_tracks: List[List[AbsoluteCompleteNoteWithTick]] = [] + + tracks_that_fit = 0 + tracks_that_split = 0 + + for old_track in timeline: + # drum tracks don't use octave offsets, only 61 samples max as well + if old_track[0].is_drum: + new_tracks.append(old_track) + tracks_that_fit += 1 + continue + + all_notes = [n.note for n in old_track] + highest_note = max(all_notes) + lowest_note = min(all_notes) + + def octave_offset_work(octave: int) -> bool: + return ((((octave - 2) * 12) <= lowest_note) and + (highest_note <= ((octave - 2) * 12 + 63))) + + # does ANY octave offset from [0, 9] work? + if any([octave_offset_work(o) for o in range(0, 10)]): + # we don't need to modify, when constructing the MakeCode Arcade Tracks, + # we'll find the correct octave offset again + new_tracks.append(old_track) + tracks_that_fit += 1 + else: + # split into two tracks, using octave offsets 0 and 6 guarantee covering the + # full shifted MIDI range + low_track = [n for n in old_track if n.note < 41] + high_track = [n for n in old_track if n.note >= 41] + new_tracks.append(low_track) + new_tracks.append(high_track) + tracks_that_split += 1 + + logger.debug(f"{tracks_that_fit} tracks fit within one track's range, " + f"{tracks_that_split} tracks had to split, total of {len(new_tracks)} " + f"tracks in timeline") + + return new_tracks + + +def timeline_group_into_perfect_chords( + timeline: List[List[AbsoluteCompleteNoteWithTick]]) -> List[ + List[AbsoluteCompleteChordWithTick]]: + """ + Go through the tracks in the timeline and group up notes that share the same start + and end tick and velocity and instruments to create "perfect" chords. + + :param timeline: A list of lists of `AbsoluteCompleteNoteWithTick` objects. + :return: A list of lists of `AbsoluteCompleteChordWithTick` objects. + """ + logger.debug("Grouping notes in timeline into perfect chords") + + new_tracks: List[List[AbsoluteCompleteChordWithTick]] = [] + + old_note_count = sum(len(track) for track in timeline) + + for old_track in timeline: + # use dictionaries to quickly find existing chords + # key is a tuple of (start_tick, end_tick, velocity, instrument, is_drum) + # values are notes in the chord + chords: Dict[Tuple[int, int, int, int, bool], List[int]] = defaultdict(list) + + for note in old_track: + key = (note.start_tick, note.end_tick, note.velocity, note.instrument, + note.is_drum) + # if a note has the same start and end tick and velocity and instrument + # they can be played as a chord + chords[key].append(note.note) + + new_track: List[AbsoluteCompleteChordWithTick] = [] + for (start_tick, end_tick, velocity, instrument, + is_drum), notes in chords.items(): + new_track.append(AbsoluteCompleteChordWithTick( + start_tick=start_tick, end_tick=end_tick, + notes=notes, + velocity=velocity, + instrument=instrument, is_drum=is_drum + )) + new_track.sort(key=lambda c: (c.start_tick, c.end_tick)) + new_tracks.append(new_track) + + new_chord_count = sum(len(track) for track in new_tracks) + + logger.debug(f"Created " + f"{sum([sum([1 if len(n.notes) > 1 else 0 for n in t]) for t in new_tracks])}" + f" multi-note chords (dropped from {old_note_count} notes to " + f"{new_chord_count} chords)") + + return new_tracks + + +def timeline_resolve_overlapping_chords( + timeline: List[List[AbsoluteCompleteChordWithTick]]) -> List[ + List[AbsoluteCompleteChordWithTick]]: + """ + Go through the melodic tracks in the timeline and check for chords overlapping in a + track. If they are, move them to an extra track. If there are no free tracks, create + one. This minimizes the number of extra tracks required while keeping the desired + polyphony of MIDI. + + :param timeline: A list of lists of `AbsoluteCompleteChordWithTick` objects. + :return: A list of lists of `AbsoluteCompleteChordWithTick` objects. + """ + logger.debug("Resolving overlapping chords") + + new_tracks: List[List[AbsoluteCompleteChordWithTick]] = [] + old_track_count = len(timeline) + + for old_track in timeline: + new_sub_tracks: List[List[AbsoluteCompleteChordWithTick]] = [[]] + + # don't have to worry about instrument matching because chords in old_track + # should all have the same instrument + for chord in old_track: + # try to place into a sub track + for sub_track in new_sub_tracks: + # if the last note in the subtrack has ended (or it's empty) + # TODO: Test if this needs to be < or <= works + # theoretically it should work fine with <= (and this will save tracks) + # but < will guarantee a "rest" + if len(sub_track) == 0 or sub_track[-1].end_tick < chord.start_tick: + sub_track.append(chord) + break + else: + # no free sub tracks, create + new_sub_tracks.append([chord]) + + # dump all generated sub tracks directly into the new timeline + new_tracks.extend(new_sub_tracks) + + new_track_count = len(new_tracks) + logger.debug(f"Created {new_track_count - old_track_count} extra tracks to handle " + f"overlapping chords (from {old_track_count} to {new_track_count} " + f"tracks)") + + return new_tracks diff --git a/src/midi_to_song/timeline/validation.py b/src/midi_to_song/timeline/validation.py new file mode 100644 index 0000000..619405f --- /dev/null +++ b/src/midi_to_song/timeline/validation.py @@ -0,0 +1,92 @@ +import logging +from typing import List + +from arcade.music_types import Song +from midi_to_song import InstrumentParameterMapping +from midi_to_song.models import AbsoluteCompleteChordWithTick +from utils.logger import create_logger + +logger = create_logger(name=__name__, level=logging.INFO) + + +def timeline_checks(song: Song, + timeline: List[List[AbsoluteCompleteChordWithTick]], + mapping: InstrumentParameterMapping): + """ + Run some basic checks on the timeline to verify assumptions before mapping to the + MakeCode Arcade dataclasses. + + :param song: The MakeCode Arcade `Song` object that will be added to, with the + correctly configured BPM and TPM. + :param timeline: A list of lists of `AbsoluteCompleteChordWithTick` objects. + :param mapping: An `InstrumentParameterMapping` object, loaded from + `load_instrument_params`. + :raises ValueError: If any violations are detected. + """ + logger.debug("Running checks on the timeline") + # Track count must be less than 256 + if len(timeline) > 255: + raise ValueError(f"Too many tracks in the timeline! (max of 255, found " + f"{len(timeline)}) Reduce polyphony or unique instruments.") + for track in timeline: + if len(track) == 0: + raise Warning("Empty track in the timeline!") + track_is_drum = track[0].is_drum + # Sort by start tick as required, just in case + track.sort(key=lambda c: c.start_tick) + # The start and end ticks must be less than 65536 + highest_tick = max( + [chord.start_tick for chord in track] + [chord.end_tick for chord in track]) + if highest_tick > 65535: + raise ValueError(f"Chord start or end tick is too high! (max of 65535, " + f"found {highest_tick}) Decrease song length or reduce " + f"BPM/TPB at the cost of worse timing.") + # The highest and lowest notes must fit within 64 notes of an integer octave + # offset for melodic instruments + if not track_is_drum: + highest_note = max([max(chord.notes) for chord in track]) + lowest_note = min([min(chord.notes) for chord in track]) + + def octave_offset_work(octave: int) -> bool: + return ((((octave - 2) * 12) <= lowest_note) and + (highest_note <= ((octave - 2) * 12 + 63))) + + if not any([octave_offset_work(o) for o in range(0, 10)]): + raise ValueError(f"Track range too big to fit! (please report)") + # Chord must have less than 256 notes + max_chord_polyphony = max([len(chord.notes) for chord in track]) + if max_chord_polyphony > 255: + raise ValueError(f"Chord has too many notes! (max of 255, found " + f"{max_chord_polyphony})") + # Chords must last at least 1 tick + min_chord_duration = min([chord.end_tick - chord.start_tick for chord in track]) + if min_chord_duration < 1: + raise ValueError(f"Chord violated minimum duration time! (min of 1 tick, " + f"found {min_chord_duration} ticks, please report)") + # All chords must have the same instrument if melodic or all chords must be + # marked as drums in a drum track + if track_is_drum: + if any([not chord.is_drum for chord in track]): + raise ValueError(f"Found non drum chord in drum track! (please report") + else: + instruments = set([chord.instrument for chord in track]) + if len(instruments) > 1: + raise ValueError(f"Track has more than one instrument! (found " + f"{len(instruments)}, please report)") + # Ensure attack < gate length + release in the amp envelope for all chords, + # otherwise a bug is triggered and the simulator freezes (see + # https://github.com/microsoft/pxt/pull/11352) + if not track_is_drum: + instrument = mapping.melodic_instruments[track[0].instrument] + attack = instrument.amp_envelope.attack + release = instrument.amp_envelope.release + ms_per_tick = (60 / song.beats_per_minute) / song.ticks_per_beat * 1000 + chord_times = [ + round((chord.end_tick - chord.start_tick) * ms_per_tick) for + chord in track] + if any([(not (attack < (chord_time + release))) for chord_time in + chord_times]): + raise ValueError("Found chord where instrument's attack time >= " + "gate length + release time! (please report)") + + logger.debug("Timeline passes all checks") diff --git a/src/utils/__init__.py b/src/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/utils/strings.py b/src/utils/strings.py new file mode 100644 index 0000000..181d3f2 --- /dev/null +++ b/src/utils/strings.py @@ -0,0 +1,37 @@ +import argparse + + +# Thanks Gemini +def parse_range(value): + """ + Parses a string of numbers, ranges, or a mix (e.g., '0-30' or '0,2,4,10-30') + and returns a sorted list of unique integers. + """ + result = set() + + # Split by commas to handle individual numbers or sub-ranges + for part in value.split(','): + part = part.strip() + if not part: + continue + + # Check if it's a range (e.g., 10-30) + if '-' in part: + try: + start, end = map(int, part.split('-')) + if start > end: + raise argparse.ArgumentTypeError( + f"Invalid range: {part} (start cannot be greater than end)") + # +1 to make the end inclusive, which is standard for CLI ranges + result.update(range(start, end + 1)) + except ValueError: + raise argparse.ArgumentTypeError( + f"Invalid range format: '{part}'. Expected 'start-end'.") + else: + # It's a single number + try: + result.add(int(part)) + except ValueError: + raise argparse.ArgumentTypeError(f"Invalid integer: '{part}'") + + return sorted(list(result))