Audio
Import a sound file, then either attach an AudioSource to an object or call api.playSound from a script. Sounds ride inside the project and inside the exported game, so a shipped game.html carries its own audio and needs no server.
Importing a sound
Drop an audio file onto the editor, or use + Import. Accepted: .ogg, .mp3, .wav, .m4a, .aac, .flac - whatever the browser can decode - and .mid / .midi, which it cannot (see MIDI).
The id is the file name without its extension: music.ogg imports as music. Re-importing the same name replaces that sound, so an edited file updates every AudioSource pointing at it rather than forking a second copy.
Imported sounds live under Audio in the Asset Browser. Double-click one to hear it; selecting one shows its format and size, a play button, and whether it is a recording or a score - and, for a MIDI, the performance controls with a play button to audition them. Drag one onto an AudioSource's clip field to set it.
The bytes are stored encoded, exactly as imported, and decoded when the game loads. Decoding wants the host's sample rate, so a project that baked decoded samples at your machine's rate would resample on everyone else's - the same reason textures compress at load rather than at import.
MIDI
A .mid is a score, not a recording: a list of which note starts when, on which instrument, and how hard. That makes it thousands of times smaller than audio of the same music. The soundtrack this was built for is 18 minutes of solo piano in 17 KB - the same piece encoded as .ogg runs to tens of megabytes, which is not something you put in a game that is otherwise 600 KB.
Nothing decodes a score, so the engine performs it: Awaken embeds a General MIDI synthesiser (webaudio-tinysynth, Apache-2.0, about 11 KB gzipped) that builds every note out of oscillators. All 128 GM instruments and the drum channel work, along with program change, pitch bend, channel volume, pan, expression and the sustain pedal.
Import and use a .mid exactly like a recording - the AudioSource clip picker and api.playSound do not care which kind it is.
What to expect:
- It is synthesised, not sampled. There is no instrument bank, which is the point: a soundfont good enough to sound real is megabytes and would undo the saving. Expect a clean, synthetic tone rather than a recorded piano. Solo and small ensembles fare best; big orchestral scores are where the gap shows most.
- One MIDI at a time. A second one starting replaces the first. Recordings are unaffected - any number of those play at once, including over a MIDI.
- Nothing is decoded ahead, so length costs nothing. An 18-minute piece uses no more memory than a 20-second one, unlike a recording, which is decoded whole into RAM before it plays.
If a MIDI matters more than the bytes, render it to .ogg with a soundfont you like and import that instead. Both paths work; this is a trade, not a limitation.
Changing the performance
A recording is a fixed performance. A score is not - the engine is still deciding how to play it when the game starts, so these cost nothing: no second file, no re-encode.
| Control | Range | What it does |
|---|---|---|
tempo | multiplier, default 1 | Speed. 2 is twice as fast, 0.5 half. Pitch is unaffected - this conducts the score rather than speeding up a tape. A score that changes tempo on its own still does, in proportion. |
transpose | semitones, default 0 | Shift every melodic note. 12 is an octave up, -5 a fourth down. Percussion is left alone: a note number on the drum channel picks which drum, not a pitch. Notes pushed past the end of the keyboard stop at the edge rather than wrapping. |
instrument | GM program 0–127, or -1 | Force every melodic channel onto one instrument whatever the score asks for. 10 music box, 12 marimba, 19 church organ, 46 harp, 52 choir aahs, 73 flute. -1 plays the written instruments. |
timbre | rich / chiptune | How each note is built. rich uses two or more oscillators; chiptune uses one - thinner, cheaper, and the right answer for a game that wants to sound like 1988 hardware. |
reverb | 0–1, default 0.3 | Room size. 0 is dry and close. |
Where they live
A sound carries its own performance, set in its asset inspector (Assets → Audio → select it). Tune the instrument, key, speed and timbre once, with a play button right there to hear each change, and every AudioSource playing that sound gets it - as does api.playSound("theme") with no options at all. The instrument picker lists all 128 General MIDI programs by name, so it is searchable rather than a number you have to look up.
Everything but Tempo restarts the audition when you change it. Tempo does not, because the synthesiser can change speed mid-performance - which is the same reason setSoundTempo exists.
An AudioSource overrides it per object: tick overrideMidi and its own four fields apply instead. Leave it off (the default) and the object plays the sound the way its asset says to. A script overrides field by field, so asking for a different tempo keeps the instrument the asset was tuned to.
asset profile → AudioSource (overrideMidi) → api.playSound opts.midiThey are fields on the AudioSource for authoring, and opts.midi on api.playSound for scripts:
ts
api.playSound("theme", { loop: true, midi: { instrument: 10, transpose: -12, tempo: 0.8 } });Tempo can change while the music plays, which is the one that earns its keep - the same theme tightens as the danger rises, on the bar it was already on, with no crossfade and no second file:
ts
onUpdate(api: ScriptApi) {
const danger = this.enemiesNearby / 10;
api.setSoundTempo(this.music, 1 + danger * 0.4); // 1.0 calm → 1.4 frantic
}The other four are applied when the sound starts, so changing one means playing it again.
Playing a sound
The AudioSource component
Add AudioSource from the Inspector's + Add Component menu.
| Field | Type | Default | Meaning |
|---|---|---|---|
clip | asset ref | (none) | Which sound, by id. The picker lists what the project has imported. |
volume | number | 1 | Linear gain. 1 is the clip's own recorded level. |
loop | bool | false | Repeat until the scene ends. |
playOnStart | bool | true | Play automatically the moment Play begins. |
overrideMidi | bool | false | MIDI only. Off: play the sound's own performance. On: this object differs. |
tempo | number | 1 | MIDI only, and only with overrideMidi on. |
transpose | number | 0 | MIDI only. Semitones. |
instrument | number | -1 | MIDI only. GM program, or -1 for as written. |
timbre | enum | rich | MIDI only. rich or chiptune. |
The last four appear only when overrideMidi is on. All five are ignored for a recording, and cannot be hidden for one: a component only sees its own fields and cannot look the clip up to find out what kind of sound it is.
When Play starts, the runtime walks every object with both a Transform and an enabled AudioSource, and plays the clip of each one whose playOnStart is set. That is the component's whole lifecycle: it fires at start, and there is no built-in trigger to fire it again. For a sound driven by gameplay, turn playOnStart off and call api.playSound from a script.
📸 Screenshot - save as img/media-audiosource-inspector.png
The Inspector showing an AudioSource component with its clip picker, volume, loop, and playOnStart fields on a selected object.
Background music
Attach an AudioSource to whatever outlives the scene - the player object is the usual choice - set the clip, and turn loop on. Audio is not positional (see Limitations), so the object's Transform makes no difference to what you hear; attaching it to the player is about lifetime, not about place.
Pressing Stop, and loading another scene, both silence it.
From a script
ts
export default class Jukebox implements ScriptBehavior {
private music: number | null = null;
onStart(api: ScriptApi) {
this.music = api.playSound("theme", { volume: 0.6, loop: true });
}
onUpdate(api: ScriptApi) {
if (api.input.isDown("m") && this.music !== null) { // mute
api.stopSound(this.music);
this.music = null;
}
}
}playSound(clip, opts?) returns a handle, or null if the project has no sound by that id. stopSound(handle) ends that one sound; a null handle is a no-op, so calling it twice is safe. You do not need to stop anything on the way out - Play stopping and a scene load stop everything.
How it works
One BrowserAudioEngine serves both the editor's Game view and the exported player, so what you hear while authoring is what ships. Headless contexts and tests get a NoopAudioEngine, which is why gameplay logic stays testable without a browser.
Details worth knowing:
- The context starts suspended. Browsers block audio until the player interacts with the page, so the engine resumes on the first
pointerdownorkeydown. A sound that fires before any input is silent until then - including music started at Play. - Each sound decodes once, however many times it plays, and the decoded buffer is cached by id.
- A sound can be stopped before it starts.
playhands back its handle immediately, while the decode is still running; stopping in that gap means it never starts. Stop pressed during the first frame of Play is exactly this case. - A file that will not decode is silent, not fatal. One bad import does not affect the rest.
Shipping
Sounds are saved in the .awaken project as raw bytes in its binary blob, and exported into game.html as base64 - the same carriage fonts and textures use.
Export ships every sound in the project, not just the ones it can see referenced. A sound is usually named by a bare string - an AudioSource clip, or api.playSound("hit") inside a script - and no component walk can find the second kind. Trimming to what the walk finds is how a game ships silent. Audio is large, so this costs real bytes: delete sounds you are not using rather than leaving them in the project.
Limitations
- 2D only. No positional audio: no distance falloff, no panning by object position.
volumeis a flat gain. That is right for music, menus and UI, and wrong for a footstep across the room. - No mixing groups or buses. Each sound builds its own source → gain → destination chain, so there is no master music/SFX slider short of tracking your own handles.
- No fades.
stopSoundis immediate. - No streaming. A recording is decoded whole into memory before it plays, so a long track costs its full decoded size in RAM. MIDI has no such cost - it is sequenced as it plays.
- One MIDI at a time, as above.
See also
- Scripting API - the full
ScriptApi, includingplaySound,stopSoundandsetSoundTempo. - Import Overview - the other file types the editor accepts.
- Export as one file - what ends up inside
game.html. - In-Game UI - the other overlay system that ships with your game.