Music
Chippy has a dynamic, layer-based music system. A song can be randomized so it's different each time, and can change certain elements based on the gameplay.
Built-in Songs
To simply re-use one of the Chippy songs for your stage, use one of these song names for the song
property in your level config json.
1 2 3 4 5 6 | { "title": "#intro.title", "description": "#intro.description", "song": "017", // ... } |
Stage song names
Stage | Resource Folder Name | Song Name |
---|---|---|
Neophyte | intro |
017 |
Kraken | octopus |
019 |
Guardian | mech |
023 |
Execution | fuse |
025 |
Overgrowth | trench |
015 |
Anomaly | onion |
022 |
Phobia | hunter |
012 |
Medusa | tentacle |
020 |
Goliath | claw |
002 |
Hermit | orb |
021 |
Monarch | frame |
013 |
Prospector | laser |
024 |
Storm | storm |
010 |
Xulgon | invasion |
011 |
Download campaign songs
These are the song files used in the campaigns, for you to learn from or remix: music.zip
Custom Songs
Example
Here's an example song: bouncy_song.zip
To test it in your stage, unzip the music
folder and place it somewhere in your plugin folder.
In the stage config json file, enter the path to the song config file.
1 | "song": "myStageName/music/bouncy_song", |
These are three different files included:
bouncy_samples.mp3
This is an audio file with all our loops crammed into it. We could have used a separate audio file for each loop if we wanted, but then we'd need to create a sample config file for each.
bouncy_sample_config.json
This is the sample config file. It defines which parts of an audio file can be used as sampled loops in the song config.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | { "filename": "bouncy_samples", "beatsPerMinute": 128, "beatsPerBar": 4, "samples": { "drum0": { "startBar": 0, "lengthBars": 2 }, "drum1": { "startBar": 2, "lengthBars": 2 }, "drum_fill": { "startBar": 4, "lengthBars": 1 }, "bass0": { "startBar": 5, "lengthBars": 2 }, "bass1": { "startBar": 7, "lengthBars": 2 }, "synth0": { "startBar": 9, "lengthBars": 2 }, "synth1": { "startBar": 11, "lengthBars": 2 }, "piano0": { "startBar": 13, "lengthBars": 8 }, "piano1": { "startBar": 21, "lengthBars": 4 }, "vox": { "startBar": 25, "lengthBars": 2 }, } } |
bouncy_song.json
This is song config file. It uses samples defined in the sample config file and arranges them into layers, each layer playing up to one sample at a time.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | { "beatsPerMinute": 128, "beatsPerBar": 4, "properties": { "drumsVolume": { "type": "Func<Float>", "value": "0.66f + sin(stageTime * 0.25) * 0.33f" }, "drumsHighpass": { "type": "Func<Float>", "value": "sin(stageTime * 0.1) * 0.2f" }, "bassVolume": { "type": "Func<Float>", "value": "0.7f + sin(stageTime * 0.33) * 0.3f" }, "synthVolume": { "type": "Func<Float>", "value": "0.6f + sin(stageTime * 0.2) * 0.2f" }, "voxVolume": { "type": "Func<Float>", "value": "0.9f + sin(stageTime * 0.66) * 0.1f" }, "voxLowpass": { "type": "Func<Float>", "value": "fastSin(stageTime * 0.275) * 0.2f" }, "voxHighpass": { "type": "Func<Float>", "value": "fastSin(stageTime * 0.325) * 0.2f" }, }, "layers": { "drums": { "volume": "drumsVolume", "segments": [ { "name": "seg0", "lengthBars": "rand.Int(1, 16) * 2", "sample": "bouncy_sample_config.drum0", "next":"seg${rand.Int(0, 2)}", "volume":1, }, { "name": "seg1", "lengthBars": "rand.Int(1, 3) * 1", "sample": "bouncy_sample_config.drum_fill", "next":"seg2", "volume":1, }, { "name": "seg2", "lengthBars": "rand.Int(1, 16) * 2", "sample": "bouncy_sample_config.drum1", "next":"seg${rand.Int(2, 4)}", "volume":1, }, { "name": "seg3", "lengthBars": "rand.Int(1, 3) * 1", "sample": "bouncy_sample_config.drum_fill", "next":"seg0", "volume":1, }, ] }, "bass": { "volume": "bassVolume", "segments": [ { "name": "seg0", "lengthBars": "rand.Int(3, 6) * 2", "next":"seg1", }, { "name": "seg1", "lengthBars": "rand.Int(1, 16) * 2", "sample": "bouncy_sample_config.bass0", "next":"seg${rand.Int(0, 3)}", "volume":1, }, { "name": "seg2", "lengthBars": "rand.Int(1, 8) * 2", "sample": "bouncy_sample_config.bass1", "next":"seg0", "volume":1, }, ] }, "synth": { "volume": "synthVolume", "segments": [ { "name": "part0", "lengthBars": "rand.Int(8, 12) * 2", }, // by not specifying "next", it will automatically go to the next segment { "name": "part1", "lengthBars": "rand.Int(1, 16) * 2", "sample": "bouncy_sample_config.synth0", "next":"part2", "volume":1, }, { "name": "part2", "lengthBars": "rand.Int(1, 16) * 2", "sample": "bouncy_sample_config.synth1", "next":"part0", "volume":1, }, ] }, "piano": { "volume": "0.8f", "segments": [ { "name": "seg0", "lengthBars": "rand.Int(14, 16) * 2", "next":"seg1", }, { "name": "seg1", "lengthBars": "rand.Int(1, 6) * 8", "sample": "bouncy_sample_config.piano0", "next":"seg2", "volume":1, }, { "name": "seg2", "lengthBars": "rand.Int(1, 4) * 4", "sample": "bouncy_sample_config.piano1", "next":"seg0", "volume":1, }, ] }, "vox": { "volume": "voxVolume", "highpass":"voxHighpass", // filter out low frequency sounds "lowpass":"voxLowpass", // filter out high frequency sounds "segments": [ { "name": "seg0", "lengthBars": "rand.Int(4, 14) * 2", "next":"seg${rand.Int(0, 2)}", }, { "name": "seg1", "lengthBars": "rand.Int(1, 16) * 2", "sample": "bouncy_sample_config.vox", "next":"seg${rand.Int(0, 2)}", "volume":1, }, ] }, } } |
Tip
This example only uses one sample config file, but you can create as many as you want.
Here's the same song playing twice, and progressing slightly differently each time:
Workflow
Audio
Find some music loops that sound good together. They should probably all be the same bpm.
I used Ableton Live, but any DAW should work.
Lay out the clips so they play one after another, and export it to mp3.
Sample Config
Now that we have our audio file, it's time to specify which sections of it to use as samples.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | { // specify the name of the audio file. since it is in the same folder, no need to specify the full path "filename": "bouncy_samples", "beatsPerMinute": 128, "beatsPerBar": 4, "samples": { "drum0": { "startBar": 0, "lengthBars": 2 }, // the first bar is 0, not 1 "drum1": { "startBar": 2, "lengthBars": 2 }, "drum_fill": { "startBar": 4, "lengthBars": 2 }, "bass0": { "startBar": 5, "lengthBars": 2 }, "bass1": { "startBar": 7, "lengthBars": 2 }, "synth0": { "startBar": 9, "lengthBars": 2 }, "synth1": { "startBar": 11, "lengthBars": 2 }, "piano0": { "startBar": 13, "lengthBars": 8 }, "piano1": { "startBar": 21, "lengthBars": 4 }, "vox": { "startBar": 25, "lengthBars": 2 }, } } |
To find the lengthBars
value, count the number of bars each sample is comprised of:
Simply increment startBar
by the lengthBars
of each sample, but be sure to start with bar 0
, not 1
.
Song Config
We have our samples defined now, and can use them to create a song.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | { // pick whatever bpm your samples are in (this value can be dynamically changed in SOME ways, but it's pretty janky, and can't use custom song properties) "beatsPerMinute": 128, "beatsPerBar": 4, // define custom properties that the layers can refer to "properties": { "bassVolume": { "type": "Func<Float>", "value": "0.7f + sin(stageTime * 0.33) * 0.3f" }, // ... }, // when the song starts, each layer will begin playing on its first segment "layers": { "bass": { "volume": "bassVolume", // using a custom property to control volume of this layer "segments": [ // if there is no sample defined, this section will be silent { "name": "seg0", "lengthBars": "rand.Int(3, 6) * 2", "next":"seg1", }, // after seg1, the next segment will randomly be selected from seg0, seg1, seg2 { "name": "seg1", "lengthBars": "rand.Int(1, 16) * 2", "sample": "bouncy_sample_config.bass0", "next":"seg${rand.Int(0, 3)}", "volume":1, }, { "name": "seg2", "lengthBars": "rand.Int(1, 8) * 2", "sample": "bouncy_sample_config.bass1", "next":"seg0", "volume":1, }, ] }, // ... } } |
Custom Property Examples
Here are some examples of ways to influence the song with the current gameplay state.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | { "properties": { // true if the boss form number is 0 (BossFormNumber is shorthand for stage.GetUnit('boss').CurrFormNum) "firstForm": { "type": "Func<Bool>", "value": "stage.BossFormNumber == 0" }, "secondForm": { "type": "Func<Bool>", "value": "stage.GetUnit('boss').CurrFormNum == 1" }, // the volume of the hats is based on whether the player is shooting or not (we must check if we're on the menu, because no player exists on the menu stage) "hatsVolume": { "type": "Func<Float>", "value": "isMenuStage ? 0f : player.Input.ShootInputPercent * 0.25f" }, }, } |
Segment Actions
Each segment of a layer can define actions to trigger when it starts. In the campaign songs, this is used to control which sections are playing.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | { "properties": { // this property will be changed with an action "shouldPlayMelody": { "type": "Bool", "value": true }, }, "layers": { "section-control": { "volume": 0, "segments": [ { "name": "intro", "next": "melody", "lengthBars": 8, "actions": [ { "action": "SetValue", "name": "shouldPlayMelody", "value": false }, ] }, { "name": "melody", "next": "intro", "lengthBars": 8, "actions": [ { "action": "SetValue", "name": "shouldPlayMelody", "value": true }, ] }, ], }, "melody": { // the volume of this layer depends on the bool value modified by the action "volume": "shouldPlayMelody ? 1 : 0", "segments":[ // ... ], }, }, } |