Created
June 9, 2026 19:56
-
-
Save Saren-Arterius/7b7c7b7bbc7f1cface5710eb3bd75017 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| syntax = "proto3"; | |
| message Note { | |
| int32 type = 1; | |
| float time = 2; | |
| float length = 3; | |
| int32 instrument = 4; | |
| float volume = 5; | |
| } | |
| message Sequence { | |
| message Settings { | |
| int32 bpm = 1; | |
| int32 time_signature = 2; | |
| map<int32, InstrumentSettings> instruments_map = 3; | |
| float one_minus_volume = 4; | |
| } | |
| message Marker { | |
| float time = 1; | |
| int32 setting = 2; | |
| int32 instrument = 3; | |
| float value = 4; | |
| bool blend = 5; | |
| } | |
| message InstrumentSettings { | |
| string name = 1; | |
| repeated int32 color = 2; | |
| float volume = 3; | |
| bool delay = 4; | |
| bool reverb = 5; | |
| int32 reverb_type = 6; | |
| float one_minus_reverb_volume = 7; | |
| bool distort = 8; | |
| int32 distort_type = 9; | |
| float distort_volume = 10; | |
| float pan = 11; | |
| float detune = 12; | |
| // ... more synth/filter settings | |
| } | |
| Settings settings = 1; | |
| repeated Note notes = 2; | |
| repeated Marker markers = 3; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * dat_to_midi.ts - Convert dat2.dat (jspb binary format from onlinesequencer.net) to MIDI | |
| * Built for Bun runtime using native protobuf support | |
| */ | |
| import { readFileSync, writeFileSync } from 'fs'; | |
| import * as protobuf from 'protobufjs'; | |
| // ============================================================================ | |
| // Data Models | |
| // ============================================================================ | |
| interface Note { | |
| type: number; | |
| time: number; | |
| length: number; | |
| instrument: number; | |
| volume: number; | |
| } | |
| interface InstrumentSettings { | |
| name: string; | |
| color: number[]; | |
| volume: number; | |
| delay: boolean; | |
| reverb: boolean; | |
| reverbType: number; | |
| oneMinusReverbVolume: number; | |
| distort: boolean; | |
| distortType: number; | |
| distortVolume: number; | |
| pan: number; | |
| detune: number; | |
| } | |
| interface SequenceSettings { | |
| bpm: number; | |
| timeSignature: number; | |
| instrumentsMap: Map<number, InstrumentSettings>; | |
| oneMinusVolume: number; | |
| } | |
| interface Sequence { | |
| settings: SequenceSettings | null; | |
| notes: Note[]; | |
| markers: unknown[]; | |
| } | |
| // ============================================================================ | |
| // MIDI Writer Class | |
| // ============================================================================ | |
| interface MidiEvent { | |
| deltaTime: number; | |
| type: number; | |
| param1?: number; | |
| param2?: number; | |
| subtype?: number; | |
| data?: Uint8Array; | |
| } | |
| class MidiTrack { | |
| private events: MidiEvent[] = []; | |
| addEvent(event: Omit<MidiEvent, 'deltaTime'>): void { | |
| this.events.push({ deltaTime: 0, ...event }); | |
| } | |
| setDeltaTime(delta: number): void { | |
| if (this.events.length > 0) { | |
| this.events[this.events.length - 1].deltaTime = delta; | |
| } | |
| } | |
| toRawEvents(): MidiEvent[] { | |
| return this.events; | |
| } | |
| } | |
| class MidiWriter { | |
| private static readonly TICKS_PER_BEAT = 384; | |
| private static readonly TICKS_PER_TIME_UNIT = 96; | |
| private tracks: MidiTrack[] = []; | |
| static writeVarInt(value: number): number[] { | |
| value = value >>> 0; | |
| if (value === 0) return [0]; | |
| const temp: number[] = []; | |
| let v = value; | |
| while (v > 0) { | |
| temp.push(v & 0x7f); | |
| v >>>= 7; | |
| } | |
| const bytes = temp.reverse(); | |
| for (let i = 0; i < bytes.length - 1; i++) { | |
| bytes[i] |= 0x80; | |
| } | |
| return bytes; | |
| } | |
| static getMidiProgram(instId: number): number { | |
| const instrumentMap = [ | |
| 5, 26, 1, 91, 28, 34, 85, 104, 1, 61, 58, 41, 43, 81, 81, 82, 81, 7, 47, 14, | |
| 46, 115, 105, 74, 67, 4, 11, 39, 20, 37, 89, 119, 27, 108, 12, 29, 1, 39, | |
| 31, 1, 1, 1, 1, 6, 31, 43, 41, 49, 33, 28, 61, 58, 91, 1, 34, 91, 40, 91, | |
| 25, 53, 1, 62, 5 | |
| ]; | |
| const program = instrumentMap[instId] !== undefined ? instrumentMap[instId] - 1 : 0; | |
| return Math.max(0, program); | |
| } | |
| static getInstrumentName(instId: number): string { | |
| const instrumentNames = [ | |
| "Elec. Piano (Classic)", "Acoustic Gtr (Classic)", "Drum Kit", | |
| "Smooth Synth (Classic)", "Electric Guitar", "Bass Guitar (Classic)", | |
| "Synth Pluck", "Scifi", "Grand Piano (Classic)", "French Horn (Classic)", | |
| "Trombone (Classic)", "Violin (Classic)", "Cello (Classic)", | |
| "8-Bit Sine", "8-Bit Square", "8-Bit Sawtooth", "8-Bit Triangle", | |
| "Harpsichord", "Concert Harp", "Xylophone", "Pizzicato", "Steel Drums", | |
| "Sitar", "Flute", "Saxophone", "Ragtime Piano", "Music Box", | |
| "Synth Bass (Classic)", "Church Organ", "Slap Bass", "Pop Synth (Classic)", | |
| "Electric Drum Kit", "Jazz Guitar", "Koto", "Vibraphone", "Muted E-Guitar", | |
| "808 Drum Kit", "808 Bass", "Dist. Guitar (Classic)", "8-Bit Drum Kit", | |
| "2013 Drum Kit", "Grand Piano", "909 Drum Kit", "Electric Piano", | |
| "Distortion Guitar", "Cello", "Violin", "Strings", "Bass", "Clean Guitar", | |
| "French Horn", "Trombone", "Smooth Synth", "2023 Drum Kit", "Bass Guitar", | |
| "Synthesizer", "Synth Bass", "Pop Synth", "Acoustic Guitar", "Lucent Choir", | |
| "EDM Kit (E)", "Brass", "Rhodes" | |
| ]; | |
| return instrumentNames[instId] || `Instrument ${instId}`; | |
| } | |
| private groupNotesByInstrument(notes: Note[]): Map<number, Note[]> { | |
| const notesByInst = new Map<number, Note[]>(); | |
| for (const note of notes) { | |
| const instId = note.instrument ?? 0; | |
| if (!notesByInst.has(instId)) { | |
| notesByInst.set(instId, []); | |
| } | |
| notesByInst.get(instId)!.push(note); | |
| } | |
| return notesByInst; | |
| } | |
| createMidiFile(sequence: Sequence): Uint8Array { | |
| const notesByInst = this.groupNotesByInstrument(sequence.notes); | |
| const sortedInstIds = [...notesByInst.keys()]; | |
| const settings = sequence.settings; | |
| const timeSig = settings?.timeSignature ?? 4; | |
| const bpm = settings?.bpm ?? 110; | |
| const mpqn = Math.round((60 / bpm) * 1000000); | |
| const tempoBytes = new Uint8Array([ | |
| (mpqn >> 16) & 0xff, | |
| (mpqn >> 8) & 0xff, | |
| mpqn & 0xff | |
| ]); | |
| for (let trackIdx = 0; trackIdx < sortedInstIds.length; trackIdx++) { | |
| const instId = sortedInstIds[trackIdx]; | |
| const notes = notesByInst.get(instId)!; | |
| const track = new MidiTrack(); | |
| if (trackIdx === 0) { | |
| track.addEvent({ type: 0xff, subtype: 0x58, data: new Uint8Array([4, 2, 24, 8]) }); | |
| track.addEvent({ type: 0xff, subtype: 0x51, data: tempoBytes }); | |
| } | |
| const trackName = MidiWriter.getInstrumentName(instId); | |
| track.addEvent({ type: 0xff, subtype: 0x03, data: Buffer.from(trackName, 'utf-8') }); | |
| const program = MidiWriter.getMidiProgram(instId); | |
| const channel = trackIdx; | |
| track.addEvent({ type: 0xc0 | channel, param1: program }); | |
| const events: Array<{ time: number; type: 'on' | 'off'; note: number; velocity?: number }> = []; | |
| for (const note of notes) { | |
| const midiNote = note.type ?? 0; | |
| const velocity = Math.min(127, Math.floor((64 * (note.volume ?? 1)))); | |
| const onTime = (note.time ?? 0) * MidiWriter.TICKS_PER_TIME_UNIT; | |
| const offTime = onTime + (note.length ?? 1) * MidiWriter.TICKS_PER_TIME_UNIT; | |
| events.push({ time: onTime, type: 'on', note: midiNote, velocity }); | |
| events.push({ time: offTime, type: 'off', note: midiNote }); | |
| } | |
| events.sort((a, b) => { | |
| if (a.time !== b.time) return a.time - b.time; | |
| return a.type === 'on' ? 1 : -1; | |
| }); | |
| let currentTime = 0; | |
| for (const ev of events) { | |
| const deltaTime = Math.max(0, Math.round(ev.time - currentTime)); | |
| currentTime = ev.time; | |
| if (ev.type === 'on') { | |
| track.addEvent({ | |
| type: 0x90 | channel, | |
| param1: ev.note, | |
| param2: ev.velocity ?? 64 | |
| }); | |
| track.setDeltaTime(deltaTime); | |
| } else { | |
| track.addEvent({ | |
| type: 0x80 | channel, | |
| param1: ev.note, | |
| param2: 0 | |
| }); | |
| track.setDeltaTime(deltaTime); | |
| } | |
| } | |
| track.addEvent({ type: 0xff, subtype: 0x2f, data: new Uint8Array() }); | |
| track.setDeltaTime(0); | |
| this.tracks.push(track); | |
| } | |
| return this.buildMidiFile(); | |
| } | |
| private buildMidiFile(): Uint8Array { | |
| const sections: Uint8Array[] = []; | |
| const header = [ | |
| 0x4d, 0x54, 0x68, 0x64, | |
| 0x00, 0x00, 0x00, 0x06, | |
| 0x00, 0x01, | |
| 0x00, this.tracks.length, | |
| (MidiWriter.TICKS_PER_BEAT >> 8) & 0xff, | |
| MidiWriter.TICKS_PER_BEAT & 0xff | |
| ]; | |
| sections.push(new Uint8Array(header)); | |
| for (const track of this.tracks) { | |
| const trackData: number[] = []; | |
| const rawEvents = track.toRawEvents(); | |
| for (const ev of rawEvents) { | |
| const deltaBytes = MidiWriter.writeVarInt(Math.max(0, ev.deltaTime)); | |
| trackData.push(...deltaBytes); | |
| if (ev.type === 0xff) { | |
| trackData.push(ev.type, ev.subtype!); | |
| trackData.push(ev.data!.length); | |
| trackData.push(...ev.data!); | |
| } else { | |
| trackData.push(ev.type); | |
| if (ev.param1 !== undefined) trackData.push(ev.param1); | |
| if (ev.param2 !== undefined) trackData.push(ev.param2); | |
| } | |
| } | |
| const trackSize = trackData.length; | |
| const trackHeader = [ | |
| 0x4d, 0x54, 0x72, 0x6b, | |
| (trackSize >> 24) & 0xff, | |
| (trackSize >> 16) & 0xff, | |
| (trackSize >> 8) & 0xff, | |
| trackSize & 0xff | |
| ]; | |
| sections.push(new Uint8Array(trackHeader)); | |
| sections.push(new Uint8Array(trackData)); | |
| } | |
| const totalSize = sections.reduce((sum, s) => sum + s.length, 0); | |
| const midiBuffer = new Uint8Array(totalSize); | |
| let offset = 0; | |
| for (const section of sections) { | |
| midiBuffer.set(section, offset); | |
| offset += section.length; | |
| } | |
| return midiBuffer; | |
| } | |
| } | |
| // ============================================================================ | |
| // Protobuf Decoder | |
| // ============================================================================ | |
| class ProtobufDecoder { | |
| async decodeSequence(data: Uint8Array, protoPath: string): Promise<Sequence> { | |
| const root = await protobuf.load(protoPath); | |
| const SequenceType = root.lookupType('Sequence'); | |
| const message = SequenceType.decode(data); | |
| return { | |
| settings: message.settings ?? null, | |
| notes: (message.notes as any[]) || [], | |
| markers: (message.markers as any[]) || [] | |
| }; | |
| } | |
| } | |
| // ============================================================================ | |
| // Main Entry Point | |
| // ============================================================================ | |
| async function main(): Promise<void> { | |
| const inputFile = process.argv[2] ?? 'dat2.dat'; | |
| const outputFile = process.argv[3] ?? 'output.mid'; | |
| console.log(`Reading ${inputFile}...`); | |
| const data = readFileSync(inputFile); | |
| console.log(`Read ${data.length} bytes`); | |
| console.log('Parsing protobuf...'); | |
| const decoder = new ProtobufDecoder(); | |
| const sequence = await decoder.decodeSequence(data, 'dat.proto'); | |
| console.log(`Found ${sequence.notes.length} notes`); | |
| const instIds = [...new Set(sequence.notes.map(n => n.instrument ?? 0))]; | |
| console.log(`Instruments: ${instIds.join(', ')}`); | |
| for (const instId of instIds) { | |
| console.log(` ID ${instId}: ${MidiWriter.getInstrumentName(instId)}`); | |
| } | |
| console.log(`BPM: ${sequence.settings?.bpm ?? 'N/A'}`); | |
| console.log('Generating MIDI...'); | |
| const writer = new MidiWriter(); | |
| const midiData = writer.createMidiFile(sequence); | |
| console.log(`MIDI file: ${midiData.length} bytes`); | |
| writeFileSync(outputFile, midiData); | |
| console.log(`Written to ${outputFile}`); | |
| } | |
| main(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment