I just remembered that I promised @UnsignedArduino that I would make a post linking to the new song format! Well, better late than never.
The format of the song buffer is documented here:
That file also contains all of the code for encoding/decoding the songs from buffers (in this case, uint8Arrays). The default instruments are all defined in the getEmptySong function.
This code is mirrored in the arcade game engine here:
If I were, hypothetically, making a webapp that converted midi files into the new song format, then I would:
Copy all of this file
Also copy these two functions
there are also a bunch of utility functions for manipulating songs here:
if (note.enharmonicSpelling === "flat") {
return note.note < octave * 12;
}
else if (note.enharmonicSpelling === "sharp") {
return note.note <= octave * 12 + 1
}
return note.note < octave * 12 + 1;
}
export function addNoteToTrack(song: pxt.assets.music.Song, trackIndex: number, note: pxt.assets.music.Note, startTick: number, endTick: number) {
return {
...song,
tracks: song.tracks.map((track, index) => index !== trackIndex ? track : {
...track,
notes: addToNoteArray(track.notes, note, startTick, endTick, track.instrument.octave, !!track.drums)
.filter(ev => (ev.startTick <= startTick || ev.startTick >= endTick) && (ev.endTick >= endTick || ev.endTick <= startTick))
})
}
}
4 Likes
Once I’m done doing hot fixes, I will write some proper docs explaining the format in more detail!
3 Likes
One other thing I just thought of, note events for tracks need to be sorted by the start tick (e.g. they need to be encoded in chronological order).
2 Likes
Also one question: I have code like this:
song.tracks[0].notes.append(
NoteEvent(
notes=[
Note(
note=49, # Lowest C in octave
enharmonicSpelling=EnharmonicSpelling.NORMAL
)
],
startTick=0,
endTick=8
)
)
Why is 49 “middle C?” Is it a MIDI note index? Or something else?
1 Like
richard
February 3, 2023, 10:19pm
6
yup! the numbers are midi note numbers
2 Likes
Hi @richard , sorry to bother you but right now, I’m having difficulty with the instrument octaves. I’m setting the octave to 2 here:
logger.debug(f"Received arguments: {args}")
input_path = Path(args.input)
logger.debug(f"Input path is {input_path}")
midi = MidiFile(input_path)
logger.debug(f"MIDI is {midi.length}s long, using {ceil(midi.length)} measures")
song = getEmptySong(ceil(midi.length))
song.ticksPerBeat = 100
song.beatsPerMeasure = 10
song.beatsPerMinute = 60
song.tracks[0].instrument.octave = 2
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"):
I’m using a test MIDI file that has a glissando of notes from MIDI note 21 to 108, the lowest and highest notes on a piano. For some reason, the notes appear to be “shifted” by a couple of octaves. You can hear it here:
Do you have any idea why? I’ve tried other octaves like 0, 1, and 3 but they all produce shifted ranges too. Do I have to split up the notes to separate tracks that handle instruments with different octaves?
Progress update I guess:
Reload the page if you want to stop the song, because restarting the simulator doesn’t stop the song for some reason.
4 Likes
Yeah, known bug… Not sure when it started.
2 Likes
richard
August 16, 2023, 10:03pm
10
Also WOW the song editor cannot handle that. Might have to look into re-implementing it…
2 Likes
(self-promotion hehe)
The Python tool is mostly done, I only need to iron out a couple of bugs:
I will be working on a web-based version.
2 Likes
Are you saying there’s another way I can make music?
2 Likes
Yes! My script can convert a .midi
file (which can be exported from any music score editor) to MakeCode Arcade’s new(ish) song format! It works much better than ArcadeMIDI and Musical-Images and does not use an extension.
2 Likes
UnsignedArduino:
I’m using a test MIDI file that has a glissando of notes from MIDI note 21 to 108, the lowest and highest notes on a piano. For some reason, the notes appear to be “shifted” by a couple of octaves.
The reason is here, I guess:
The “& 0x3f” masked off notes range to 0~63+octave*12, so whatever the octave set it can’t arrange 88 difference notes. So need 2 instruments at lease, 2 tracks as well(each track contain only 1 intrument).
1 Like
Thank you so much!!! I just fixed the converter so that all the high notes work.
1 Like
Hi @richard sorry to bother you, but I can’t seem to find the declaration of U
: (which I’m assuming is the same as pxt.U
)
export function encodeSongToHex(song: Song) {
const encoded = encodeSong(song);
return U.toHex(encoded);
}
export function decodeSongFromHex(hex: string) {
const bytes = pxt.U.fromHex(hex);
return decodeSong(bytes);
}
Is it a namespace of utility functions?
function get16BitNumber(buf: Uint8Array, offset: number) {
const temp = new Uint8Array(2);
temp[0] = buf[offset];
temp[1] = buf[offset + 1];
return new Uint16Array(temp.buffer)[0];
}
export function encodeSongToHex(song: Song) {
const encoded = encodeSong(song);
return U.toHex(encoded);
}
/**
* 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
current += 2;
for (const note of encodedNotes) {
out.set(note, current);
current += note.length
}
return out;
}
export function decodeSongFromHex(hex: string) {
const bytes = pxt.U.fromHex(hex);
return decodeSong(bytes);
}
function decodeSong(buf: Uint8Array) {
const res: Song = {
beatsPerMinute: get16BitNumber(buf, 1),
beatsPerMeasure: buf[3],
ticksPerBeat: buf[4],
measures: buf[5],
1 Like
richard
September 11, 2023, 4:39pm
17
It’s the same as the pxtc.Util namespace. Declared here: https://github.com/microsoft/pxt/blob/master/pxtlib/main.ts#L31
And here is that function:
return res;
}
export function toHex(bytes: ArrayLike<number>) {
let r = ""
for (let i = 0; i < bytes.length; ++i)
r += ("0" + bytes[i].toString(16)).slice(-2)
return r
}
export function fromHex(hex: string) {
let r = new Uint8Array(hex.length >> 1)
for (let i = 0; i < hex.length; i += 2)
r[i >> 1] = parseInt(hex.slice(i, i + 2), 16)
return r
}
export class PromiseQueue {
promises: pxt.Map<(() => Promise<any>)[]> = {};
enqueue<T>(id: string, f: () => Promise<T>): Promise<T> {
2 Likes
Dude. WHAT AM I EVEN LOOKING AT. Seriously, some people are just too smart.
2 Likes