Spaces:
Sleeping
Sleeping
| 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") | |
| } | |