UNPKG

@oletizi/audio-tools

Version:

Monorepo for hardware sampler utilities and format parsers

424 lines (349 loc) 13 kB
import {byte2nibblesLE, bytes2numberLE, nibbles2byte, newClientOutput, ProcessOutput} from "@oletizi/sampler-lib" import { KeygroupHeader, parseKeygroupHeader, parseProgramHeader, parseSampleHeader, ProgramHeader, SampleHeader, akaiByte2String, string2AkaiBytes, nextByte } from "@oletizi/sampler-devices/s3k"; import * as easymidi from "easymidi"; type MidiMessage = number[]; import {ExecutionResult} from "@oletizi/sampler-devices"; import EventEmitter from "events"; export interface Device { init(): Promise<void> getCurrentProgram(): Program | undefined fetchSampleNames(names: any[]): Promise<String[]> fetchProgramNames(names: string[]): Promise<string[]> fetchSampleHeader(sampleNumber: number, header: SampleHeader): Promise<SampleHeader> getSampleNames(names: string[]): string[] getProgramNames(names: string[]): string[] getProgramHeader(programName: string): ProgramHeader | undefined fetchProgramHeader(programNumber: number, header: ProgramHeader): Promise<ProgramHeader> getProgram(programNumber: number): Promise<Program> getSample(sampleName: string): Promise<AkaiS3kSample> fetchKeygroupHeader(programNumber: number, keygroupNumber: number, header: KeygroupHeader): Promise<KeygroupHeader> writeProgramName(header: ProgramHeader, name: string): Promise<void> writeProgramPolyphony(header: ProgramHeader, polyphony: number): Promise<void> send(opcode: number, data: number[]): Promise<number[]> sendRaw(message: number[]): Promise<number[]> format(partitionSize: number, partitionCount: number): Promise<ExecutionResult> } export function newDevice(input: easymidi.Input, output: easymidi.Output, out: ProcessOutput = newClientOutput()): Device { return new s3000xl(input, output, out) } // noinspection JSUnusedGlobalSymbols enum Opcode { // ID Mnemonic Direction Description // 00h RSTAT < request S1000 status RSTAT = 0x00, // 01h STAT > S1000 status report STAT, // 02h RPLIST > request list of resident program names RPLIST, // 03h PLIST > list of resident program names PLIST, // 04h RSLIST < request list of resident sample names RLIST, // 05h SLIST > list of resident sample names SLIST, // 06h RPDATA < request program common data RPDATA, // 07h PDATA <> program common data PDATA, // 08h RKDATA < request keygroup data RKDATA, // 09h KDATA <> keygroup data KDATA, // 0Ah RSDATA < request sample header data RSDATA, // 0Bh SDATA <> sample header data SDATA, // 0Ch RSPACK < request sample data packet(s) RSPACK, // 0Dh ASPACK < accept sample data packet(s) ASPACK, // 0Eh RDDATA < request drum settings OxRDDATA, // 0Fh DDATA <> drum input settings DDATA, // 10h RMDATA < request miscellaneous data RMDATA, // 11h MDATA <> miscellaneous data MDATA, // 12h DELP < delete program and its keygroup DELP, // 13h DELK < delete keygroup DELK, // 14h DELS < delete sample header and data DELS, // 15h SETEX < set S1000 exclusive channel SETEX, // 16h REPLY > S1000 command reply (error or ok) REPLY, // 1Dh CASPACK < corrected ASPACK CASPACK = 0x1D } class s3000xl implements Device { private readonly midiInput: easymidi.Input; private readonly midiOutput: easymidi.Output; private readonly programNames: string[] = [] private readonly programs = new Map<string, ProgramHeader>() private readonly sampleNames: string[] = [] private currentProgram: Program | undefined private readonly out: ProcessOutput constructor(input: easymidi.Input, output: easymidi.Output, out: ProcessOutput) { this.midiInput = input this.midiOutput = output this.out = out } async init() { const out = this.out await this.fetchProgramNames([]) out.log(`Fetching current program...`) this.currentProgram = await this.getProgram(0) out.log(`Current program set: ${this.currentProgram.getProgramName()}`) await this.fetchSampleNames([]) } getCurrentProgram(): Program | undefined { return this.currentProgram } getSampleNames(names: string[]) { this.sampleNames.forEach(n => names.push(n)) return names } getProgramNames(names: string[]) { this.programNames.forEach(n => names.push(n)) return names } async fetchProgramNames(names: string[] = []) { const out = this.out this.programNames.length = 0 let offset = 5 out.log(`Request program names...`) const m = await this.send(Opcode.RPLIST, []) out.log(`Received program names.`) let b = m.slice(offset, offset + 2) offset += 2 const programCount = bytes2numberLE(b) for (let i = 0; i < programCount; i++, offset += 12) { let n = akaiByte2String(m.slice(offset, offset + 12)); this.programNames.push(n) names.push(n) } return names } async fetchSampleNames(names: string[]) { this.sampleNames.length = 0 let offset = 5 const m = await this.send(Opcode.RLIST, []) let b = m.slice(offset, offset + 2); offset += 2 const sampleCount = bytes2numberLE(b) for (let i = 0; i < sampleCount; i++, offset += 12) { const n = akaiByte2String(m.slice(offset, offset + 12)); this.sampleNames.push(n) names.push(n) } return names } getProgramHeader(programName: string): ProgramHeader | undefined { return this.programs.get(programName) } async fetchProgramHeader(programNumber: number, header: ProgramHeader) { // See header spec: https://lakai.sourceforge.net/docs/s2800_sysex.html const out = this.out//newClientOutput(true, 'getProgramHeader') const m = await this.send(Opcode.RPDATA, byte2nibblesLE(programNumber)) const opcode = getOpcode(m) if (opcode === Opcode.REPLY) { throw new Error(`Error fetching program header.`) } const v = {value: 0, offset: 5} out.log(`PNUMBER: offset: ${v.offset}`) // header['PNUMBER'] = nextByte(m, v).value nextByte(m, v) out.log(`ProgramHeader header data offset: ${v.offset}`) const headerData = m.slice(v.offset, m.length - 1) parseProgramHeader(headerData, 1, header) header.raw = m out.log(header) return header } async getProgram(programNumber: number) { const header = await this.fetchProgramHeader(programNumber, {} as ProgramHeader) return new Program(this, header) } async getSample(sampleName: string) { const names: string[] = [] let rv = null await this.fetchSampleNames(names) let sampleNumber = -1 for (let i = 0; i < names.length; i++) { if (sampleName.trim().toUpperCase() === names[i].trim()) { sampleNumber = i break } } if (sampleNumber >= 0) { const header = await this.fetchSampleHeader(sampleNumber, {} as SampleHeader) rv = new AkaiS3kSample(this, header) } else { throw new Error(`Can't find sample named: ${sampleName}`) } return rv } async writeProgramName(header: ProgramHeader, name: string) { const offset = 13 // offset into raw sysex message for start of program name let v = '' for (let i = offset; i < offset + 12 * 2; i += 2) { const b = nibbles2byte(header.raw[i], header.raw[i + 1]) v += akaiByte2String([b]) } this.out.log(`current name: ${v}`) const data = string2AkaiBytes(name) for (let i = offset, j = 0; i < offset + 12 * 2; i += 2, j++) { const nibbles = byte2nibblesLE(data[j]) header.raw[i] = nibbles[0] header.raw[i + 1] = nibbles[1] } v = '' for (let i = offset; i < offset + 12 * 2; i += 2) { const b = nibbles2byte(header.raw[i], header.raw[i + 1]) v += akaiByte2String([b]) } this.out.log(`new name: ${v}`) await this.send(Opcode.PDATA, header.raw) } async writeProgramPolyphony(header: ProgramHeader, polyphony: number): Promise<void> { let offset = 13 // start of program name offset += 2 * 12 // PRGNUM offset += 2 // PMCHAN offset += 2 // POLYPH this.out.log(`Offset: ${offset}; calculated: ${(offset - 13) / 2}`) const d = byte2nibblesLE(polyphony) header.raw[offset] = d[0] header.raw[offset + 1] = d[1] await this.send(Opcode.PDATA, header.raw) } async fetchKeygroupHeader(programNumber: number, keygroupNumber: number, header: KeygroupHeader) { const out = this.out //newClientOutput(true, 'getKeygroupHeader') const m = await this.send(Opcode.RKDATA, byte2nibblesLE(programNumber).concat(keygroupNumber)) const v = {value: 0, offset: 5} out.log(`PNUMBER: offset: ${v.offset}`) // header['PNUMBER'] = nextByte(m, v).value nextByte(m, v) out.log(`KNUMBER: offset: ${v.offset}`) // header['KNUMBER'] = m[v.offset++] out.log(`offset after KNUMBER: ${v.offset}`) const headerData = m.slice(v.offset, m.length - 1) parseKeygroupHeader(headerData, 0, header) out.log(header) return header } async fetchSampleHeader(sampleNumber: number, header: SampleHeader) { // See header spec: https://lakai.sourceforge.net/docs/s2800_sysex.html const out = this.out // newClientOutput(true, 'getSampleHeader') const m = await this.send(Opcode.RSDATA, byte2nibblesLE(sampleNumber)) const v = {value: 0, offset: 5} out.log(`SNUMBER: offset: ${v.offset}`) // header['SNUMBER'] = nextByte(m, v).value nextByte(m, v) parseSampleHeader(m.slice(v.offset, m.length - 1), 0, header) out.log(header) header.raw = m return header } async send(opcode: Opcode, data: number[]): Promise<number[]> { const message = [ 0xf0, // 00: (240) SYSEX_START 0x47, // 01: ( 71) AKAI 0x00, // 02: ( 0) CHANNEL opcode, 0x48, // 04: ( 72) DEVICE ID ].concat(data) message.push(0xf7) // 21: (247) SYSEX_END) return this.sendRaw(message) } async sendRaw(message: number[]) { const out = this.out const input = this.midiInput const output = this.midiOutput const response = new Promise<number[]>((resolve) => { function listener(msg: {bytes: MidiMessage}) { input.removeListener('sysex', listener) // TODO: make sure the opcode in the response message is correct resolve(msg.bytes) } input.on('sysex', listener) }) out.log(`Sending message...`) output.send('sysex', {bytes: message}) return response } format(partitionSize: number, partitionCount: number): Promise<ExecutionResult> { return Promise.resolve({errors: [], code: 0}) } } function getOpcode(message: number[]) { let rv = -1 if (message.length >= 4) { rv = message[3] } return rv } export class Program { private readonly device: Device private readonly header: ProgramHeader constructor(device: Device, header: ProgramHeader) { this.device = device this.header = header } getHeader(): ProgramHeader { return this.header } async save() { return this.device.sendRaw(this.header.raw) } getProgramName(): string { return this.header.PRNAME } } export class Keygroup { private readonly device: Device private readonly header: KeygroupHeader constructor(device: Device, header: KeygroupHeader) { this.device = device this.header = header } getHeader(): KeygroupHeader { return this.header } async save() { return this.device.sendRaw(this.header.raw) } } export class AkaiS3kSample { private readonly device: Device private readonly header: SampleHeader constructor(device: Device, header: SampleHeader) { this.device = device this.header = header } getHeader(): SampleHeader { return this.header } async save() { return this.device.sendRaw(this.header.raw) } getSampleName(): string { return this.header.SHNAME } getPitch(): number { return this.header.SPITCH } } // Alias for backward compatibility export const Sample = AkaiS3kSample