import * as vscode from "vscode" ; import { logger , LogLevel , getConfiguration , Accidentals } from "./util" ; import * as JZZ from "jzz" ; import * as jzzMidiSmf from "jzz-midi-smf" ; import { langId } from "./consts" ; jzzMidiSmf ( JZZ ) ; export namespace MIDIIn { type MIDIInStateType = { midiInPort : any ; active : boolean ; } ; export type MIDIInputConfig = { accidentals : Accidentals ; relativeMode : boolean ; chordMode : boolean ; } ; const initialMidiInState = { midiInPort : undefined , active : false , } ; let MIDIInState = initialMidiInState ; const statusBarItems = { } ; export const activeNotes = new Set ( ) ; export const chordNotes = new Set ( ) ; const midiInMsgProcessor = JZZ . Widget ( ) ; const getMIDIInputConfig = ( ) : MIDIInputConfig => { const config = getConfiguration ( ) ; const { accidentals , relativeMode , chordMode } = config . midiInput ; return { accidentals : accidentals , relativeMode : relativeMode , chordMode : chordMode , } ; } ; export const getAbsoluteOctavePostfix = ( octaveNum ) : string => { if ( ! Number . isInteger ( octaveNum ) || octaveNum < 0 || octaveNum > 9 ) { throw new Error ( `OctaveNumber should be an integer within [0,9]; got ${ octaveNum }` ) ; } if ( octaveNum <= 3 ) { return `,` . repeat ( 3 - octaveNum ) ; } else { return `'` . repeat ( octaveNum - 3 ) ; } } ; export const getNoteChar = ( accidentals , noteNum ) : string => { const isSharp = accidentals === `sharps` ; const map = { 0 : `c` , 1 : isSharp ? `cis` : `des` , 2 : `d` , 3 : isSharp ? `dis` : `ees` , 4 : `e` , 5 : `f` , 6 : isSharp ? `fis` : `ges` , 7 : `g` , 8 : isSharp ? `gis` : `aes` , 9 : `a` , 10 : isSharp ? `ais` : `bes` , 11 : `b` , } ; if ( ! ( noteNum in map ) ) { throw new Error ( `NoteNumber should be a integer within [0,11]; got ${ noteNum }` ) ; } return map [ noteNum ] ; } ; export const midiNumberToNoteName = ( note , accidentals , relativeMode ) : string => { if ( ! Number . isInteger ( note ) || note < 12 || note > 127 ) { throw new Error ( `MIDI Note should be an integer within [12, 127], got ${ note }` ) ; } const octaveNum = Math . trunc ( note / 12 ) - 1 ; const noteNum = note % 12 ; const fullNoteStr = getNoteChar ( accidentals , noteNum ) + ( relativeMode ? `` : getAbsoluteOctavePostfix ( octaveNum ) ) ; return fullNoteStr ; } ; export const notesToString = ( notes , accidentals , relativeMode ) : string => { try { if ( notes . size === 1 ) { return ( ` ` + midiNumberToNoteName ( [ ... notes ] [ 0 ] , accidentals , relativeMode ) ) ; } else if ( notes . size > 1 ) { return ( ` <` + [ ... notes ] . sort ( ) . map ( ( note ) => midiNumberToNoteName ( note , accidentals , relativeMode ) ) . join ( ` ` ) + `>` ) ; } } catch ( err ) { logger ( `Error outputting note: ${ err }` , LogLevel . error , false ) ; } return `` ; } ; export type OutputNotesFnType = ( notes : Set < number > , accidentals : "sharps" | "flats" , relativeMode : boolean ) => void ; const outputNotes = ( notes , accidentals , relativeMode ) => { const outputString = notesToString ( notes , accidentals , relativeMode ) ; if ( outputString . length ) { try { const activeTextEditor = vscode . window . activeTextEditor ; if ( ! activeTextEditor ) { throw new Error ( `No active text editor open` ) ; } activeTextEditor . edit ( ( editBuilder ) => { const position = activeTextEditor . selection . active ; editBuilder . insert ( position , outputString ) ; } ) ; } catch ( err ) { logger ( String ( err ) , LogLevel . error , false ) ; } } } ; export const processNote = ( MIDINoteNumber , keyDown , MIDIInputConfig ) => ( outputNoteFn ) => { const { accidentals , relativeMode , chordMode } = MIDIInputConfig ; if ( keyDown ) { if ( ! chordMode ) { if ( activeNotes . size ) { if ( activeNotes . size > 1 ) { logger ( `Outputting a chord despite not in chord mode!` , LogLevel . warning , false ) ; } outputNoteFn ( activeNotes , accidentals , relativeMode ) ; activeNotes . clear ( ) ; } } activeNotes . add ( MIDINoteNumber ) ; } else { if ( chordMode ) { chordNotes . add ( MIDINoteNumber ) ; activeNotes . delete ( MIDINoteNumber ) ; if ( activeNotes . size === 0 ) { outputNoteFn ( chordNotes , accidentals , relativeMode ) ; chordNotes . clear ( ) ; } } else { if ( activeNotes . size ) { if ( activeNotes . size > 1 ) { logger ( `Outputting a chord despite not in chord mode!` , LogLevel . warning , false ) ; } outputNoteFn ( activeNotes , accidentals , relativeMode ) ; activeNotes . clear ( ) ; } } } } ; midiInMsgProcessor . _receive = ( msg ) => { const statusByte = msg [ 0 ] & 0xf0 ; const MIDINoteNumber = msg [ 1 ] ; const velocity = msg [ 2 ] ; if ( [ 0x80 , 0x90 ] . includes ( statusByte ) && MIDINoteNumber >= 12 && MIDINoteNumber <= 127 ) { const keyDown = statusByte === 0x90 && velocity > 0 ; const MIDIInputConfig = getMIDIInputConfig ( ) ; processNote ( MIDINoteNumber , keyDown , MIDIInputConfig ) ( outputNotes ) ; } else { logger ( `Received other MIDI message: ${ msg }` , LogLevel . warning , true ) ; } } ; export const startMIDIInput = async ( ) => { try { const inputs = await getInputMIDIDevices ( ) ; if ( inputs . length === 0 ) { throw new Error ( `No input MIDI devices are found.` ) ; } const config = getConfiguration ( ) ; MIDIInState . midiInPort = config . midiInput . input . length ? JZZ ( ) . openMidiIn ( config . midiInput . input ) : JZZ ( ) . openMidiIn ( ) ; MIDIInState . midiInPort . connect ( midiInMsgProcessor ) ; MIDIInState . active = true ; } catch ( err ) { logger ( String ( err ) , LogLevel . error , false ) ; } updateMIDIStatusBarItem ( ) ; } ; export const stopMIDIInput = async ( ) => { if ( MIDIInState . midiInPort ) { MIDIInState . midiInPort . disconnect ( ) ; MIDIInState . midiInPort . close ( ) ; } MIDIInState = initialMidiInState ; MIDIInState . active = false ; updateMIDIStatusBarItem ( ) ; } ; export const restartMIDIInput = async ( ) => { await stopMIDIInput ( ) ; await startMIDIInput ( ) ; } ; const getInputMIDIDevices = async ( ) => { const inputs = JZZ ( ) . info ( ) . inputs . map ( ( x ) => x . name ) ; return inputs ; } ; export const setInputMIDIDevice = async ( ) => { const inputs = await getInputMIDIDevices ( ) ; vscode . window . showQuickPick ( inputs ) . then ( ( val ) => { if ( val ) { const config = getConfiguration ( ) ; config . update ( `midiInput.input` , val ) ; if ( MIDIInState . active ) { restartMIDIInput ( ) ; } } } ) ; } ; export const initMIDIStatusBarItems = ( ) => { { const startBtn = vscode . window . createStatusBarItem ( vscode . StatusBarAlignment . Right , 2 ) ; startBtn . command = `vslilypond.startMIDIInput` ; startBtn . text = `$(circle-filled) Start MIDI Input` ; startBtn . tooltip = `Start MIDI Input` ; statusBarItems . start = startBtn ; } { const stopBtn = vscode . window . createStatusBarItem ( vscode . StatusBarAlignment . Right , 1 ) ; stopBtn . command = `vslilypond.stopMIDIInput` ; stopBtn . text = `$(debug-stop) Stop MIDI Input` ; stopBtn . tooltip = `Stop MIDI Input` ; statusBarItems . stop = stopBtn ; } updateMIDIStatusBarItem ( ) ; } ; const shouldShowStatusBarItems = ( ) : boolean => { const activeTextEditor = vscode . window . activeTextEditor ; if ( activeTextEditor && activeTextEditor . document . languageId === langId ) { return true ; } return false ; } ; export const updateMIDIStatusBarItem = async ( ) => { if ( shouldShowStatusBarItems ( ) ) { if ( MIDIInState . active ) { statusBarItems . start . hide ( ) ; statusBarItems . stop . show ( ) ; } else { statusBarItems . start . show ( ) ; statusBarItems . stop . hide ( ) ; } } else { Object . values ( statusBarItems ) . forEach ( ( x ) => x . hide ( ) ) ; } } ; }