import 'cross-fetch/polyfill' ; import * as Debug from 'debug' ; import * as path from 'path' ; import { SmartPlayer } from './SmartPlayer' ; import { MemoryRenderer } from './renderers/MemoryRenderer' ; import { RendererInfo } from './renderers/RendererInfo' ; import { writeFile as FSWriteFile , readFile as FSReadFile } from 'fs' ; import { exec as CPExec } from 'child_process' ; import { promisify } from 'util' ; import * as tempy from 'tempy' ; import * as WavDecoder from 'wav-decoder' ; import * as WavEncoder from 'wav-encoder' ; import { XAudioBuffer } from './XAudioBuffer' ; import { TimeInstant } from './time' ; import { Score } from 'nf-grapher' ; const exec = promisify ( CPExec ) ; const writeFile = promisify ( FSWriteFile ) ; const readFile = promisify ( FSReadFile ) ; const dbg = Debug ( 'nf:cli' ) ; const dbgdecoder = Debug ( 'nf:cli-decoder' ) ; const pkg = require ( '../package.json' ) ; const showHelpAndExit = ( ) => { console . log ( `
Usage
  $ nf-player <command> [options]

Commands
  save

Global Options
  --input-file, -i                    The file path to the input Score JSON. (Required)
  --output-file, -o [nf-player.wav]   The file path to the output WAVE file.
  --duration, -d [60.0]               Scores can be infinite! Duration is in seconds.
  --seek, -s [0.0]                    Where to start rendering from.
  --quantum, -q [256]                 Size of the render quantum. A smaller value will
                                      produce more accurate Time Stretching and Pitch Shifting,
                                      but at the cost of CPU time.
  --version, -v                       The version of this library. ${ pkg . version }
  --help, -h                          Display this help.

Examples
  # Render 30 seconds of a score to a wav file:
  $ nf-player-cli save --duration 30 -i ./score.json -o ./output.wav
` ) ; process . exit ( 1 ) ; } ; type ParsedCLIFlags = { command : 'save' ; quantum : number ; inputFile : string ; outputFile : string ; duration : number ; seek : number ; } ; function parseRawFlags ( ) { const parsed = { command : parseBareArg ( { name : [ 'save' ] } ) , quantum : parseFlagArg ( { long : '--quantum' , kind : 'number' , alias : '-q' , default : 256 } ) , inputFile : parseFlagArg ( { long : '--input-file' , kind : 'string' , alias : '-i' , required : true } ) , outputFile : parseFlagArg ( { long : '--output-file' , kind : 'string' , alias : '-o' , default : path . join ( process . cwd ( ) , 'nf-player.wav' ) } ) , duration : parseFlagArg ( { long : '--duration' , kind : 'number' , alias : '-d' , default : 60 } ) , seek : parseFlagArg ( { long : '--seek' , kind : 'number' , alias : '-s' , default : 0 } ) } ; return parsed ; } function go ( ) { if ( parseFlagArg ( { long : '--help' , kind : 'boolean' , alias : '-h' } ) ) return showHelpAndExit ( ) ; if ( parseFlagArg ( { long : '--version' , kind : 'boolean' , alias : '-v' , default : false } ) ) { console . log ( pkg . version ) ; return process . exit ( 1 ) ; } let flags ; try { flags = parseRawFlags ( ) ; } catch ( err ) { console . error ( err ) ; return process . exit ( 1 ) ; } if ( flags . command === 'save' ) { saveToFile ( flags ) ; } else { return showHelpAndExit ( ) ; } } go ( ) ; async function saveToFile ( flags ) { const { quantum , inputFile , duration , outputFile } = flags ; const info = { sampleRate : 44100 , channelCount : 2 , quantumSize : quantum , decode } ; const renderer = new MemoryRenderer ( info ) ; const player = new SmartPlayer ( renderer ) ; if ( flags . seek > 0 ) { await player . seek ( TimeInstant . fromSeconds ( flags . seek ) ) ; } const inputData = await readFile ( inputFile , { encoding : 'utf8' } ) ; const rawScore = JSON . parse ( inputData ) ; const s = Score . from ( rawScore ) ; await player . enqueueScore ( s ) ; player . playing = true ; dbg ( 'Rendering %f seconds of score...' , duration ) ; const output = renderer . renderDuration ( TimeInstant . fromSeconds ( duration ) ) ; dbg ( 'Encoding wav file' ) ; let channelData = [ ] ; for ( let i = 0 ; i < output . numberOfChannels ; i ++ ) { channelData . push ( output . getChannelData ( i ) ) ; } const encoded = await WavEncoder . encode ( { sampleRate : info . sampleRate , channelData } ) ; dbg ( 'Writing as %s (%d bytes)' , outputFile , encoded . byteLength ) ; await writeFile ( outputFile , new DataView ( encoded ) ) ; } function decode ( uri , buffer ) { return new Promise ( async ( resolve , reject ) => { const inputName = tempy . file ( ) ; const outputName = tempy . file ( ) ; dbgdecoder ( '%s decode requested' , uri ) ; await writeFile ( inputName , new DataView ( buffer ) ) ; dbgdecoder ( '%s wrote %s (%d bytes?)' , uri , inputName , buffer . byteLength ) ; dbgdecoder ( '%s transcoding wav %s -> %s' , uri , inputName , outputName ) ; await exec ( `ffmpeg -i ${ inputName } -f wav ${ outputName }` ) ; dbgdecoder ( '%s finished transcoding' , uri ) ; dbgdecoder ( '%s loading transcoded file' , uri ) ; const wavData = await readFile ( outputName ) ; dbgdecoder ( '%s loaded transcoded wav (%d bytes)' , uri , wavData . byteLength ) ; dbgdecoder ( '%s decoding wav' , uri ) ; const decoded = await WavDecoder . decode ( wavData ) ; dbgdecoder ( '%s decoded into %d channels @ %dhz' , uri , decoded . channelData . length , decoded . sampleRate ) ; const audio = new XAudioBuffer ( { sampleRate : decoded . sampleRate , numberOfChannels : decoded . channelData . length , length : decoded . channelData [ 0 ] . length } ) ; for ( let i = 0 ; i < decoded . channelData . length ; i ++ ) { const chan = decoded . channelData [ i ] ; audio . copyToChannel ( chan , i , 0 ) ; } resolve ( audio ) ; } ) ; } type StringToType < T extends 'string' | 'number' | 'boolean' > = T extends 'string' ? string : T extends 'number' ? number : T extends 'boolean' ? boolean : never ; function parseFlagArg < KindName extends 'string' | 'number' | 'boolean' , Kind extends StringToType < KindName > > ( config ) { const longIdx = process . argv . indexOf ( config . long ) ; const shortIdx = process . argv . indexOf ( config . alias ) ; let value ; if ( longIdx > - 1 ) { value = config . kind === 'boolean' ? true : process . argv [ longIdx + 1 ] ; } else if ( shortIdx > - 1 ) { value = config . kind === 'boolean' ? true : process . argv [ shortIdx + 1 ] ; } else { value = config . default ; } if ( config . required && value === undefined ) throw new Error ( `Required flag ${ config . long } (${ config . alias }) had no value.` ) ; const parsed = config . kind === 'boolean' ? Boolean ( value ) : config . kind === 'number' ? Number ( value ) : String ( value ) ; return parsed as Kind ; } function parseBareArg < Command extends string > ( config ) { let command ; for ( let i = 0 ; i < config . name . length ; i ++ ) { const idx = process . argv . indexOf ( config . name [ i ] ) ; if ( idx > - 1 ) command = config . name [ i ] ; } if ( command === undefined ) throw new Error ( `Required command "${ config . name }" not provided.` ) ; return command ; }