import { partition } from "lodash" import groupBy from "lodash/groupBy" import { AnyEvent, EndOfTrackEvent, MidiFile, read, StreamSource, write as writeMidiFile, } from "midifile-ts" import { toJS } from "mobx" import { isNotNull } from "../helpers/array" import { downloadBlob } from "../helpers/Downloader" import { addDeltaTime, toRawEvents } from "../helpers/toRawEvents" import { addTick, tickedEventsToTrackEvents, toTrackEvents, } from "../helpers/toTrackEvents" import Song from "../song" import Track, { AnyEventFeature } from "../track" const trackFromMidiEvents = (events: AnyEvent[]): Track => { const track = new Track() const channel = findChannel(events) if (channel !== undefined) { track.channel = channel } track.addEvents(toTrackEvents(events)) return track } const tracksFromFormat0Events = (events: AnyEvent[]): Track[] => { const tickedEvents = addTick(events) const eventsPerChannel = groupBy(tickedEvents, (e) => { if ("channel" in e) { return e.channel + 1 } return 0 // conductor track }) const tracks: Track[] = [] for (const channel of Object.keys(eventsPerChannel)) { const events = eventsPerChannel[channel] const ch = parseInt(channel) while (tracks.length <= ch) { const track = new Track() track.channel = ch > 0 ? ch - 1 : undefined tracks.push(track) } const track = tracks[ch] const trackEvents = tickedEventsToTrackEvents(events) track.addEvents(trackEvents) } return tracks } const findChannel = (events: AnyEvent[]) => { const chEvent = events.find((e) => { return e.type === "channel" }) if (chEvent !== undefined && "channel" in chEvent) { return chEvent.channel } return undefined } const isConductorTrack = (track: AnyEvent[]) => findChannel(track) === undefined const isConductorEvent = (e: AnyEventFeature) => "subtype" in e && (e.subtype === "timeSignature" || e.subtype === "setTempo") export const createConductorTrackIfNeeded = ( tracks: AnyEvent[][], ): AnyEvent[][] => { // Find conductor track let [conductorTracks, normalTracks] = partition(tracks, isConductorTrack) // Create a conductor track if there is no conductor track if (conductorTracks.length === 0) { conductorTracks.push([]) } const [conductorTrack, ...restTracks] = [ ...conductorTracks, ...normalTracks, ].map(addTick) const newTracks = restTracks.map((track) => track .map((e) => { // Collect all conductor events if (isConductorEvent(e)) { conductorTrack.push(e) return null } return e }) .filter(isNotNull), ) return [conductorTrack, ...newTracks].map(addDeltaTime) } const getTracks = (midi: MidiFile): Track[] => { switch (midi.header.formatType) { case 0: return tracksFromFormat0Events(midi.tracks[0]) case 1: return createConductorTrackIfNeeded(midi.tracks).map(trackFromMidiEvents) default: throw new Error(`Unsupported midi format ${midi.header.formatType}`) } } export function songFromMidi(data: StreamSource) { const song = new Song() const midi = read(data) getTracks(midi).forEach((t) => song.addTrack(t)) if (midi.header.formatType === 1 && song.tracks.length > 0) { // Use the first track name as the song title const name = song.tracks[0].name if (name !== undefined) { song.name = name } } song.timebase = midi.header.ticksPerBeat return song } const setChannel = (channel: number) => (e: AnyEvent): AnyEvent => { if (e.type === "channel") { return { ...e, channel } } return e } export function songToMidiEvents(song: Song): AnyEvent[][] { const tracks = toJS(song.tracks) return tracks.map((t) => { const endOfTrack: EndOfTrackEvent = { deltaTime: 0, type: "meta", subtype: "endOfTrack", } const rawEvents = [...toRawEvents(t.events), endOfTrack] if (t.channel !== undefined) { return rawEvents.map(setChannel(t.channel)) } return rawEvents }) } export function songToMidi(song: Song) { const rawTracks = songToMidiEvents(song) return writeMidiFile(rawTracks, song.timebase) } export function downloadSongAsMidi(song: Song) { const bytes = songToMidi(song) const blob = new Blob([bytes], { type: "application/octet-stream" }) downloadBlob(blob, song.filepath.length > 0 ? song.filepath : "no name.mid") }