import * as vscode from "vscode" ; import * as fs from "fs" ; import * as filetype from "file-type" ; import * as path from "path" ; import { logger , LogLevel , stripFileExtension , getConfiguration } from "./util" ; import * as JZZ from "jzz" ; import * as jzzMidiSmf from "jzz-midi-smf" ; import { langId } from "./consts" ; jzzMidiSmf ( JZZ ) ; export namespace MIDIOut { let timeout = undefined ; const statusBarItems = { } ; type MIDIOutStateType = { player : any | undefined ; playing : boolean ; paused : boolean ; currMidiFilePath : string | undefined ; } ; const initialMIDIOutState = { player : undefined , playing : false , paused : false , currMidiFilePath : undefined , } ; let MIDIOutState = initialMIDIOutState ; export const msToMMSS = ( ms ) => { if ( ms < 0 ) { throw new Error ( `Time cannot be negative` ) ; } const seconds = Math . round ( ms / 1000 ) ; const mm = Math . round ( seconds / 60 ) . toString ( ) ; const ss = Math . round ( seconds % 60 ) . toString ( ) . padStart ( 2 , `0` ) ; return `${ mm }:${ ss }` ; } ; export const MMSSToms = ( mmss ) : number => { const matches = mmss . match ( /^[0-9]+:[0-5][0-9]$/ ) ; if ( matches && matches . length && matches [ 0 ] . length ) { const [ mm , ss ] = mmss . split ( `:` ) . map ( ( s ) => parseInt ( s ) ) ; const res = ( mm * 60 + ss ) * 1000 ; return res ; } throw new Error ( `${ mmss } does not match the required syntax: /^[0-9]+:[0-5][0-9]$/. Valid examples: 1:23, 10:59, 0:12` ) ; } ; export const validateMIDIStartTimeInput = ( durationMS , durationMMSS , mmss ) : string | undefined => { try { const ms = MMSSToms ( mmss ) ; if ( ms > durationMS ) { throw new Error ( `Duration ${ mmss } is longer than duration of actual MIDI file ${ durationMMSS }` ) ; } } catch ( err ) { return String ( err ) ; } return undefined ; } ; const askAndSetMIDIStartTime = async ( durationMS ) : Promise < number | undefined > => { const durationMMSS = msToMMSS ( durationMS ) ; const data = vscode . window . showInputBox ( { ignoreFocusOut : true , placeHolder : `0:00` , prompt : `Duration of MIDI is ${ durationMMSS }. Please enter the start timestamp required in the format m:ss.` , validateInput : ( mmss ) => validateMIDIStartTimeInput ( durationMS , durationMMSS , mmss ) , } ) ; const value = await data ; if ( value !== undefined ) { const res = MMSSToms ( value ) ; return res ; } return undefined ; } ; const getMidiFilePathFromWindow = async ( ) => { const activeTextEditor = vscode . window . activeTextEditor ; if ( ! activeTextEditor ) { throw new Error ( `No active \`lilypond\`text editor open (Please click inside a \`lilypond\` text document to make it active)` ) ; } const existsCheck = ( filePath ) : string | undefined => { if ( fs . existsSync ( filePath ) ) { return filePath ; } return undefined ; } ; const getFirstMidiTypeFilePath = async ( baseFilePath ) => { const dirname = path . dirname ( baseFilePath ) ; const filePaths = fs . readdirSync ( dirname ) . map ( ( p ) => `${ dirname }${ path . sep }${ p }` ) . filter ( ( p ) => p . substr ( 0 , baseFilePath . length ) === baseFilePath ) . sort ( ( a , b ) => a . length - b . length ) ; for ( const p of filePaths ) { const mimeType = ( await filetype . fromFile ( p ) ) ?. mime ; if ( mimeType === `audio/midi` ) { return p ; } } return undefined ; } ; const baseFilePath = stripFileExtension ( activeTextEditor . document . uri . fsPath ) ; const midiFileName = existsCheck ( `${ baseFilePath }.midi` ) ?? existsCheck ( `${ baseFilePath }.mid` ) ?? ( await getFirstMidiTypeFilePath ( baseFilePath ) ) ; if ( ! midiFileName ) { throw new Error ( `Cannot find MIDI file to play - make sure you are outputting a MIDI file, and the editor is currently active on the file that produces the MIDI.` ) ; } return midiFileName ; } ; const loadMIDI = async ( ) => { const midiFileName = await getMidiFilePathFromWindow ( ) ; const data = fs . readFileSync ( midiFileName , `binary` ) ; const smf = JZZ . MIDI . SMF ( data ) ; MIDIOutState . currMidiFilePath = midiFileName ; MIDIOutState . player = smf . player ( ) ; const outputs = await getOutputMIDIDevices ( ) ; if ( outputs . length === 0 ) { throw new Error ( `No output MIDI devices are found.` ) ; } const config = getConfiguration ( ) ; const midiout = config . midiPlayback . output . length ? JZZ ( ) . openMidiOut ( config . midiPlayback . output ) : JZZ ( ) . openMidiOut ( ) ; MIDIOutState . player . connect ( midiout ) ; } ; const getPlayTimeStamp = ( durationMS , positionMS ) : string => { const durationMMSS = msToMMSS ( durationMS ) ; const positionMMSS = msToMMSS ( positionMS ) ; return `${ positionMMSS }/${ durationMMSS }` ; } ; const pollMIDIStatus = async ( ) => { if ( ! MIDIOutState . player ) { throw new Error ( `MIDI Player not initialised!` ) ; } const durationMS = MIDIOutState . player . durationMS ( ) ; const positionMS = MIDIOutState . player . positionMS ( ) ; if ( positionMS === 0 && ( ( MIDIOutState . player && MIDIOutState . playing ) || MIDIOutState . paused ) ) { stopMIDI ( ) ; } else { vscode . window . setStatusBarMessage ( `Playing \`${ MIDIOutState . currMidiFilePath }\`: ${ getPlayTimeStamp ( durationMS , positionMS ) }` ) ; timeout = setTimeout ( pollMIDIStatus , 100 ) ; } } ; export const playMIDI = async ( ) => { try { await loadMIDI ( ) ; if ( MIDIOutState . player ) { MIDIOutState . player . play ( ) ; MIDIOutState . playing = true ; timeout = setTimeout ( pollMIDIStatus , 100 ) ; } else { throw new Error ( `Unable to load MIDI player` ) ; } } catch ( err ) { logger ( String ( err ) , LogLevel . error , false ) ; } updateMIDIStatusBarItem ( ) ; } ; export const playMIDIFrom = async ( ) => { try { await loadMIDI ( ) ; if ( MIDIOutState . player ) { const durationMS = MIDIOutState . player . durationMS ( ) ; const startMS = await askAndSetMIDIStartTime ( durationMS ) ; if ( startMS !== undefined ) { MIDIOutState . player . play ( ) ; MIDIOutState . player . jumpMS ( startMS ) ; MIDIOutState . playing = true ; timeout = setTimeout ( pollMIDIStatus , 100 ) ; } } else { throw new Error ( `Unable to load MIDI player` ) ; } } catch ( err ) { logger ( String ( err ) , LogLevel . error , false ) ; } updateMIDIStatusBarItem ( ) ; } ; export const stopMIDI = async ( ) => { try { if ( ( MIDIOutState . player && MIDIOutState . playing ) || MIDIOutState . paused ) { MIDIOutState . player . stop ( ) ; MIDIOutState . playing = false ; MIDIOutState . paused = false ; vscode . window . setStatusBarMessage ( `` ) ; if ( timeout ) { clearTimeout ( timeout ) ; } } else { throw new Error ( `No active MIDI file to stop` ) ; } } catch ( err ) { logger ( String ( err ) , LogLevel . error , false ) ; } updateMIDIStatusBarItem ( ) ; } ; export const pauseMIDI = async ( ) => { try { if ( MIDIOutState . player && MIDIOutState . playing && ! MIDIOutState . paused ) { MIDIOutState . player . pause ( ) ; MIDIOutState . paused = true ; MIDIOutState . playing = false ; const durationMS = MIDIOutState . player . durationMS ( ) ; const positionMS = MIDIOutState . player . positionMS ( ) ; vscode . window . setStatusBarMessage ( `Paused MIDI: \`${ MIDIOutState . currMidiFilePath }\`: ${ getPlayTimeStamp ( durationMS , positionMS ) }` ) ; if ( timeout ) { clearTimeout ( timeout ) ; } } else { throw new Error ( `No active MIDI file to pause` ) ; } } catch ( err ) { logger ( String ( err ) , LogLevel . error , false ) ; } updateMIDIStatusBarItem ( ) ; } ; export const resumeMIDI = async ( ) => { try { if ( MIDIOutState . player && MIDIOutState . paused && ! MIDIOutState . playing ) { MIDIOutState . player . resume ( ) ; MIDIOutState . paused = false ; MIDIOutState . playing = true ; timeout = setTimeout ( pollMIDIStatus , 100 ) ; } else { playMIDI ( ) ; } } catch ( err ) { logger ( String ( err ) , LogLevel . error , false ) ; } updateMIDIStatusBarItem ( ) ; } ; export const resetMIDI = async ( mute = false ) => { if ( ( MIDIOutState . player && MIDIOutState . playing ) || MIDIOutState . paused ) { stopMIDI ( ) ; } if ( timeout ) { clearTimeout ( timeout ) ; } MIDIOutState = initialMIDIOutState ; logger ( `MIDI Playback reset` , LogLevel . info , mute ) ; updateMIDIStatusBarItem ( ) ; } ; export const initMIDIStatusBarItems = ( ) => { { const playBtn = vscode . window . createStatusBarItem ( vscode . StatusBarAlignment . Left , 100 ) ; playBtn . command = `vslilypond.resumeMIDI` ; playBtn . text = `$(debug-start) Play MIDI` ; playBtn . tooltip = `Play MIDI output file (Resumes if paused)` ; statusBarItems . play = playBtn ; } { const playFromBtn = vscode . window . createStatusBarItem ( vscode . StatusBarAlignment . Left , 99 ) ; playFromBtn . command = `vslilypond.playMIDIFrom` ; playFromBtn . text = `$(debug-continue) Play MIDI From...` ; playFromBtn . tooltip = `Play MIDI output file from a certain timesatmp` ; statusBarItems . playFrom = playFromBtn ; } { const pauseBtn = vscode . window . createStatusBarItem ( vscode . StatusBarAlignment . Left , 98 ) ; pauseBtn . command = `vslilypond.pauseMIDI` ; pauseBtn . text = `$(debug-pause) Pause MIDI` ; pauseBtn . tooltip = `Pause MIDI playback` ; statusBarItems . pause = pauseBtn ; } { const stopBtn = vscode . window . createStatusBarItem ( vscode . StatusBarAlignment . Left , 97 ) ; stopBtn . command = `vslilypond.stopMIDI` ; stopBtn . text = `$(debug-stop) Stop MIDI` ; stopBtn . tooltip = `Stop MIDI playback` ; statusBarItems . stop = stopBtn ; } updateMIDIStatusBarItem ( ) ; } ; export const updateMIDIStatusBarItem = async ( ) => { if ( await shouldShowStatusBarItems ( ) ) { if ( MIDIOutState . playing ) { statusBarItems . play . hide ( ) ; statusBarItems . playFrom . hide ( ) ; statusBarItems . pause . show ( ) ; statusBarItems . stop . show ( ) ; } else { statusBarItems . play . show ( ) ; statusBarItems . playFrom . show ( ) ; statusBarItems . pause . hide ( ) ; statusBarItems . stop . hide ( ) ; } } else { Object . values ( statusBarItems ) . forEach ( ( x ) => x . hide ( ) ) ; } } ; const getOutputMIDIDevices = async ( ) => { const outputs = JZZ ( ) . info ( ) . outputs . map ( ( x ) => x . name ) ; return outputs ; } ; export const setOutputMIDIDevice = async ( ) => { const outputs = await getOutputMIDIDevices ( ) ; vscode . window . showQuickPick ( outputs ) . then ( ( val ) => { if ( val ) { const config = getConfiguration ( ) ; config . update ( `midiPlayback.output` , val ) ; } } ) ; } ; const shouldShowStatusBarItems = async ( ) : Promise < boolean > => { const activeTextEditor = vscode . window . activeTextEditor ; if ( activeTextEditor && activeTextEditor . document . languageId === langId ) { return true ; } return false ; } ; }