midi-player-demo / src /common /midi /midiConversion.ts
Yann
test
f23825d
raw
history blame
4.46 kB
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")
}