Your Bambu A1 sings a different song every print. Not a firmware mod — the jingle is plain gcode in the machine start/end blocks, so every print can carry its own tune.
It also contains, as far as we can tell, the first full write-up of the M1006
music grammar: note encoding, duration units, the three-voice layout, and
Bambu's own per-motor loudness tables. OpenBambuAPI
has one example line and "allows playing music with the stepper coils"; the rest
of this was decoded from Bambu's own MIDI→gcode converter bundle and verified
against the stock jingles in a real sliced file.
python3 jukebox.py list # the songbook, and what it has played
python3 jukebox.py preview all # hear every song as a WAV, before printing
python3 jukebox.py concert # a gcode that ONLY sings — send it like a print
python3 jukebox.py splice job.gcode # swap a fresh song into a sliced file
python3 jukebox.py restore job.gcode # put Bambu's stock jingles back
⚠️ The printer ignores everyM1006unless Settings → Print options → Sound is ON. If nothing plays, check that first.
Nothing here touches firmware. The worst case is an ugly song.
M17 ; energize the motors — they ARE the speaker
M400 S1 ; settle
M1006 S1 ; begin sequence
M1006 L70 M70 N99 ; per-motor master volume (X, Y, Z)
M1006 A<n> B<d> L<v> C<n> D<d> M<v> E<n> F<d> N<v>
... ; one line per timeline slice
M1006 W ; wait for playback to finish
M18 ; release the motors
Three motors, so three simultaneous voices — a hard ceiling, confirmed by Bambu's own wiki.
| voice | motor | note | duration | volume |
|---|---|---|---|---|
| 1 | X | A |
B |
L |
| 2 | Y | C |
D |
M |
| 3 | Z | E |
F |
N |
- note =
MIDI − 23, clamped at ≥ 0.0is silence. Legal1..72= MIDI 24..95 = C1..B6. So37is middle C. - duration is in hundredths of a second —
B20is 200 ms. - volume =
floor(master / 100 × CURVE[motor][note − 1]), and is omitted when it works out to 0. The three 72-entry response tables are involume_curves.json. - A note longer than 2.00 s gets re-struck in 2.00 s chunks.
Stepper motors are nothing like flat. Straight out of the response curves:
- C3–F#3 is a dead zone on all three motors. Never put anything there.
- Y is the strongest overall — ≥90 across F#1..B6, dead only at F2 and D#3–F3. Give it the lead melody.
- X is loud at C2 and again F#4–D5, dead B2–F#3. Good for a counter-melody up high.
- Z is loud C1–C2 and again from D5 up. Bass down low, or sparkle up top.
That is exactly why Bambu's own tune sits at C4–C5 over a quiet D4 pedal.
The notch that will catch you: X craters just above D5. It reads 99 at D5 and
99 again from F#5 up, but in between — D#5 = 75, E5 = 55, F5 = 75. A line that
walks up past D5 falls into a half-volume hole for three semitones. Go up an
octave instead: X is a flat 99 across C6–G6.
songbook.py's lint() checks all of this for you — out-of-range notes, notes
sitting in a motor's dead spot, and voices whose lengths don't add up.
Songs are readable, not gcode. Each one has a start phrase and a matching end phrase, so a single song bookends a print — a question when it begins and an answer when it finishes.
dict(
name="Bell Tower",
idea="Slow, wide, plagal — the amen cadence. Big spaces between the "
"voices so the motors ring instead of chatter.",
tempo=100,
start=dict(Y="C4/2 F4/2", X="r/1 C5/3", Z="F2/2 C2/2"),
end=dict( Y="F4/2 C4/2", X="A4/2 E4/2", Z="F2/2 C2/2"),
),PITCH/BEATS, r for a rest, fractional beats fine. Then:
python3 jukebox.py preview "Bell Tower" # renders a WAV so you can hear it
python3 jukebox.py show "Bell Tower" # the gcode it compiles toThe nine songs in songbook.py are originals, written against the loudness map
above. previews/AUDITION_REEL.wav is all of them back to back.
A caveat worth stating plainly: the pitches, rhythm and per-motor loudness are exact, straight off Bambu's tables. The timbre in the WAV previews is a synth guess at what a stepper sounds like. Use them to check the notes, not the tone.
M1006 blocks live in the machine start/end gcode, so the natural place to swap
one in is after slicing, before uploading. In OrcaSlicer:
Print Settings → Others → Post-processing Scripts
python3 /path/to/a1-jukebox/orca_hook.py
Orca waits for the script, so the song is chosen for the print you just started — not by a timer, and not popped off a queue built in advance. The picker takes the least-recently-played song and never repeats inside the last three, so consecutive prints never sound the same.
ledger.json is the memory: what exists, what played and when, on which file.
It fails safe. Any error and the gcode is left exactly as sliced — a print must never break because the music didn't work. The hook is stdlib-only for the same reason, since Orca's flatpak sandbox has no site-packages.
splice is careful about what it touches: on a 221,937-line gcode, a diff shows
only the two jingle blocks changed. Sliced .3mf files work too — every
Metadata/plate_*.gcode inside is patched and the .md5 sidecar refreshed.
Generates a gcode that only sings: no heating, no homing, no extrusion, no
motion. Send it like a print and the printer plays the whole songbook standing
still. It carries M73 progress lines the way Bambu's own converter does, so
the job reports a real duration instead of finishing the instant it starts.
Verified inert before it ever goes to a machine — zero heat, home, motion or extrude commands in the file.
python3 jukebox.py concert # writes concert.gcodeexamples/concert.gcode is a pre-built one: all nine songs, 71 seconds.
m1006.py |
the instrument: encode() notation→gcode, decode()/describe() gcode→notes, render_wav() |
volume_curves.json |
Bambu's three per-motor response tables, extracted exactly |
songbook.py |
the songs, in readable notation, plus lint() |
jukebox.py |
pick / preview / splice / restore / concert, and the ledger |
orca_hook.py |
the post-processing hook — a song chosen when you hit Print |
previews/ |
a WAV of every song, and the audition reel |
Python 3, standard library only. No dependencies.
Built by Iris with Ivy, August 2026, on an A1 with an AMS Lite. The idea of publishing it was Ivy's mum's.
MIT — see LICENSE.