├── .nvmrc ├── .npmrc ├── src ├── chunks │ ├── presets │ │ ├── index.ts │ │ └── headers.ts │ ├── samples │ │ ├── index.ts │ │ └── headers.ts │ ├── instruments │ │ ├── index.ts │ │ └── headers.ts │ ├── index.ts │ ├── modulators.ts │ ├── generators.ts │ └── zones.ts ├── utils │ ├── index.ts │ ├── buffer.ts │ └── memoize.ts ├── index.ts ├── riff │ ├── index.ts │ ├── parseError.ts │ ├── parser.ts │ ├── chunkIterator.ts │ └── riffChunk.ts ├── types │ ├── bank.ts │ ├── index.ts │ ├── instrument.ts │ ├── preset.ts │ ├── key.ts │ ├── zone.ts │ ├── presetData.ts │ ├── metaData.ts │ ├── sample.ts │ ├── modulator.ts │ └── generator.ts ├── constants.ts ├── chunk.ts └── soundFont2.ts ├── tests ├── fonts │ ├── valid.sf2 │ └── invalid.sf2 ├── riff │ ├── parseError.test.ts │ ├── chunkIterator.test.ts │ ├── riffChunk.test.ts │ ├── mocks │ │ └── buffer.ts │ └── parser.test.ts └── soundFont2.test.ts ├── jest.setup.js ├── book.json ├── .prettierrc ├── docs ├── api │ ├── soundfont2 │ │ ├── bank.md │ │ ├── preset.md │ │ ├── meta-data.md │ │ ├── instrument.md │ │ ├── preset-data.md │ │ ├── sample │ │ │ ├── README.md │ │ │ ├── sample-type.md │ │ │ └── sample-header.md │ │ ├── key.md │ │ └── README.md │ ├── modulator.md │ ├── generator │ │ ├── README.md │ │ └── generator-type.md │ └── README.md ├── getting-started │ ├── README.md │ ├── basic-usage.md │ ├── installation.md │ └── soundfont2-structure.md ├── _layouts │ └── website │ │ ├── header.html │ │ └── summary.html ├── todo.md ├── README.md ├── SUMMARY.md └── development.md ├── .editorconfig ├── .travis.yml ├── jest.config.js ├── .gitignore ├── tsconfig.json ├── tslint.json ├── README.md ├── LICENSE ├── webpack.config.ts └── package.json /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/* 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /src/chunks/presets/index.ts: -------------------------------------------------------------------------------- 1 | export * from './headers'; 2 | -------------------------------------------------------------------------------- /src/chunks/samples/index.ts: -------------------------------------------------------------------------------- 1 | export * from './headers'; 2 | -------------------------------------------------------------------------------- /src/chunks/instruments/index.ts: -------------------------------------------------------------------------------- 1 | export * from './headers'; 2 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './buffer'; 2 | export * from './memoize'; 3 | -------------------------------------------------------------------------------- /tests/fonts/valid.sf2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mrtenz/soundfont2/HEAD/tests/fonts/valid.sf2 -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | // Required for Jest to work on older Node.js versions 2 | global.TextDecoder = require('util').TextDecoder; 3 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | export * from './chunk'; 3 | export * from './constants'; 4 | export * from './soundFont2'; 5 | -------------------------------------------------------------------------------- /book.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": "./docs", 3 | "title": "Soundfont2", 4 | "description": "A SoundFont2 parser for Node.js and web browsers" 5 | } 6 | -------------------------------------------------------------------------------- /src/riff/index.ts: -------------------------------------------------------------------------------- 1 | export * from './chunkIterator'; 2 | export * from './parseError'; 3 | export * from './parser'; 4 | export * from './riffChunk'; 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true, 4 | "useTabs": false, 5 | "semi": true, 6 | "tabWidth": 2, 7 | "trailingComma": "none" 8 | } 9 | -------------------------------------------------------------------------------- /docs/api/soundfont2/bank.md: -------------------------------------------------------------------------------- 1 | # Bank 2 | 3 | TODO 4 | 5 | You can refer to [the source code](https://github.com/Mrtenz/soundfont2/blob/master/src/types/bank.ts#L6) for now. 6 | -------------------------------------------------------------------------------- /docs/api/modulator.md: -------------------------------------------------------------------------------- 1 | # Modulator 2 | 3 | TODO 4 | 5 | You can refer to [the source code](https://github.com/Mrtenz/soundfont2/blob/master/src/types/modulator.ts#L17) for now. 6 | -------------------------------------------------------------------------------- /docs/api/soundfont2/preset.md: -------------------------------------------------------------------------------- 1 | # Preset 2 | 3 | TODO 4 | 5 | You can refer to [the source code](https://github.com/Mrtenz/soundfont2/blob/master/src/types/preset.ts#L40) for now. 6 | -------------------------------------------------------------------------------- /docs/api/generator/README.md: -------------------------------------------------------------------------------- 1 | # Generator 2 | 3 | TODO 4 | 5 | You can refer to [the source code](https://github.com/Mrtenz/soundfont2/blob/master/src/types/generator.ts#L470) for now. 6 | -------------------------------------------------------------------------------- /docs/api/soundfont2/meta-data.md: -------------------------------------------------------------------------------- 1 | # MetaData 2 | 3 | TODO 4 | 5 | You can refer to [the source code](https://github.com/Mrtenz/soundfont2/blob/master/src/types/metaData.ts#L4) for now. 6 | -------------------------------------------------------------------------------- /docs/api/soundfont2/instrument.md: -------------------------------------------------------------------------------- 1 | # Instrument 2 | 3 | TODO 4 | 5 | You can refer to [the source code](https://github.com/Mrtenz/soundfont2/blob/master/src/types/instrument.ts#L23) for now. 6 | -------------------------------------------------------------------------------- /docs/api/soundfont2/preset-data.md: -------------------------------------------------------------------------------- 1 | # PresetData 2 | 3 | TODO 4 | 5 | You can refer to [the source code](https://github.com/Mrtenz/soundfont2/blob/master/src/types/presetData.ts#L11) for now. 6 | -------------------------------------------------------------------------------- /docs/api/generator/generator-type.md: -------------------------------------------------------------------------------- 1 | # GeneratorType 2 | 3 | TODO 4 | 5 | You can refer to [the source code](https://github.com/Mrtenz/soundfont2/blob/master/src/types/generator.ts#L6) for now. 6 | -------------------------------------------------------------------------------- /src/chunks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './instruments'; 2 | export * from './presets'; 3 | export * from './samples'; 4 | export * from './generators'; 5 | export * from './modulators'; 6 | export * from './zones'; 7 | -------------------------------------------------------------------------------- /docs/getting-started/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ## Table of Contents 4 | 5 | * [Installation](installation.md) 6 | * [Basic Usage](basic-usage.md) 7 | * [SoundFont2 Structure](soundfont2-structure.md) 8 | -------------------------------------------------------------------------------- /src/types/bank.ts: -------------------------------------------------------------------------------- 1 | import { Preset } from './preset'; 2 | 3 | /** 4 | * Describes a MIDI bank. 5 | */ 6 | export interface Bank { 7 | /** 8 | * The presets in the bank. 9 | */ 10 | presets: Preset[]; 11 | } 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | end_of_line = lf 6 | charset = utf-8 7 | indent_size = 2 8 | indent_style = space 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | cache: 4 | yarn: true 5 | directories: 6 | - node_modules 7 | 8 | install: 9 | - yarn --silent 10 | 11 | script: 12 | - yarn run test 13 | - yarn run tslint 14 | - yarn run prettier:diff 15 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './bank'; 2 | export * from './generator'; 3 | export * from './instrument'; 4 | export * from './key'; 5 | export * from './metaData'; 6 | export * from './modulator'; 7 | export * from './preset'; 8 | export * from './presetData'; 9 | export * from './sample'; 10 | export * from './zone'; 11 | -------------------------------------------------------------------------------- /docs/_layouts/website/header.html: -------------------------------------------------------------------------------- 1 | {% block book_header %} 2 | 7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | preset: 'ts-jest', 4 | testMatch: [ 5 | '**/tests/**/*.test.ts' 6 | ], 7 | moduleFileExtensions: [ 8 | 'ts', 9 | 'js' 10 | ], 11 | moduleNameMapper: { 12 | '~/(.*)': '/src/$1' 13 | }, 14 | setupFiles: [ 15 | '/jest.setup.js' 16 | ] 17 | }; 18 | -------------------------------------------------------------------------------- /docs/api/soundfont2/sample/README.md: -------------------------------------------------------------------------------- 1 | # Sample 2 | 3 | ## Table of Contents 4 | 5 | * [`Sample.header`](#sampleheader) 6 | * [`Sample.data`](#sampledata) 7 | 8 | ## `Sample.header` 9 | 10 | * [<SampleHeader>](sample-header.md) - The sample header data. 11 | 12 | ## `Sample.data` 13 | 14 | * [<Int16Array>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Int16Array) - The 16-bit WAV sample data. 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | *.log 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | pids 7 | *.pid 8 | *.seed 9 | *.pid.lock 10 | lib-cov 11 | coverage 12 | .nyc_output 13 | .grunt 14 | bower_components 15 | .lock-wscript 16 | build/Release 17 | node_modules/ 18 | jspm_packages/ 19 | typings/ 20 | .npm 21 | .eslintcache 22 | .node_repl_history 23 | *.tgz 24 | .yarn-integrity 25 | .env 26 | .next 27 | 28 | # Webpack 29 | lib 30 | 31 | # Docs 32 | _book 33 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./src/", 4 | "sourceMap": true, 5 | "strictNullChecks": true, 6 | "allowSyntheticDefaultImports": true, 7 | "module": "commonjs", 8 | "target": "es2017", 9 | "noEmitOnError": true, 10 | "strict": true, 11 | "paths": { 12 | "~/*": [ 13 | "*" 14 | ] 15 | } 16 | }, 17 | "include": [ 18 | "src" 19 | ], 20 | "exclude": [ 21 | "node_modules" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /src/riff/parseError.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Error class used for all errors encountered during the parsing of the SoundFont 2 file. 3 | */ 4 | export class ParseError extends Error { 5 | public constructor(message: string); 6 | public constructor(message: string, expected: string, received: string); 7 | public constructor(message: string, expected?: string, received?: string) { 8 | super( 9 | `${message}${expected && received ? `, expected ${expected}, received ${received}` : ``}` 10 | ); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/riff/parseError.test.ts: -------------------------------------------------------------------------------- 1 | import { ParseError } from '../../src/riff'; 2 | 3 | describe('ParseError', () => { 4 | it('should have a message without expected and received result', () => { 5 | const error = new ParseError('foo'); 6 | expect(error.message).toBe('foo'); 7 | }); 8 | 9 | it('should have a message with expected and received result', () => { 10 | const error = new ParseError('foo', 'bar', 'baz'); 11 | expect(error.message).toBe('foo, expected bar, received baz'); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /docs/todo.md: -------------------------------------------------------------------------------- 1 | # Todo 2 | 3 | These are things that will likely be implemented in the future. 4 | 5 | * Comply to the full [SoundFont 2.04 specification](http://www.synthfont.com/sfspec24.pdf). 6 | * Add support for 24-bit data samples. 7 | * Better checks if a SoundFont is valid. 8 | * ~~Support for global preset and instrument zones.~~ 9 | * Maybe add support for sfArk compressed SoundFont files. 10 | * Add option to create, edit and save SoundFonts. 11 | * Increase unit test coverage. 12 | * Improve documentation. 13 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const SF_VERSION_LENGTH = 4; 2 | export const SF_PRESET_HEADER_SIZE = 38; 3 | export const SF_BAG_SIZE = 4; 4 | export const SF_MODULATOR_SIZE = 10; 5 | export const SF_GENERATOR_SIZE = 4; 6 | export const SF_INSTRUMENT_HEADER_SIZE = 22; 7 | export const SF_SAMPLE_HEADER_SIZE = 46; 8 | 9 | export const DEFAULT_SAMPLE_RATE = 22050; 10 | 11 | export type SF_INFO_CHUNKS = 12 | | 'ifil' 13 | | 'isng' 14 | | 'INAM' 15 | | 'irom' 16 | | 'iver' 17 | | 'ICRD' 18 | | 'IENG' 19 | | 'IPRD' 20 | | 'ICOP' 21 | | 'ICMT' 22 | | 'ISFT'; 23 | -------------------------------------------------------------------------------- /tests/riff/chunkIterator.test.ts: -------------------------------------------------------------------------------- 1 | import buffer from './mocks/buffer'; 2 | import { parseBuffer } from '../../src/riff'; 3 | 4 | const chunk = parseBuffer(buffer); 5 | 6 | describe('ChunkIterator', () => { 7 | it('should increase the position on reading', () => { 8 | const iterator = chunk.iterator(); 9 | expect(iterator.getString(4)).toBe('sfbk'); 10 | expect(iterator.currentPosition).toBe(4); 11 | expect(iterator.getString(4)).toBe('LIST'); 12 | expect(iterator.getUInt32()).toBe(8); 13 | expect(iterator.currentPosition).toBe(12); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/utils/buffer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Convert a UTF-8 encoded buffer into a string. This will read the full buffer as UTF-8 encoded 3 | * string and return anything before the first NULL character. The output text is trimmed of any 4 | * preceding or following spaces. 5 | * 6 | * @param {Buffer} buffer - The input buffer 7 | */ 8 | export const getStringFromBuffer = (buffer: Uint8Array): string => { 9 | const decoded = new TextDecoder('utf8').decode(buffer); 10 | const nullIndex = decoded.indexOf('\0'); 11 | return (nullIndex === -1 ? decoded : decoded.slice(0, nullIndex)).trim(); 12 | }; 13 | -------------------------------------------------------------------------------- /docs/api/README.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | This section has information on all the public APIs in the library. 4 | 5 | ## Table of Contents 6 | 7 | * [SoundFont2](soundfont2/README.md) 8 | * [MetaData](soundfont2/meta-data.md) 9 | * [Sample](soundfont2/sample/README.md) 10 | * [SampleHeader](soundfont2/sample/sample-header.md) 11 | * [SampleType](soundfont2/sample/sample-type.md) 12 | * [PresetData](soundfont2/preset-data.md) 13 | * [Preset](soundfont2/preset.md) 14 | * [Instrument](soundfont2/instrument.md) 15 | * [Bank](soundfont2/bank.md) 16 | * [Key](soundfont2/key.md) 17 | * [Generator](generator/README.md) 18 | * [GeneratorType](generator/generator-type.md) 19 | * [Modulator](modulator.md) 20 | -------------------------------------------------------------------------------- /src/types/instrument.ts: -------------------------------------------------------------------------------- 1 | import { ZoneItems } from './zone'; 2 | import { Sample } from './sample'; 3 | 4 | export interface InstrumentHeader { 5 | /** 6 | * The name of the instrument. 7 | */ 8 | name: string; 9 | 10 | /** 11 | * Index in the instrument's zone list found in the instrument bag sub-chunk. 12 | */ 13 | bagIndex: number; 14 | } 15 | 16 | export interface InstrumentZone extends ZoneItems { 17 | /** 18 | * The sample for the instrument zone. 19 | */ 20 | sample: Sample; 21 | } 22 | 23 | export interface Instrument { 24 | /** 25 | * The instrument header. 26 | */ 27 | header: InstrumentHeader; 28 | 29 | /** 30 | * The instrument zones. 31 | */ 32 | zones: InstrumentZone[]; 33 | } 34 | -------------------------------------------------------------------------------- /src/utils/memoize.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns a memoized function for the original function. Function arguments are serialized as a 3 | * JSON string and stored in an in-memory object. 4 | * 5 | * @template T 6 | * @template U 7 | * @param {(...originalArgs: T[]) => U} originalFunction 8 | */ 9 | export const memoize = ( 10 | originalFunction: (...originalArgs: T[]) => U 11 | ): ((...args: T[]) => U) => { 12 | const memo: { [key: string]: U } = {}; 13 | 14 | return (...args: T[]) => { 15 | const serializedArgs = JSON.stringify(args); 16 | if (serializedArgs in memo) { 17 | return memo[serializedArgs]; 18 | } 19 | 20 | const output = originalFunction(...args); 21 | memo[serializedArgs] = output; 22 | return output; 23 | }; 24 | }; 25 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | This is a SoundFont2 parser (and soon to be editor), written in TypeScript. It works in Node.js and (modern) web browsers. The parser is based on the [SoundFont 2.01 specification](http://www.synthfont.com/SFSPEC21.PDF), but is currently not fully compliant yet. The end goal is to be fully compliant with the [SoundFont 2.04 specification](http://www.synthfont.com/sfspec24.pdf). 4 | 5 | This library is not ready for production yet, hence the version 0.x.x. Some SoundFonts may be parsed incorrectly and the API may have breaking changes in the future. The first release that is suitable for production use-cases will be version 1.0.0 and it will follow semver from there on. 6 | 7 | ## Getting Started 8 | 9 | See [Getting Started](getting-started/README.md). 10 | -------------------------------------------------------------------------------- /tests/fonts/invalid.sf2: -------------------------------------------------------------------------------- 1 | RIFF\B2\00\00sfbkLIST\B0\00\00\00INFOfoo\00\00\00\00\00\00isng\00\00\00EMU8000\00INAM 2 | \00\00\00no title\00\00IPRD\00\00\00Jest\00\00ICOP \00\00\00Copyright (c) Maarten Zuidhoorn\00ICMT.\00\00\00This SoundFont is made for testing purposes.\00\00ISFT 3 | \00\00\00Polyphone\00LIST \00\00\00sdtasmpl\00\00\00\00LIST\DA\00\00\00pdtaphdr&\00\00\00EOP\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00pbag\00\00\00\00\00\00\00pmod 4 | \00\00\00\00\00\00\00\00\00\00\00\00\00pgen\00\00\00\00\00\00\00inst\00\00\00EOI\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00ibag\00\00\00\00\00\00\00imod 5 | \00\00\00\00\00\00\00\00\00\00\00\00\00igen\00\00\00\00\00\00\00shdr.\00\00\00EOS\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00 6 | -------------------------------------------------------------------------------- /src/chunks/instruments/headers.ts: -------------------------------------------------------------------------------- 1 | import { SF2Chunk } from '~/chunk'; 2 | import { ParseError } from '~/riff'; 3 | import { SF_INSTRUMENT_HEADER_SIZE } from '~/constants'; 4 | import { InstrumentHeader } from '~/types'; 5 | 6 | /** 7 | * Get all instrument headers from a `inst` sub-chunk. 8 | * 9 | * @param {SF2Chunk} chunk - The input chunk 10 | */ 11 | export const getInstrumentHeaders = (chunk: SF2Chunk): InstrumentHeader[] => { 12 | if (chunk.id !== 'inst') { 13 | throw new ParseError('Unexpected chunk ID', `'inst'`, `'${chunk.id}'`); 14 | } 15 | 16 | if (chunk.length % SF_INSTRUMENT_HEADER_SIZE) { 17 | throw new ParseError(`Invalid size for the 'inst' sub-chunk`); 18 | } 19 | 20 | return chunk.iterate(iterator => { 21 | return { 22 | name: iterator.getString(), 23 | bagIndex: iterator.getInt16() 24 | }; 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /docs/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | * [Introduction](README.md) 4 | * [Getting Started](getting-started/README.md) 5 | * [Installation](getting-started/installation.md) 6 | * [Basic Usage](getting-started/basic-usage.md) 7 | * [SoundFont2 Structure](getting-started/soundfont2-structure.md) 8 | * [API](api/README.md) 9 | * [SoundFont2](api/soundfont2/README.md) 10 | * [MetaData](api/soundfont2/meta-data.md) 11 | * [Sample](api/soundfont2/sample/README.md) 12 | * [SampleHeader](api/soundfont2/sample/sample-header.md) 13 | * [SampleType](api/soundfont2/sample/sample-type.md) 14 | * [PresetData](api/soundfont2/preset-data.md) 15 | * [Preset](api/soundfont2/preset.md) 16 | * [Instrument](api/soundfont2/instrument.md) 17 | * [Bank](api/soundfont2/bank.md) 18 | * [Key](api/soundfont2/key.md) 19 | * [Generator](api/generator/README.md) 20 | * [GeneratorType](api/generator/generator-type.md) 21 | * [Modulator](api/modulator.md) 22 | * [Development](development.md) 23 | * [Todo](todo.md) 24 | -------------------------------------------------------------------------------- /src/types/preset.ts: -------------------------------------------------------------------------------- 1 | import { ZoneItems } from './zone'; 2 | import { Instrument } from './instrument'; 3 | 4 | export interface PresetHeader { 5 | /** 6 | * The name of the preset. 7 | */ 8 | name: string; 9 | 10 | /** 11 | * The MIDI preset number which to apply to the preset. 12 | */ 13 | preset: number; 14 | 15 | /** 16 | * The preset bank. 17 | */ 18 | bank: number; 19 | 20 | /** 21 | * Index in the preset's zone list found in the preset bag sub-chunk. 22 | */ 23 | bagIndex: number; 24 | 25 | /** 26 | * Reserved for future implementation. 27 | */ 28 | library: number; 29 | genre: number; 30 | morphology: number; 31 | } 32 | 33 | export interface PresetZone extends ZoneItems { 34 | /** 35 | * The instrument for the preset zone. 36 | */ 37 | instrument: Instrument; 38 | } 39 | 40 | export interface Preset { 41 | /** 42 | * The preset header. 43 | */ 44 | header: PresetHeader; 45 | 46 | /** 47 | * The preset zones. 48 | */ 49 | zones: PresetZone[]; 50 | } 51 | -------------------------------------------------------------------------------- /src/chunks/presets/headers.ts: -------------------------------------------------------------------------------- 1 | import { SF2Chunk } from '~/chunk'; 2 | import { ParseError } from '~/riff'; 3 | import { SF_PRESET_HEADER_SIZE } from '~/constants'; 4 | import { PresetHeader } from '~/types'; 5 | 6 | /** 7 | * Get all preset headers from a `phdr` sub-chunk. 8 | * 9 | * @param {SF2Chunk} chunk - The input chunk 10 | */ 11 | export const getPresetHeaders = (chunk: SF2Chunk): PresetHeader[] => { 12 | if (chunk.id !== 'phdr') { 13 | throw new ParseError('Invalid chunk ID', `'phdr'`, `'${chunk.id}'`); 14 | } 15 | 16 | if (chunk.length % SF_PRESET_HEADER_SIZE) { 17 | throw new ParseError(`Invalid size for the 'phdr' sub-chunk`); 18 | } 19 | 20 | return chunk.iterate(iterator => { 21 | return { 22 | name: iterator.getString(), 23 | preset: iterator.getInt16(), 24 | bank: iterator.getInt16(), 25 | bagIndex: iterator.getInt16(), 26 | library: iterator.getUInt32(), 27 | genre: iterator.getUInt32(), 28 | morphology: iterator.getUInt32() 29 | }; 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint:recommended", "tslint-config-prettier"], 4 | "jsRules": { 5 | "quotemark": false, 6 | "max-line-length": false, 7 | "trailing-comma": false, 8 | "object-literal-sort-keys": false 9 | }, 10 | "rules": { 11 | "quotemark": false, 12 | "trailing-comma": false, 13 | "object-literal-sort-keys": false, 14 | "arrow-return-shorthand": true, 15 | "prefer-method-signature": true, 16 | "arrow-parens": false, 17 | "no-unnecessary-type-assertion": true, 18 | "array-type": [true, "array"], 19 | "interface-name": false, 20 | "no-duplicate-imports": true, 21 | "no-console": false, 22 | "no-var-requires": false, 23 | "comment-format": false, 24 | "ordered-imports": false, 25 | "member-access": [true, "check-accessor", "check-constructor", "check-parameter-property"], 26 | "jsdoc-format": [true, "check-multiline-start"], 27 | "no-bitwise": false 28 | }, 29 | "rulesDirectory": ["node_modules/tslint-microsoft-contrib"] 30 | } 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # soundfont2 2 | 3 | This is a SoundFont2 parser (and soon to be editor), written in TypeScript. It works in Node.js and (modern) web browsers. The parser is based on the [SoundFont 2.01 specification](http://www.synthfont.com/SFSPEC21.PDF), but is currently not fully compliant yet. The end goal is to be fully compliant with the [SoundFont 2.04 specification](http://www.synthfont.com/sfspec24.pdf). 4 | 5 | This library is not ready for production yet, hence the version 0.x.x. Some SoundFonts may be parsed incorrectly and the API may have breaking changes in the future. The first release that is suitable for production use-cases will be version 1.0.0 and it will follow semver from there on. 6 | 7 | ## Installation 8 | 9 | The library can be installed with `yarn`, `npm` or using a ` 42 | ``` 43 | 44 | The library will be available as `window.SoundFont2`. 45 | 46 | ```typescript 47 | const { SoundFont2 } = window.SoundFont2; 48 | ``` 49 | -------------------------------------------------------------------------------- /src/types/presetData.ts: -------------------------------------------------------------------------------- 1 | import { PresetHeader } from './preset'; 2 | import { Zone } from './zone'; 3 | import { Modulator } from './modulator'; 4 | import { Generator } from './generator'; 5 | import { InstrumentHeader } from './instrument'; 6 | import { SampleHeader } from './sample'; 7 | 8 | /** 9 | * All the data found in the `pdta` sub-chunk. 10 | */ 11 | export interface PresetData { 12 | /** 13 | * The preset headers, found in the `PHDR' sub-chunk. 14 | */ 15 | presetHeaders: PresetHeader[]; 16 | 17 | /** 18 | * The preset zones, found in the `PBAG` sub-chunk. 19 | */ 20 | presetZones: Zone[]; 21 | 22 | /** 23 | * The preset modulators, found in the `PMOD` sub-chunk. 24 | */ 25 | presetModulators: Modulator[]; 26 | 27 | /** 28 | * The preset generators, found in the `PGEN` sub-chunk. 29 | */ 30 | presetGenerators: Generator[]; 31 | 32 | /** 33 | * The instrument headers, found in the `INST` sub-chunk. 34 | */ 35 | instrumentHeaders: InstrumentHeader[]; 36 | 37 | /** 38 | * The instrument zones, found in the `IBAG` sub-chunk. 39 | */ 40 | instrumentZones: Zone[]; 41 | 42 | /** 43 | * The instrument modulators, found in the `IMOD` sub-chunk. 44 | */ 45 | instrumentModulators: Modulator[]; 46 | 47 | /** 48 | * The instrument generators, found in the `IGEN` sub-chunk. 49 | */ 50 | instrumentGenerators: Generator[]; 51 | 52 | /** 53 | * The sample headers, found in the `SHDR` sub-chunk. 54 | */ 55 | sampleHeaders: SampleHeader[]; 56 | } 57 | -------------------------------------------------------------------------------- /src/chunks/modulators.ts: -------------------------------------------------------------------------------- 1 | import { SF2Chunk } from '~/chunk'; 2 | import { Modulator, ControllerValue } from '~/types'; 3 | import { ParseError } from '~/riff'; 4 | import { SF_MODULATOR_SIZE } from '~/constants'; 5 | 6 | /** 7 | * Get the modulator enumerator value from a 16-bit integer. 8 | * 9 | * @param {number} value - The 16-bit integer 10 | */ 11 | const getModulatorValue = (value: number): ControllerValue => { 12 | return { 13 | type: (value >> 10) & 0x3f, 14 | polarity: (value >> 9) & 1, 15 | direction: (value >> 8) & 1, 16 | palette: (value >> 7) & 1, 17 | index: value & 0x7f 18 | }; 19 | }; 20 | 21 | /** 22 | * Get the modulators from either a `pmod` (presets) or `imod` (instruments) chunk. 23 | * 24 | * @param {SF2Chunk} chunk - The input chunk 25 | * @param {string} type - The type of chunk, either 'pmod' or 'imod' 26 | */ 27 | export const getModulators = (chunk: SF2Chunk, type: 'pmod' | 'imod'): Modulator[] => { 28 | if (chunk.id !== type) { 29 | throw new ParseError('Unexpected chunk ID', `'${type}'`, `'${chunk.id}'`); 30 | } 31 | 32 | if (chunk.length % SF_MODULATOR_SIZE) { 33 | throw new ParseError(`Invalid size for the '${type}' sub-chunk`); 34 | } 35 | 36 | return chunk.iterate(iterator => { 37 | return { 38 | source: getModulatorValue(iterator.getInt16BE()), 39 | id: iterator.getInt16BE(), 40 | value: iterator.getInt16BE(), 41 | valueSource: getModulatorValue(iterator.getInt16BE()), 42 | transform: iterator.getInt16BE() 43 | }; 44 | }); 45 | }; 46 | -------------------------------------------------------------------------------- /docs/development.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | The library uses Node.js, TypeScript and Webpack for development, Jest to run unit tests, TSLint for linting the source code and Prettier for the code style. 4 | 5 | ## Table of Contents 6 | 7 | * [Requirements](#requirements) 8 | * [Getting Started](#getting-started) 9 | * [Running Unit Tests](#running-unit-tests) 10 | * [Linting](#linting) 11 | * [Code Style](#code-style) 12 | 13 | ## Requirements 14 | 15 | * A modern Node.js version 16 | * `yarn` 17 | 18 | ## Getting Started 19 | 20 | 1. Clone the repository. 21 | 22 | ```bash 23 | $ git clone git@github.com:Mrtenz/soundfont2.git 24 | ``` 25 | 26 | 2. Install the dependencies. 27 | 28 | ```bash 29 | $ yarn 30 | ``` 31 | 32 | 3. Build the files. 33 | 34 | ```bash 35 | $ yarn run build 36 | ``` 37 | 38 | ### Running Unit Tests 39 | 40 | The library uses Jest for unit tests. This is done automatically before committing, to prevent any bugs, but you can also run Jest manually. 41 | 42 | ```bash 43 | $ yarn run test 44 | ``` 45 | 46 | ### Linting 47 | 48 | Files are linted with TSLint. This is done automatically before committing, to ensure a consistent code base, but you can also run TSLint manually. 49 | 50 | ```bash 51 | $ yarn run tslint 52 | ``` 53 | 54 | ## Code Style 55 | 56 | The library uses Prettier to ensure a consistent code style. The Prettier settings can be found in [this file](https://github.com/Mrtenz/soundfont2/blob/master/.prettierrc). It is automatically run before committing, but you can also run Prettier manually. 57 | 58 | ```bash 59 | $ yarn run prettier:diff 60 | ``` 61 | -------------------------------------------------------------------------------- /docs/api/soundfont2/sample/sample-type.md: -------------------------------------------------------------------------------- 1 | # SampleType 2 | 3 | `SampleType` is an [enum](https://www.typescriptlang.org/docs/handbook/enums.html) with all the possible sample types. 4 | 5 | ## Table of Contents 6 | 7 | * [`SampleType.Mono`](#sampletypemono) 8 | * [`SampleType.Right`](#sampletyperight) 9 | * [`SampleType.Left`](#sampletypeleft) 10 | * [`SampleType.Linked`](#sampletypelinked) 11 | * [`SampleType.RomMono`](#sampletyperommono) 12 | * [`SampleType.RomRight`](#sampletyperomlinked) 13 | * [`SampleType.RomLeft`](#sampletyperomleft) 14 | * [`SampleType.RomLinked`](#sampletyperomlinked) 15 | 16 | ## `SampleType.Mono` 17 | 18 | * Value: `1` 19 | 20 | A sample with only one (mono) channel. 21 | 22 | ## `SampleType.Right` 23 | 24 | * Value: `2` 25 | 26 | A sample with two channels, where this sample is the right channel. 27 | 28 | ## `SampleType.Left` 29 | 30 | * Value: `4` 31 | 32 | A sample with two channels, where this sample is the left channel. 33 | 34 | ## `SampleType.Linked` 35 | 36 | * Value `8` 37 | 38 | This sample type is not fully defined in the SoundFont2 specification and is likely not used. 39 | 40 | ## `SampleType.RomMono` 41 | 42 | * Value `0x8001` (or `32769`) 43 | 44 | A sample, stored in the ROM, with only one (mono) channel. 45 | 46 | ## `SampleType.RomRight` 47 | 48 | * Value `0x8002` (or `32770`) 49 | 50 | A sample, stored in the ROM, where this sample is the right channel. 51 | 52 | ## `SampleType.RomLeft` 53 | 54 | * Value `0x8004` (or `32772`) 55 | 56 | A sample, stored in the ROM, where this sample is the left channel. 57 | 58 | ## `SampleType.RomLinked` 59 | 60 | * Value `0x8008` (or `32776`) 61 | 62 | This sample type is not fully defined in the SoundFont2 specification and is likely not used. 63 | -------------------------------------------------------------------------------- /docs/_layouts/website/summary.html: -------------------------------------------------------------------------------- 1 | {% macro articles(_articles) %} 2 | {% for article in _articles %} 3 |
  • 4 | {% if article.path and getPageByPath(article.path) %} 5 | 6 | {% elif article.url %} 7 | 8 | {% else %} 9 | 10 | {% endif %} 11 | {% if article.level != "0" and config.pluginsConfig['theme-default'].showLevel %} 12 | {{ article.level }}. 13 | {% endif %} 14 | {{ article.title }} 15 | {% if article.path or article.url %} 16 | 17 | {% else %} 18 | 19 | {% endif %} 20 | 21 | {% if article.articles.length > 0 %} 22 |
      23 | {{ articles(article.articles, file, config) }} 24 |
    25 | {% endif %} 26 |
  • 27 | {% endfor %} 28 | {% endmacro %} 29 | 30 |
      31 | {% set _divider = false %} 32 | {% if config.links.sidebar %} 33 | {% for linkTitle, link in config.links.sidebar %} 34 | {% set _divider = true %} 35 |
    • 36 | {{ linkTitle }} 37 |
    • 38 | {% endfor %} 39 | {% endif %} 40 | 41 | {% if _divider %} 42 |
    • 43 | {% endif %} 44 | 45 | {% for part in summary.parts %} 46 | {% if part.title %} 47 |
    • {{ part.title }}
    • 48 | {% elif not loop.first %} 49 |
    • 50 | {% endif %} 51 | {{ articles(part.articles, file, config) }} 52 | {% endfor %} 53 | 54 |
    • 55 |
    56 | -------------------------------------------------------------------------------- /webpack.config.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import { Configuration } from 'webpack'; 3 | import * as nodeExternals from 'webpack-node-externals'; 4 | import * as merge from 'webpack-merge'; 5 | import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin'; 6 | import * as webpack from 'webpack'; 7 | 8 | const isDevelopment = process.env.NODE_ENV !== 'production'; 9 | const sourcePath = path.resolve(__dirname, 'src'); 10 | 11 | const config: Configuration = { 12 | mode: isDevelopment ? 'development' : 'production', 13 | entry: { 14 | SoundFont2: path.resolve(sourcePath, 'index.ts') 15 | }, 16 | output: { 17 | path: path.resolve(__dirname, 'lib'), 18 | library: 'SoundFont2' 19 | }, 20 | resolve: { 21 | extensions: ['.ts'], 22 | plugins: [new TsconfigPathsPlugin()] 23 | }, 24 | module: { 25 | rules: [ 26 | { 27 | test: /\.ts$/, 28 | use: [ 29 | { 30 | loader: 'babel-loader', 31 | options: { 32 | cacheDirectory: true, 33 | presets: ['@babel/preset-env', '@babel/preset-typescript'], 34 | plugins: ['@babel/plugin-proposal-class-properties'] 35 | } 36 | } 37 | ], 38 | exclude: /node_modules/ 39 | } 40 | ] 41 | }, 42 | devtool: 'inline-source-map' 43 | }; 44 | 45 | /** 46 | * Create a bundle for web browsers. 47 | */ 48 | const browser: Configuration = merge.smart(config, { 49 | target: 'web', 50 | output: { 51 | libraryTarget: 'umd', 52 | umdNamedDefine: true, 53 | filename: '[name].js' 54 | }, 55 | optimization: { 56 | minimize: true 57 | } 58 | }); 59 | 60 | /** 61 | * Create a bundle for Node.js. 62 | */ 63 | const node: Configuration = merge.smart(config, { 64 | target: 'node', 65 | externals: [nodeExternals()], 66 | output: { 67 | libraryTarget: 'commonjs2', 68 | filename: '[name].node.js' 69 | } 70 | }); 71 | 72 | export default [browser, node]; 73 | -------------------------------------------------------------------------------- /docs/api/soundfont2/README.md: -------------------------------------------------------------------------------- 1 | # SoundFont2 2 | 3 | ## Table of Contents 4 | 5 | * [`new SoundFont2(buffer)`](#new-soundfont2buffer) 6 | * [`SoundFont2.chunk`](#soundfont2chunk) 7 | * [`SoundFont2.metaData`](#soundfont2metadata) 8 | * [`SoundFont2.sampleData`](#soundfont2sampledata) 9 | * [`SoundFont2.samples`](#soundfont2samples) 10 | * [`SoundFont2.presetData`](#soundfont2presetdata) 11 | * [`SoundFont2.presets`](#soundfont2presets) 12 | * [`SoundFont2.instruments`](#soundfont2instruments) 13 | * [`SoundFont2.banks`](#soundfont2banks) 14 | * [`SoundFont2.getKeyData(keyNumber, bankNumber, presetNumber)`](#soundfont2getkeydatakeynumber-banknumber-presetnumber) 15 | 16 | ## `new SoundFont2(buffer)` 17 | 18 | * `buffer` [<Uint8Array>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) - The binary SoundFont2 data to parse. 19 | 20 | Create a new instance of of the `SoundFont2` class. This may throw a `ParseError` if the SoundFont2 is invalid. 21 | 22 | ### `SoundFont2.chunk` 23 | 24 | * [<SF2Chunk>](https://github.com/Mrtenz/soundfont2/blob/master/src/chunk.ts) - The raw, unparsed chunk data. 25 | 26 | ### `SoundFont2.metaData` 27 | 28 | * [<MetaData>](meta-data.md) - The parsed meta data. 29 | 30 | ### `SoundFont2.sampleData` 31 | 32 | * [<Uint8Array>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) - The full, unparsed sample data. 33 | 34 | This includes all the samples in the SoundFont as a single buffer. 35 | 36 | ### `SoundFont2.samples` 37 | 38 | * [<Sample[]>](sample/README.md) - An array of all samples with the sample headers. 39 | 40 | ### `SoundFont2.presetData` 41 | 42 | * [<PresetData>](preset-data.md) - The raw, unparsed preset data. 43 | 44 | ### `SoundFont2.presets` 45 | 46 | * [<Preset[]>](preset.md) - An array of all presets with the preset zones. 47 | 48 | ### `SoundFont2.instruments` 49 | 50 | * [<Instrument[]>](instrument.md) - An array of all instruments with the instrument zones. 51 | 52 | ### `SoundFont2.banks` 53 | 54 | * [<Bank[]>](bank.md) - An array of all MIDI banks with the presets. 55 | 56 | ### `SoundFont2.getKeyData(keyNumber, bankNumber, presetNumber)` 57 | 58 | * `keyNumber` [<number>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Number_type) - The MIDI key number. 59 | * `bankNumber` [<number>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Number_type) - The MIDI bank number. 60 | * `presetNumber` [<number>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Number_type) - The MIDI preset number. 61 | * Returns: [<Key>](key.md) 62 | 63 | The result of this function is [memoized](https://en.wikipedia.org/wiki/Memoization). 64 | -------------------------------------------------------------------------------- /src/types/metaData.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The meta data found in the INFO sub-chunk in the SoundFont file. 3 | */ 4 | export interface MetaData { 5 | /** 6 | * The SoundFont specification version level to which the file complies, found in the `ifil` 7 | * sub-chunk. This field is required, if it is not set in the input SoundFont file or it is not 8 | * exactly 4 bytes in length, the file should be rejected. 9 | */ 10 | version: string; 11 | 12 | /** 13 | * The sound engine for which the SoundFont was optimized, found in the `isng` sub-chunk. This 14 | * field is required, but if it's not set, a default of "EMU8000" can be assumed. This field is 15 | * case-sensitive. 16 | */ 17 | soundEngine: string; 18 | 19 | /** 20 | * The name of the SoundFont compatible bank, found in the `INAM` sub-chunk. This field is 21 | * required, if it is not set in the input SoundFont file, the file should be rejected. 22 | */ 23 | name: string; 24 | 25 | /** 26 | * A wavetable sound data ROM to which any ROM samples refer, found in the `irom` sub-chunk. This 27 | * field is optional. If the field is missing or references an unknown ROM, it should be ignored 28 | * and the file should be assumed to not reference ROM samples. 29 | */ 30 | rom?: string; 31 | 32 | /** 33 | * A wavetable sound data ROM revision to which any ROM samples refer, found in the `iver` 34 | * sub-chunk. This field is optional. If the field is set, but is not exactly 4 bytes in length, 35 | * it should be ignored and the file should be assumed to not reference ROM samples. 36 | */ 37 | romVersion?: string; 38 | 39 | /** 40 | * The creation date of the SoundFont file, found in the `ICRD` sub-chunk. This field is 41 | * optional. Conventionally, the format is 'Month Day, Year', but this is not a requirement. 42 | */ 43 | creationDate?: string; 44 | 45 | /** 46 | * The sound designers or engineers responsible for the SoundFont file, found in the `IENG` 47 | * sub-chunk. This field is optional. 48 | */ 49 | author?: string; 50 | 51 | /** 52 | * The product for which the SoundFont file is intended, found in the `IPDR` sub-chunk. This 53 | * field is optional and case-sensitive. 54 | */ 55 | product?: string; 56 | 57 | /** 58 | * Copyright assertion string associated with the SoundFont file, found in the `ICOP` sub-chunk. 59 | * This field is optional. 60 | */ 61 | copyright?: string; 62 | 63 | /** 64 | * Any comments associated with the SoundFont file, found in the `ICMT` sub-chunk. This field is 65 | * optional. 66 | */ 67 | comments?: string; 68 | 69 | /** 70 | * The SoundFont compatible tool, used to create the SoundFile or modify the SoundFont file, 71 | * sound in the `ISFT` sub-chunk. This field is optional. 72 | */ 73 | createdBy?: string; 74 | } 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "soundfont2", 3 | "version": "0.5.0", 4 | "description": "A SoundFont2 parser for Node.js and web browsers", 5 | "main": "lib/SoundFont2.node.js", 6 | "module": "lib/SoundFont2.js", 7 | "typings": "typings/index.d.ts", 8 | "keywords": [ 9 | "soundfont2", 10 | "soundfont", 11 | "sf2", 12 | "midi", 13 | "synthesizer" 14 | ], 15 | "author": "Maarten Zuidhoorn ", 16 | "repository": "https://github.com/Mrtenz/soundfont2.git", 17 | "license": "MIT", 18 | "scripts": { 19 | "tslint": "tslint --project .", 20 | "test": "jest", 21 | "prettier:diff": "prettier --write --config ./.prettierrc --list-different 'src/**/*.ts'", 22 | "clean": "rimraf ./lib ./typings", 23 | "build": "yarn run build:declarations && webpack", 24 | "build:declarations": "tsc --project tsconfig.json --declaration --declarationDir typings --emitDeclarationOnly", 25 | "prepublishOnly": "yarn run clean && cross-env NODE_ENV=development yarn run build", 26 | "docs:serve": "gitbook serve", 27 | "docs:build": "gitbook build", 28 | "docs:deploy": "yarn run docs:build && gh-pages -d _book" 29 | }, 30 | "files": [ 31 | "lib", 32 | "src", 33 | "typings" 34 | ], 35 | "devDependencies": { 36 | "@babel/core": "^7.2.2", 37 | "@babel/plugin-proposal-class-properties": "^7.2.3", 38 | "@babel/preset-env": "^7.2.3", 39 | "@babel/preset-typescript": "^7.1.0", 40 | "@types/jest": "^23.3.12", 41 | "@types/node": "^10.12.18", 42 | "@types/webpack": "^4.4.22", 43 | "@types/webpack-merge": "^4.1.3", 44 | "@types/webpack-node-externals": "^1.6.3", 45 | "awesome-typescript-loader": "^5.2.1", 46 | "babel-loader": "^8.0.5", 47 | "cross-env": "^5.2.0", 48 | "gh-pages": "^2.0.1", 49 | "gitbook-cli": "^2.3.2", 50 | "husky": "^1.3.1", 51 | "jest": "^23.6.0", 52 | "lint-staged": "^8.1.0", 53 | "prettier": "^1.15.3", 54 | "rimraf": "^2.6.3", 55 | "ts-jest": "^23.10.5", 56 | "ts-node": "^7.0.1", 57 | "tsconfig-paths-webpack-plugin": "^3.2.0", 58 | "tslint": "^5.12.0", 59 | "tslint-config-prettier": "^1.17.0", 60 | "tslint-microsoft-contrib": "^6.0.0", 61 | "typescript": "^3.2.2", 62 | "webpack": "^4.28.4", 63 | "webpack-cli": "^3.2.1", 64 | "webpack-merge": "^4.2.1", 65 | "webpack-node-externals": "^1.7.2" 66 | }, 67 | "lint-staged": { 68 | "*.ts": [ 69 | "prettier --write --config ./.prettierrc --config-precedence file-override", 70 | "git add" 71 | ] 72 | }, 73 | "husky": { 74 | "hooks": { 75 | "post-commit": "git update-index --again", 76 | "pre-commit": "yarn run tslint && yarn run test && lint-staged" 77 | } 78 | }, 79 | "packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610" 80 | } 81 | -------------------------------------------------------------------------------- /src/riff/parser.ts: -------------------------------------------------------------------------------- 1 | import { ParseError } from './parseError'; 2 | import { getStringFromBuffer } from '~/utils/buffer'; 3 | import { RIFFChunk } from './riffChunk'; 4 | 5 | /** 6 | * Attempts to parse a RIFF file from a raw buffer. 7 | * 8 | * @param {Uint8Array} buffer - The input buffer 9 | */ 10 | export const parseBuffer = (buffer: Uint8Array): RIFFChunk => { 11 | const id = getChunkId(buffer); 12 | if (id !== 'RIFF') { 13 | throw new ParseError('Invalid file format', 'RIFF', id); 14 | } 15 | 16 | const signature = getChunkId(buffer, 8); 17 | if (signature !== 'sfbk') { 18 | throw new ParseError('Invalid signature', 'sfbk', signature); 19 | } 20 | 21 | const newBuffer = buffer.subarray(8); 22 | const subChunks = getSubChunks(newBuffer.subarray(4)); 23 | return new RIFFChunk(id, newBuffer.length, newBuffer, subChunks); 24 | }; 25 | 26 | /** 27 | * Get a RIFF chunk from a buffer. 28 | * 29 | * @param {Buffer} buffer - The input buffer 30 | * @param {number} start - Where to start reading the buffer 31 | */ 32 | export const getChunk = (buffer: Uint8Array, start: number): RIFFChunk => { 33 | const id = getChunkId(buffer, start); 34 | const length = getChunkLength(buffer, start + 4); 35 | 36 | // RIFF and LIST chunks can have sub-chunks 37 | let subChunks: RIFFChunk[] = []; 38 | if (id === 'RIFF' || id === 'LIST') { 39 | subChunks = getSubChunks(buffer.subarray(start + 12)); 40 | } 41 | 42 | return new RIFFChunk(id, length, buffer.subarray(start + 8), subChunks); 43 | }; 44 | 45 | /** 46 | * Get the length of a chunk, based on the RIFF length specifier. 47 | * 48 | * @param {Buffer} buffer - The input buffer 49 | * @param {number} start - Where to start reading the buffer for the length 50 | */ 51 | export const getChunkLength = (buffer: Uint8Array, start: number) => { 52 | buffer = buffer.subarray(start, start + 4); 53 | 54 | return (buffer[0] | (buffer[1] << 8) | (buffer[2] << 16) | (buffer[3] << 24)) >>> 0; 55 | }; 56 | 57 | /** 58 | * Get all sub-chunks in a buffer. This will read until the end of the buffer and return any 59 | * sub-chunks found in it. 60 | * 61 | * @param {Buffer} buffer - The input buffer 62 | */ 63 | export const getSubChunks = (buffer: Uint8Array): RIFFChunk[] => { 64 | const chunks: RIFFChunk[] = []; 65 | let index = 0; 66 | 67 | while (index <= buffer.length - 8) { 68 | const subChunk = getChunk(buffer, index); 69 | chunks.push(subChunk); 70 | 71 | index += 8 + subChunk.length; 72 | index = index % 2 ? index + 1 : index; 73 | } 74 | 75 | return chunks; 76 | }; 77 | 78 | /** 79 | * Get the chunk ID (fourCC) from the buffer. This assumes the fourCC code is formatted as an UTF-8 80 | * string. 81 | * 82 | * @param {Buffer} buffer - The input buffer 83 | * @param {number} start - Where to start reading the chunk ID from the buffer 84 | */ 85 | export const getChunkId = (buffer: Uint8Array, start: number = 0) => { 86 | return getStringFromBuffer(buffer.subarray(start, start + 4)); 87 | }; 88 | -------------------------------------------------------------------------------- /src/types/sample.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * SoundFont2 samples are in the WAV format, meaning that they consist of a signed 16-bit array, 3 | * instead of a unsigned 8-bit array, which is read by default. The sample data in the `smpl` 4 | * sub-chunk is parsed as Int16Array before creating a sample. 5 | */ 6 | export type SampleData = Int16Array; 7 | 8 | /** 9 | * The sample type, found in the `type` field in the header. 10 | */ 11 | export enum SampleType { 12 | EOS = 0, 13 | Mono = 1, 14 | Right = 2, 15 | Left = 4, 16 | Linked = 8, 17 | RomMono = 32769, 18 | RomRight = 32770, 19 | RomLeft = 32772, 20 | RomLinked = 32776 21 | } 22 | 23 | export interface SampleHeader { 24 | /** 25 | * The name of the sample. This may be EOS, indicating end of samples, with all of the other 26 | * values set to zero. 27 | */ 28 | name: string; 29 | 30 | /** 31 | * The start of the sample in data points, from the beginning of the sample data field to the 32 | * first data point of the sample. 33 | */ 34 | start: number; 35 | 36 | /** 37 | * The end of the sample in data points, from the beginning of the sample data field to the first 38 | * set of 46 zero valued data points following this sample. 39 | */ 40 | end: number; 41 | 42 | /** 43 | * The index in sample data points, from the beginning of the sample data field to the first data 44 | * point in the loop of this sample. 45 | */ 46 | startLoop: number; 47 | 48 | /** 49 | * The index in sample data points, from the beginning of the sample data field to the first data 50 | * point following the loop of this sample. 51 | */ 52 | endLoop: number; 53 | 54 | /** 55 | * The sample rate in hertz, at which the sample was acquired or to which it was most recently 56 | * converted. The value should be between 400 and 50000 hertz, but this is not a strict 57 | * requirement. A value of zero is illegal. 58 | */ 59 | sampleRate: number; 60 | 61 | /** 62 | * The MIDI key number of the recorded pitch of the sample. For unpitched sounds, this should be 63 | * a value of 255. Values between 128 and 254 are illegal and a value of 60 should be used 64 | * instead. 65 | */ 66 | originalPitch: number; 67 | 68 | /** 69 | * The pitch correction in cents that should be applied to the sample on playback, to compensate 70 | * for any pitch errors during the sample recording. 71 | */ 72 | pitchCorrection: number; 73 | 74 | /** 75 | * The sample header index of the associated left or right sample, if the sample type is a left 76 | * or right type. Both samples should be played at the same time, with the pitch controlled by 77 | * the right sample's generators. 78 | */ 79 | link: number; 80 | 81 | /** 82 | * Indicates the type of sample. 83 | */ 84 | type: SampleType; 85 | } 86 | 87 | export interface Sample { 88 | /** 89 | * The sample header containing the meta data. 90 | */ 91 | header: SampleHeader; 92 | 93 | /** 94 | * The sample data parsed as Int16Array. 95 | */ 96 | data: SampleData; 97 | } 98 | -------------------------------------------------------------------------------- /src/chunks/generators.ts: -------------------------------------------------------------------------------- 1 | import { SF2Chunk } from '~/chunk'; 2 | import { ParseError } from '~/riff'; 3 | import { Generator, GeneratorType } from '~/types'; 4 | import { SF_GENERATOR_SIZE } from '~/constants'; 5 | 6 | /** 7 | * An array of GeneratorTypes that cannot be specified for presets. If one of these generator types 8 | * is found, the generator should be ignored. 9 | */ 10 | const PRESET_TYPES_BLACKLIST: number[] = [ 11 | GeneratorType.StartAddrsOffset, 12 | GeneratorType.EndAddrsOffset, 13 | GeneratorType.StartLoopAddrsOffset, 14 | GeneratorType.EndLoopAddrsOffset, 15 | GeneratorType.StartAddrsCoarseOffset, 16 | GeneratorType.EndAddrsCoarseOffset, 17 | GeneratorType.StartLoopAddrsCoarseOffset, 18 | GeneratorType.KeyNum, 19 | GeneratorType.Velocity, 20 | GeneratorType.EndLoopAddrsCoarseOffset, 21 | GeneratorType.SampleModes, 22 | GeneratorType.ExclusiveClass, 23 | GeneratorType.OverridingRootKey 24 | ]; 25 | 26 | /** 27 | * An array of GeneratorTypes that cannot be specified for instruments. If one of these generator 28 | * types is found, the generator should be ignored. 29 | */ 30 | const INSTRUMENT_TYPES_BLACKLIST: number[] = [ 31 | GeneratorType.Unused1, 32 | GeneratorType.Unused2, 33 | GeneratorType.Unused3, 34 | GeneratorType.Unused4, 35 | GeneratorType.Reserved1, 36 | GeneratorType.Reserved2, 37 | GeneratorType.Reserved3 38 | ]; 39 | 40 | /** 41 | * These GeneratorTypes specify a range of key numbers or velocity. 42 | */ 43 | const RANGE_TYPES: number[] = [GeneratorType.KeyRange, GeneratorType.VelRange]; 44 | 45 | /** 46 | * Get all generators for either an preset generator chunk or a instrument generator chunk. 47 | * 48 | * TODO: Check if generator chunk is valid, by following the rules defined in the spec. See for 49 | * example: https://github.com/FluidSynth/fluidsynth/blob/v2.0.3/src/sfloader/fluid_sffile.c 50 | * 51 | * @param {SF2Chunk} chunk - The input chunk 52 | * @param {string} type - The type, can be 'pgen' or 'igen' 53 | */ 54 | export const getGenerators = (chunk: SF2Chunk, type: 'pgen' | 'igen'): Generator[] => { 55 | if (chunk.id !== type) { 56 | throw new ParseError('Unexpected chunk ID', `'${type}'`, `'${chunk.id}'`); 57 | } 58 | 59 | if (chunk.length % SF_GENERATOR_SIZE) { 60 | throw new ParseError(`Invalid size for the '${type}' sub-chunk`); 61 | } 62 | 63 | return chunk.iterate(iterator => { 64 | const id = iterator.getInt16(); 65 | 66 | // Ignore invalid IDs 67 | if (!GeneratorType[id]) { 68 | return null; 69 | } 70 | 71 | if (type === 'pgen' && PRESET_TYPES_BLACKLIST.includes(id)) { 72 | return null; 73 | } 74 | 75 | if (type === 'igen' && INSTRUMENT_TYPES_BLACKLIST.includes(id)) { 76 | return null; 77 | } 78 | 79 | if (RANGE_TYPES.includes(id)) { 80 | return { 81 | id, 82 | range: { 83 | lo: iterator.getByte(), 84 | hi: iterator.getByte() 85 | } 86 | }; 87 | } 88 | 89 | return { 90 | id, 91 | value: iterator.getInt16BE() 92 | }; 93 | }); 94 | }; 95 | -------------------------------------------------------------------------------- /src/riff/chunkIterator.ts: -------------------------------------------------------------------------------- 1 | import { RIFFChunk } from './riffChunk'; 2 | import { getStringFromBuffer } from '~/utils'; 3 | 4 | /** 5 | * A utility class to quickly iterate over a buffer. 6 | */ 7 | export class ChunkIterator { 8 | public readonly target: T[] = []; 9 | private readonly chunk: RIFFChunk; 10 | private position: number = 0; 11 | 12 | public constructor(chunk: RIFFChunk, start: number = 0) { 13 | this.chunk = chunk; 14 | this.position = start; 15 | } 16 | 17 | /** 18 | * Get the position from the iterator. 19 | */ 20 | public get currentPosition(): number { 21 | return this.position; 22 | } 23 | 24 | /** 25 | * Iterate over the chunk. 26 | * 27 | * @param {Function} callback - The callback that is called every iteration 28 | */ 29 | public iterate(callback: (iterator: ChunkIterator) => T | null) { 30 | while (this.position < this.chunk.length) { 31 | const object = callback(this); 32 | if (object) { 33 | this.target.push(object); 34 | } 35 | } 36 | } 37 | 38 | /** 39 | * Get a string from the buffer. 40 | * 41 | * @param {number} length - The length of the string. If no length is specified, a default of 20 42 | * is assumed 43 | */ 44 | public getString(length: number = 20): string { 45 | const text = getStringFromBuffer(this.getBuffer(this.position, length)); 46 | this.position += length; 47 | return text; 48 | } 49 | 50 | /** 51 | * Get a signed 16-bit integer from the chunk. 52 | */ 53 | public getInt16(): number { 54 | return this.chunk.buffer[this.position++] | (this.chunk.buffer[this.position++] << 8); 55 | } 56 | 57 | /** 58 | * Get a signed 16-bit integer from the chunk in the big-endian format. 59 | */ 60 | public getInt16BE(): number { 61 | return (this.getInt16() << 16) >> 16; 62 | } 63 | 64 | /** 65 | * Get an unsigned 32-bit integer from the chunk. 66 | */ 67 | public getUInt32(): number { 68 | return ( 69 | (this.chunk.buffer[this.position++] | 70 | (this.chunk.buffer[this.position++] << 8) | 71 | (this.chunk.buffer[this.position++] << 16) | 72 | (this.chunk.buffer[this.position++] << 24)) >>> 73 | 0 74 | ); 75 | } 76 | 77 | /** 78 | * Get a single byte from the chunk. 79 | */ 80 | public getByte(): number { 81 | return this.chunk.buffer[this.position++]; 82 | } 83 | 84 | /** 85 | * Get a signed char from the chunk. 86 | */ 87 | public getChar(): number { 88 | return (this.chunk.buffer[this.position++] << 24) >> 24; 89 | } 90 | 91 | /** 92 | * Skip ahead in the buffer. 93 | * 94 | * @param {number} length 95 | */ 96 | public skip(length: number): void { 97 | this.position += length; 98 | } 99 | 100 | /** 101 | * Get a part of the buffer from start to start + length. 102 | * 103 | * @param {number} start 104 | * @param {number} length 105 | */ 106 | private getBuffer(start: number, length: number): Uint8Array { 107 | return this.chunk.buffer.subarray(start, start + length); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /docs/api/soundfont2/sample/sample-header.md: -------------------------------------------------------------------------------- 1 | # SampleHeader 2 | 3 | The `SampleHeader` object contains all the meta data for a sample. 4 | 5 | ## Table of Contents 6 | 7 | * [`SampleHeader.name`](#sampleheadername) 8 | * [`SampleHeader.start`](#sampleheaderstart) 9 | * [`SampleHeader.end`](#sampleheaderend) 10 | * [`SampleHeader.startLoop`](#sampleheaderstartloop) 11 | * [`SampleHeader.endLoop`](#sampleheaderendloop) 12 | * [`SampleHeader.sampleRate`](#sampleheadersamplerate) 13 | * [`SampleHeader.originalPitch`](#sampleheaderoriginalpitch) 14 | * [`SampleHeader.pitchCorrection`](#sampleheaderpitchcorrection) 15 | * [`SampleHeader.link`](#sampleheaderlink) 16 | * [`SampleHeader.type`](#sampleheadertype) 17 | 18 | ## `SampleHeader.name` 19 | 20 | * [<string>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type) - The name of the sample. 21 | 22 | This name should be unique. 23 | 24 | ## `SampleHeader.start` 25 | 26 | * [<number>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Number_type) - The start of the sample data in the `smpl` chunk. 27 | 28 | Note that the sample data in the [`Sample`](README.md) object is already the correct sample data. 29 | 30 | ## `SampleHeader.end` 31 | 32 | * [<number>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Number_type) - The end of the sample data in the `smpl` chunk. 33 | 34 | Note that the sample data in the [`Sample`](README.md) object is already the correct sample data. 35 | 36 | ## `SampleHeader.startLoop` 37 | 38 | * [<number>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Number_type) - The index in sample data points, where the sample should start the loop. 39 | 40 | The `startLoop` index is corrected by the `start` value. 41 | 42 | ## `SampleHeader.endLoop` 43 | 44 | * [<number>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Number_type) - The index in sample data points, where the sample should end the loop. 45 | 46 | The `endLoop` index is corrected by the `start` value. 47 | 48 | ## `SampleHeader.sampleRate` 49 | 50 | * [<number>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Number_type) - The sample rate in hertz. 51 | 52 | ## `SampleHeader.originalPitch` 53 | 54 | * [<number>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Number_type) - The MIDI key number of the recorded pitch of the sample. 55 | 56 | This is a number between 0 and 127, or 255 if the sample is unpitched. 57 | 58 | ## `SampleHeader.pitchCorrection` 59 | 60 | * [<number>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Number_type) - The pitch correction in cents that should be applied to the sample on playback. 61 | 62 | This is to compensate for any pitch errors during the recording of the sample. 63 | 64 | ## `SampleHeader.link` 65 | 66 | * [<number>](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Number_type) - The sample header index of the associated left or right sample. 67 | 68 | Both samples should be played at the same time, with the pitch being controlled by the right sample's generators. 69 | 70 | ## `SampleHeader.type` 71 | 72 | * [<SampleType>](sample-type.md) - The type of the sample. 73 | -------------------------------------------------------------------------------- /src/riff/riffChunk.ts: -------------------------------------------------------------------------------- 1 | import { ChunkIterator } from './chunkIterator'; 2 | import { getStringFromBuffer } from '~/utils'; 3 | 4 | export class RIFFChunk { 5 | /** 6 | * The chunk ID (fourCC). 7 | */ 8 | public readonly id: string; 9 | 10 | /** 11 | * The length of the chunk. 12 | */ 13 | public readonly length: number; 14 | 15 | /** 16 | * The raw buffer of the chunk. 17 | */ 18 | public readonly buffer: Uint8Array; 19 | 20 | /** 21 | * The sub-chunks of the chunk. If the chunk is not a RIFF or LIST chunk, this will be an empty 22 | * array. 23 | */ 24 | public readonly subChunks: RIFFChunk[]; 25 | 26 | public constructor(id: string, length: number, buffer: Uint8Array, subChunks: RIFFChunk[]) { 27 | this.id = id; 28 | this.length = length; 29 | this.buffer = buffer; 30 | this.subChunks = subChunks; 31 | } 32 | 33 | /** 34 | * Get a string from the buffer. If no position and no length is specified, it returns the whole 35 | * buffer as a string. 36 | * 37 | * @param {number} [position] 38 | * @param {number} [length] 39 | */ 40 | public getString(position: number = 0, length?: number): string { 41 | return getStringFromBuffer(this.getBuffer(position, length || this.length - position)); 42 | } 43 | 44 | /** 45 | * Get a signed 16-bit integer from the buffer. 46 | * 47 | * @param {number} [position] 48 | */ 49 | public getInt16(position: number = 0): number { 50 | return this.buffer[position++] | (this.buffer[position] << 8); 51 | } 52 | 53 | /** 54 | * Get an unsigned 32-bit integer from the buffer. 55 | * 56 | * @param {number} [position] 57 | */ 58 | public getUInt32(position: number = 0): number { 59 | return ( 60 | (this.buffer[position++] | 61 | (this.buffer[position++] << 8) | 62 | (this.buffer[position++] << 16) | 63 | (this.buffer[position] << 24)) >>> 64 | 0 65 | ); 66 | } 67 | 68 | /** 69 | * Get a byte from the buffer. 70 | * 71 | * @param {number} [position] 72 | */ 73 | public getByte(position: number = 0): number { 74 | return this.buffer[position]; 75 | } 76 | 77 | /** 78 | * Get a char from the buffer. 79 | * 80 | * @param {number} [position] 81 | */ 82 | public getChar(position: number = 0): number { 83 | return (this.buffer[position] << 24) >> 24; 84 | } 85 | 86 | /** 87 | * Get a chunk iterator for the chunk. 88 | * 89 | * @param {number} [start] - The position where to start iterating. Defaults to 0. 90 | */ 91 | public iterator(start: number = 0): ChunkIterator { 92 | return new ChunkIterator(this, start); 93 | } 94 | 95 | /** 96 | * Utility function to quickly iterate over a function. 97 | * 98 | * @template T 99 | * @param {(iterator: ChunkIterator): T} callback - The callback that returns an instance of the 100 | * specified return type 101 | * @param {number} [start] - The optional index where to start iterating over the chunk 102 | */ 103 | public iterate(callback: (iterator: ChunkIterator) => T | null, start: number = 0): T[] { 104 | const iterator = new ChunkIterator(this, start); 105 | iterator.iterate(callback); 106 | return iterator.target; 107 | } 108 | 109 | /** 110 | * Get a buffer from `start` to `start` + `length`. The buffer is not copied (e.g. when using 111 | * .slice()), so any modifications to the buffer are done to the original buffer too. 112 | * 113 | * @param {number} start 114 | * @param {number} length 115 | */ 116 | private getBuffer(start: number, length: number): Uint8Array { 117 | return this.buffer.subarray(start, start + length); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/chunk.ts: -------------------------------------------------------------------------------- 1 | import { ParseError, RIFFChunk } from './riff'; 2 | import { MetaData, PresetData } from './types'; 3 | import { SF_INFO_CHUNKS, SF_VERSION_LENGTH } from './constants'; 4 | import { 5 | getGenerators, 6 | getInstrumentHeaders, 7 | getModulators, 8 | getPresetHeaders, 9 | getSampleHeaders, 10 | getZones 11 | } from './chunks'; 12 | 13 | export class SF2Chunk extends RIFFChunk { 14 | /** 15 | * All sub-chunks of this `SF2Chunk` as `SF2Chunk`. 16 | */ 17 | public readonly subChunks: SF2Chunk[]; 18 | 19 | public constructor(chunk: RIFFChunk) { 20 | super(chunk.id, chunk.length, chunk.buffer, chunk.subChunks); 21 | 22 | this.subChunks = chunk.subChunks.map(subChunk => new SF2Chunk(subChunk)); 23 | } 24 | 25 | /** 26 | * Get meta data from the chunk. This assumes the chunk is a LIST chunk, containing INFO 27 | * sub-chunks. 28 | */ 29 | public getMetaData(): MetaData { 30 | if (this.id !== 'LIST') { 31 | throw new ParseError('Unexpected chunk ID', `'LIST'`, `'${this.id}'`); 32 | } 33 | 34 | const info = this.subChunks.reduce<{ [key in SF_INFO_CHUNKS]?: string }>((target, chunk) => { 35 | if (chunk.id === 'ifil' || chunk.id === 'iver') { 36 | // ifil and iver length must be 4 bytes 37 | if (chunk.length !== SF_VERSION_LENGTH) { 38 | throw new ParseError(`Invalid size for the '${chunk.id}' sub-chunk`); 39 | } 40 | target[chunk.id as SF_INFO_CHUNKS] = `${chunk.getInt16()}.${chunk.getInt16(2)}`; 41 | } else { 42 | target[chunk.id as SF_INFO_CHUNKS] = chunk.getString(); 43 | } 44 | 45 | return target; 46 | }, {}); 47 | 48 | if (!info.ifil) { 49 | throw new ParseError(`Missing required 'ifil' sub-chunk`); 50 | } 51 | 52 | if (!info.INAM) { 53 | throw new ParseError(`Missing required 'INAM' sub-chunk`); 54 | } 55 | 56 | return { 57 | version: info.ifil, 58 | soundEngine: info.isng || 'EMU8000', 59 | name: info.INAM, 60 | rom: info.irom, 61 | romVersion: info.iver, 62 | creationDate: info.ICRD, 63 | author: info.IENG, 64 | product: info.IPRD, 65 | copyright: info.ICOP, 66 | comments: info.ICMT, 67 | createdBy: info.ISFT 68 | }; 69 | } 70 | 71 | /** 72 | * Get the sample data as a unsigned 8-bit buffer from the chunk. This assumes the chunk is a 73 | * LIST chunk containing a smpl sub-chunk. 74 | */ 75 | public getSampleData(): Uint8Array { 76 | if (this.id !== 'LIST') { 77 | throw new ParseError('Unexpected chunk ID', `'LIST'`, `'${this.id}'`); 78 | } 79 | 80 | const sampleChunk = this.subChunks[0]; 81 | if (sampleChunk.id !== 'smpl') { 82 | throw new ParseError('Invalid chunk signature', `'smpl'`, `'${sampleChunk.id}'`); 83 | } 84 | 85 | return sampleChunk.buffer; 86 | } 87 | 88 | /** 89 | * Get the preset data from the chunk. This assumes the chunk is a LIST chunk containing the 90 | * preset data sub-chunks. 91 | */ 92 | public getPresetData(): PresetData { 93 | if (this.id !== 'LIST') { 94 | throw new ParseError('Unexpected chunk ID', `'LIST'`, `'${this.id}'`); 95 | } 96 | 97 | return { 98 | presetHeaders: getPresetHeaders(this.subChunks[0]), 99 | presetZones: getZones(this.subChunks[1], 'pbag'), 100 | presetModulators: getModulators(this.subChunks[2], 'pmod'), 101 | presetGenerators: getGenerators(this.subChunks[3], 'pgen'), 102 | instrumentHeaders: getInstrumentHeaders(this.subChunks[4]), 103 | instrumentZones: getZones(this.subChunks[5], 'ibag'), 104 | instrumentModulators: getModulators(this.subChunks[6], 'imod'), 105 | instrumentGenerators: getGenerators(this.subChunks[7], 'igen'), 106 | sampleHeaders: getSampleHeaders(this.subChunks[8]) 107 | }; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /docs/getting-started/soundfont2-structure.md: -------------------------------------------------------------------------------- 1 | # SoundFont2 Structure 2 | 3 | This section explains a bit about the general SoundFont2 structure. It's not necessarily required to know this information in order to use the library, but it may make it easier to understand how the library and SoundFonts work. 4 | 5 | ## Table of Contents 6 | 7 | * [General Structure](#general-structure) 8 | * [RIFF Chunks](#riff-chunks) 9 | * [`INFO` Chunk](#info-chunk) 10 | * [`smpl` Chunk](#smpl-chunk) 11 | * [`pdta` Chunk](#pdta-chunk) 12 | * [Preset Data](#preset-data) 13 | * [Instrument Data](#instrument-data) 14 | * [Sample Data](#sample-data) 15 | * [Presets](#presets) 16 | * [Instruments](#instruments) 17 | 18 | ## General Structure 19 | 20 | A SoundFont has one or more presets, which can normally be selected with the `MIDI Program Change` command. The presets have one or more preset zones, that have one instrument. The instruments have instrument zones and these have one sample each. 21 | 22 | Multiple preset zones can refer to the same instrument and multiple instrument zones can refer to the same samples. 23 | 24 | ![General Structure](https://i.imgur.com/c2Gud3u.png) 25 | 26 | ## RIFF Chunks 27 | 28 | A SoundFont2 file is a RIFF (Resource Interchange File Format) file, which contains multiple chunks. Those chunks are identified by a [FourCC (four-character code)](https://en.wikipedia.org/wiki/FourCC). If an identifier is not for characters long, it will be padded by spaces. For example, "foo" would be read as "foo ". 29 | 30 | Every SoundFont2 file should have at least three top-level sub-chunks: an `INFO` chunk containing metadata, a `sdta` chunk containing Wave Audio (WAV) samples and a `pdta` header containing the preset, instrument and sample headers. 31 | 32 | All the raw (unparsed) data is available in the `SoundFont2` class, as `metaData`, `sampleData` and `presetData`. 33 | 34 | ![SoundFont2 RIFF chunks](https://i.imgur.com/BL8FvcC.png) 35 | 36 | ### `INFO` Chunk 37 | 38 | The `INFO` chunk contains at least the following sub-chunks. 39 | 40 | * `ifil` - The SoundFont specification version. 41 | 42 | Available as `metaData.version`. 43 | 44 | * `isng` - The sound engine for which the SoundFont was optimized. 45 | 46 | Available as `metaData.soundEngine`. 47 | 48 | * `INAM` - The name of the SoundFont. 49 | 50 | Available as `metaData.name`. 51 | 52 | The other sub-chunks are **optional**. 53 | 54 | * `irom` - A sound data ROM to which any ROM samples refer. 55 | 56 | Available as `metaData.rom`. 57 | 58 | * `iver` - A sound data ROM revision to which any ROM samples refer. 59 | 60 | Available as `metaData.romVersion`. 61 | 62 | * `ICRD` - The creation date of the SoundFont, conventionally in the 'Month Day, Year' format. 63 | 64 | Available as `metaData.creationDate`. 65 | 66 | * `IENG` - The author or authors of the SoundFont. 67 | 68 | Available as `metaData.author`. 69 | 70 | * `IPDR` - The product for which the SoundFont is intended. 71 | 72 | Available as `metaData.product`. 73 | 74 | * `ICOP` - Copyright assertion string associated with the SoundFont. 75 | 76 | Available as `metaData.copyright`. 77 | 78 | * `ICMT` - Any comments associated with the SoundFont. 79 | 80 | Available as `metaData.comments`. 81 | 82 | * `ISFT` - The tool used to create the SoundFont. 83 | 84 | Available as `metaData.createdBy`. 85 | 86 | ### `sdta` Chunk 87 | 88 | The sample chunk has one or two sub-chunks. 89 | 90 | * `smpl` - The 16-bit WAV sample data. 91 | 92 | Available as `sampleData`. 93 | 94 | * `sm24` - The 8-bit WAV sample data, in addition to the 16-bit data. 95 | 96 | Currently not available in the API. 97 | 98 | The `sdta` and `sm24` sub-chunks can be combined to get 24-bit WAV sample data, but the library currently does not support this yet. 99 | 100 | ### `pdta` Chunk 101 | 102 | The `pdta` sub-chunk contains data for presets, instruments and samples in the SoundFont. There are four preset and four instrument chunks, which form the instruments and presets. There is just one sample header chunk, that forms the samples together with the data from the `sdta` chunk. 103 | 104 | #### Preset Data 105 | 106 | The `pdta` has four sub-chunks that form the preset data. 107 | 108 | * `phdr` - The preset headers. 109 | 110 | Available as `presetData.presetHeaders`. 111 | 112 | * `pbag` - The preset zone indices. 113 | 114 | Available as `presetData.presetZones`. 115 | 116 | * `pmod` - The preset modulators. 117 | 118 | Available as `presetData.presetModulators` 119 | 120 | * `pgen` - The preset generators. 121 | 122 | Available as `presetData.presetGenerators` 123 | 124 | #### Instrument Data 125 | 126 | The `pdta` has four sub-chunks that form the instrument data. 127 | 128 | * `inst` - The instrument headers. 129 | 130 | Available as `presetData.instrumentHeaders`. 131 | 132 | * `ibag` - The instrument zone indices. 133 | 134 | Available as `presetData.instrumentZones`. 135 | 136 | * `imod` - The instrument modulators. 137 | 138 | Available as `presetData.instrumentModulators` 139 | 140 | * `igen` - The instrument generators. 141 | 142 | Available as `presetData.instrumentGenerators` 143 | 144 | #### Sample Data 145 | 146 | The `pdta` has one sub-chunk with the sample header data. 147 | 148 | * `shdr` - The sample headers. 149 | 150 | Available as `presetData.sampleHeaders` 151 | 152 | ## Presets 153 | 154 | TODO 155 | 156 | ## Instruments 157 | 158 | TODO 159 | -------------------------------------------------------------------------------- /src/chunks/zones.ts: -------------------------------------------------------------------------------- 1 | import { SF2Chunk } from '~/chunk'; 2 | import { ParseError } from '~/riff'; 3 | import { SF_BAG_SIZE } from '~/constants'; 4 | import { 5 | Generator, 6 | GeneratorType, 7 | Modulator, 8 | Zone, 9 | ZoneItems, 10 | ZoneItemsWithReference, 11 | ZoneMap 12 | } from '~/types'; 13 | 14 | /** 15 | * Get the preset or instrument zones from a chunk. 16 | * 17 | * @param {SF2Chunk} chunk - The input chunk 18 | * @param {string} type - The type of chunk ('pbag' or 'ibag') 19 | */ 20 | export const getZones = (chunk: SF2Chunk, type: 'pbag' | 'ibag'): Zone[] => { 21 | if (chunk.id !== type) { 22 | throw new ParseError('Unexpected chunk ID', `'${type}'`, `'${chunk.id}'`); 23 | } 24 | 25 | if (chunk.length % SF_BAG_SIZE) { 26 | throw new ParseError(`Invalid size for the '${type}' sub-chunk`); 27 | } 28 | 29 | return chunk.iterate(iterator => ({ 30 | generatorIndex: iterator.getInt16(), 31 | modulatorIndex: iterator.getInt16() 32 | })); 33 | }; 34 | 35 | /** 36 | * Get all modulators, generators and the instrument (for presets) or sample (for instruments) in a 37 | * preset or instrument. 38 | * 39 | * @template T 40 | * @template R 41 | * @param {T} headers - The preset or instrument headers 42 | * @param {Zone[]} zones - All zones for the preset or instrument 43 | * @param {Modulator[]} itemModulators - All modulators for the preset or instrument 44 | * @param {Generator[]} itemGenerators - All generators for the preset or instrument 45 | * @param {R[]} references - The instruments or samples to reference in the zone 46 | * @param {GeneratorType} referenceType - The generator type to reference it by 47 | */ 48 | export const getItemsInZone = ( 49 | headers: T[], 50 | zones: Zone[], 51 | itemModulators: Modulator[], 52 | itemGenerators: Generator[], 53 | references: R[], 54 | referenceType: GeneratorType 55 | ): { header: T; zones: ZoneItemsWithReference[]; globalZone?: ZoneItems }[] => { 56 | const items: { header: T; zones: ZoneItemsWithReference[]; globalZone?: ZoneItems }[] = []; 57 | 58 | for (let i = 0; i < headers.length; i++) { 59 | const header = headers[i]; 60 | const next = headers[i + 1]; 61 | 62 | const start = header.bagIndex; 63 | const end = next ? next.bagIndex : zones.length; 64 | 65 | const zoneItems: ZoneItemsWithReference[] = []; 66 | let globalZone; 67 | for (let j = start; j < end; j++) { 68 | const modulators = getModulators(j, zones, itemModulators); 69 | const generators = getGenerators(j, zones, itemGenerators); 70 | 71 | const keyRange = 72 | generators[GeneratorType.KeyRange] && generators[GeneratorType.KeyRange]!.range; 73 | const referenceId = generators[referenceType]; 74 | if (!referenceId) { 75 | if (j - start === 0) { 76 | // first item without reference = global zone 77 | // Spec 7.3: If a preset has more than one zone, the first zone may be a global zone. 78 | // A global zone is determined by the fact that the last generator in the list is not an Instrument generator. 79 | // Spec 7.9: "Unless the zone is a global zone, the last generator in the list is a “sampleID” generator" 80 | globalZone = { 81 | keyRange, 82 | modulators, 83 | generators 84 | }; 85 | } 86 | continue; 87 | } 88 | 89 | const reference = references[referenceId.value!]; 90 | if (!reference) { 91 | continue; 92 | } 93 | 94 | zoneItems.push({ 95 | keyRange, 96 | modulators, 97 | generators, 98 | reference 99 | }); 100 | } 101 | 102 | items.push({ 103 | header, 104 | globalZone, 105 | zones: zoneItems 106 | }); 107 | } 108 | 109 | return items; 110 | }; 111 | 112 | /** 113 | * Get all modulators from a zone, based on the index. The end index is the modulator index of the 114 | * next zone, or the total zone length if the current zone is the last one. 115 | * 116 | * @param {number} index - The index 117 | * @param {Zone[]} zones - ALl zones for the preset or instrument 118 | * @param {Modulator[]} modulators - All modulators for the preset or instrument 119 | */ 120 | const getModulators = ( 121 | index: number, 122 | zones: Zone[], 123 | modulators: Modulator[] 124 | ): ZoneMap => { 125 | const zone = zones[index]; 126 | const next = zones[index + 1]; 127 | 128 | const start = zone.modulatorIndex; 129 | const end = next ? next.modulatorIndex : zones.length; 130 | 131 | return getZone(start, end, modulators); 132 | }; 133 | 134 | /** 135 | * Get all generators from a zone, based on the index. The end index is the generators index of the 136 | * next zone, or the total zone length if the current zone is the last one. 137 | * 138 | * @param {number} index - The index 139 | * @param {Zone[]} zones - ALl zones for the preset or instrument 140 | * @param {Generator[]} generators - All generators for the preset or instrument 141 | */ 142 | const getGenerators = ( 143 | index: number, 144 | zones: Zone[], 145 | generators: Generator[] 146 | ): ZoneMap => { 147 | const zone = zones[index]; 148 | const next = zones[index + 1]; 149 | 150 | const start = zone.generatorIndex; 151 | const end = next ? next.generatorIndex : zones.length; 152 | 153 | return getZone(start, end, generators); 154 | }; 155 | 156 | /** 157 | * Returns all modulators or generators as a key-value object, where the key is the `GeneratorType` 158 | * of the modulator or generator. 159 | * 160 | * @template T 161 | * @param {number} start - The start index 162 | * @param {number} end - The end index 163 | * @param {T[]} items - The modulators or generators 164 | */ 165 | const getZone = ( 166 | start: number, 167 | end: number, 168 | items: T[] 169 | ): { [key in GeneratorType]?: T } => { 170 | const itemsObject: ZoneMap = {}; 171 | 172 | for (let i = start; i < end; i++) { 173 | const item = items[i]; 174 | if (item) { 175 | itemsObject[item.id] = item; 176 | } 177 | } 178 | 179 | return itemsObject; 180 | }; 181 | -------------------------------------------------------------------------------- /src/soundFont2.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Bank, 3 | GeneratorType, 4 | Instrument, 5 | Key, 6 | MetaData, 7 | Preset, 8 | PresetData, 9 | Sample, 10 | ZoneItems 11 | } from './types'; 12 | import { SF2Chunk } from './chunk'; 13 | import { parseBuffer, ParseError } from './riff'; 14 | import { getItemsInZone } from './chunks'; 15 | import { memoize } from './utils'; 16 | 17 | export class SoundFont2 { 18 | /** 19 | * Create a new `SoundFont2` instance from a raw input buffer. 20 | * 21 | * @param {Uint8Array} buffer 22 | * @deprecated Replaced with `new SoundFont2(buffer: Uint8Array);` 23 | */ 24 | public static from(buffer: Uint8Array): SoundFont2 { 25 | return new SoundFont2(buffer); 26 | } 27 | 28 | /** 29 | * The raw RIFF chunk data. 30 | */ 31 | public readonly chunk: SF2Chunk; 32 | 33 | /** 34 | * The meta data. 35 | */ 36 | public readonly metaData: MetaData; 37 | 38 | /** 39 | * The raw sample data. 40 | */ 41 | public readonly sampleData: Uint8Array; 42 | 43 | /** 44 | * The parsed samples. 45 | */ 46 | public readonly samples: Sample[]; 47 | 48 | /** 49 | * The unparsed preset data. 50 | */ 51 | public readonly presetData: PresetData; 52 | 53 | /** 54 | * The parsed instuments. 55 | */ 56 | public readonly instruments: Instrument[]; 57 | 58 | /** 59 | * The parsed presets. 60 | */ 61 | public readonly presets: Preset[]; 62 | 63 | /** 64 | * The parsed banks. 65 | */ 66 | public readonly banks: Bank[]; 67 | 68 | /** 69 | * Load a SoundFont2 file from a `Uint8Array` or a `SF2Chunk`. The recommended way is to use a 70 | * Uint8Array, loading a SoundFont2 from a `SF2Chunk` only exists for backwards compatibility and 71 | * will likely be removed in a future version. 72 | * 73 | * @param {Uint8Array|SF2Chunk} chunk 74 | */ 75 | public constructor(chunk: Uint8Array | SF2Chunk) { 76 | if (!(chunk instanceof SF2Chunk)) { 77 | const parsedBuffer = parseBuffer(chunk); 78 | chunk = new SF2Chunk(parsedBuffer); 79 | } 80 | 81 | if (chunk.subChunks.length !== 3) { 82 | throw new ParseError( 83 | 'Invalid sfbk structure', 84 | '3 chunks', 85 | `${chunk.subChunks.length} chunks` 86 | ); 87 | } 88 | 89 | this.chunk = chunk; 90 | this.metaData = chunk.subChunks[0].getMetaData(); 91 | this.sampleData = chunk.subChunks[1].getSampleData(); 92 | this.presetData = chunk.subChunks[2].getPresetData(); 93 | 94 | this.samples = this.getSamples(); 95 | this.instruments = this.getInstruments(); 96 | this.presets = this.getPresets(); 97 | this.banks = this.getBanks(); 98 | } 99 | 100 | /** 101 | * Get the key data by MIDI bank, preset and key number. May return null if no instrument was 102 | * found for the given inputs. Note that this does not process any of the generators that are 103 | * specific to the key number. 104 | * 105 | * The result is memoized based on all arguments, to prevent having to check all presets, 106 | * instruments etc. every time. 107 | * 108 | * @param {number} memoizedKeyNumber - The MIDI key number 109 | * @param {number} [memoizedBankNumber] - The bank index number, defaults to 0 110 | * @param {number} [memoizedPresetNumber] - The preset number, defaults to 0 111 | */ 112 | public getKeyData( 113 | memoizedKeyNumber: number, 114 | memoizedBankNumber: number = 0, 115 | memoizedPresetNumber: number = 0 116 | ): Key | null { 117 | // Get a memoized version of the function 118 | return memoize((keyNumber: number, bankNumber: number, presetNumber: number): Key | null => { 119 | const bank = this.banks[bankNumber]; 120 | if (bank) { 121 | const preset = bank.presets[presetNumber]; 122 | if (preset) { 123 | const presetZones = preset.zones.filter(zone => this.isKeyInRange(zone, keyNumber)); 124 | if (presetZones.length > 0) { 125 | for (const presetZone of presetZones) { 126 | const instrument = presetZone.instrument; 127 | const instrumentZones = instrument.zones.filter(zone => 128 | this.isKeyInRange(zone, keyNumber) 129 | ); 130 | if (instrumentZones.length > 0) { 131 | for (const instrumentZone of instrumentZones) { 132 | const sample = instrumentZone.sample; 133 | const generators = { ...presetZone.generators, ...instrumentZone.generators }; 134 | const modulators = { ...presetZone.modulators, ...instrumentZone.modulators }; 135 | 136 | return { 137 | keyNumber, 138 | preset, 139 | instrument, 140 | sample, 141 | generators, 142 | modulators 143 | }; 144 | } 145 | } 146 | } 147 | } 148 | } 149 | } 150 | 151 | return null; 152 | })(memoizedKeyNumber, memoizedBankNumber, memoizedPresetNumber); 153 | } 154 | 155 | /** 156 | * Checks if a MIDI key number is in the range of a zone. 157 | * 158 | * @param {ZoneItems} zone - The zone to check 159 | * @param {number} keyNumber - The MIDI key number, must be between 0 and 127 160 | */ 161 | private isKeyInRange(zone: ZoneItems, keyNumber: number): boolean { 162 | return ( 163 | zone.keyRange === undefined || 164 | (zone.keyRange.lo <= keyNumber && zone.keyRange.hi >= keyNumber) 165 | ); 166 | } 167 | 168 | /** 169 | * Parse the presets to banks. 170 | */ 171 | private getBanks(): Bank[] { 172 | return this.presets.reduce((target, preset) => { 173 | const bankNumber = preset.header.bank; 174 | 175 | if (!target[bankNumber]) { 176 | target[bankNumber] = { 177 | presets: [] 178 | }; 179 | } 180 | 181 | target[bankNumber].presets[preset.header.preset] = preset; 182 | return target; 183 | }, []); 184 | } 185 | 186 | /** 187 | * Parse the raw preset data to presets. 188 | */ 189 | private getPresets(): Preset[] { 190 | const { presetHeaders, presetZones, presetGenerators, presetModulators } = this.presetData; 191 | 192 | const presets = getItemsInZone( 193 | presetHeaders, 194 | presetZones, 195 | presetModulators, 196 | presetGenerators, 197 | this.instruments, 198 | GeneratorType.Instrument 199 | ); 200 | 201 | return presets 202 | .filter(preset => preset.header.name !== 'EOP') 203 | .map(preset => { 204 | return { 205 | header: preset.header, 206 | globalZone: preset.globalZone, 207 | zones: preset.zones.map(zone => { 208 | return { 209 | keyRange: zone.keyRange, 210 | generators: zone.generators, 211 | modulators: zone.modulators, 212 | instrument: zone.reference 213 | }; 214 | }) 215 | }; 216 | }); 217 | } 218 | 219 | /** 220 | * Parse the raw instrument data (found in the preset data) to instruments. 221 | */ 222 | private getInstruments(): Instrument[] { 223 | const { 224 | instrumentHeaders, 225 | instrumentZones, 226 | instrumentModulators, 227 | instrumentGenerators 228 | } = this.presetData; 229 | 230 | const instruments = getItemsInZone( 231 | instrumentHeaders, 232 | instrumentZones, 233 | instrumentModulators, 234 | instrumentGenerators, 235 | this.samples, 236 | GeneratorType.SampleId 237 | ); 238 | 239 | return instruments 240 | .filter(instrument => instrument.header.name !== 'EOI') 241 | .map(instrument => { 242 | return { 243 | header: instrument.header, 244 | globalZone: instrument.globalZone, 245 | zones: instrument.zones.map(zone => { 246 | return { 247 | keyRange: zone.keyRange, 248 | generators: zone.generators, 249 | modulators: zone.modulators, 250 | sample: zone.reference 251 | }; 252 | }) 253 | }; 254 | }); 255 | } 256 | 257 | /** 258 | * Parse the raw sample data and sample headers to samples. 259 | */ 260 | private getSamples(): Sample[] { 261 | return this.presetData.sampleHeaders 262 | .filter(sample => sample.name !== 'EOS') 263 | .map(header => { 264 | // Sample rate must be above 0 265 | if (header.name !== 'EOS' && header.sampleRate <= 0) { 266 | throw new Error( 267 | `Illegal sample rate of ${header.sampleRate} hz in sample '${header.name}'` 268 | ); 269 | } 270 | 271 | // Original pitch cannot be between 128 and 254 272 | if (header.originalPitch >= 128 && header.originalPitch <= 254) { 273 | header.originalPitch = 60; 274 | } 275 | 276 | header.startLoop -= header.start; 277 | header.endLoop -= header.start; 278 | 279 | // Turns the Uint8Array into a Int16Array 280 | const data = new Int16Array( 281 | new Uint8Array(this.sampleData.subarray(header.start * 2, header.end * 2)).buffer 282 | ); 283 | 284 | return { 285 | header, 286 | data 287 | }; 288 | }); 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /src/types/modulator.ts: -------------------------------------------------------------------------------- 1 | import { GeneratorType } from './generator'; 2 | 3 | export enum ControllerType { 4 | /** 5 | * The controller moves linearly from the minimum to the maximum value, with the direction and 6 | * polarity specified by the modulator. 7 | */ 8 | Linear = 0, 9 | 10 | /** 11 | * The controller moves in a concave fashion from the minimum to the maximum value, with the 12 | * direction and polarity specified by the modulator. 13 | * 14 | * `output = Math.log(Math.sqrt(value ** 2) / [max value] ** 2)` 15 | */ 16 | Concave = 1, 17 | 18 | /** 19 | * The controller moves in a convex fashion from the minimum to the maximum value, with the 20 | * direction and polarity specified by the modulator. This is the same as the concave curve, but 21 | * with the start and end points reversed. 22 | */ 23 | Convex = 2, 24 | 25 | /** 26 | * The controller output is at a minimum value while the controller input moves from the minimum 27 | * to half of the maximum, after which the controller output is at a maximum. The direction and 28 | * polarity are specified by the modulator. 29 | */ 30 | Switch = 3 31 | } 32 | 33 | export enum ControllerPolarity { 34 | /** 35 | * The controller should be mapped with a minimum value of 0 and a maximum value of 1. It behaves 36 | * similar to the modulation wheel controller of the MIDI specification. 37 | */ 38 | Unipolar = 0, 39 | 40 | /** 41 | * The controller should be mapped with a minimum value of -1 and a maximum value of 1. It 42 | * behaves similar to the pitch wheel controller of the MIDI specification. 43 | */ 44 | Bipolar = 1 45 | } 46 | 47 | export enum ControllerDirection { 48 | /** 49 | * The direction of the controller should be from the minimum to the maximum value. 50 | */ 51 | Increasing = 0, 52 | 53 | /** 54 | * The direction of the controller should be from the maximum to the minimum value. 55 | */ 56 | Decreasing = 1 57 | } 58 | 59 | export enum ControllerPalette { 60 | /** 61 | * Use the general controller palette as described by the `Controller` enum. 62 | */ 63 | GeneralController = 0, 64 | 65 | /** 66 | * Use the MIDI controller palette. 67 | */ 68 | MidiController = 1 69 | } 70 | 71 | export enum Controller { 72 | /** 73 | * No controller is to be used. 74 | */ 75 | NoController = 0, 76 | 77 | /** 78 | * The controller source to be used is the velocity value which is sent from the MIDI note-on 79 | * command. 80 | */ 81 | NoteOnVelocity = 2, 82 | 83 | /** 84 | * The controller source to be used is the key number value which was sent from the MIDI note-on 85 | * command. 86 | */ 87 | NoteOnKeyNumber = 3, 88 | 89 | /** 90 | * The controller source to be used is the poly pressure amount that is sent from the MIDI 91 | * poly-pressure command. 92 | */ 93 | PolyPressure = 10, 94 | 95 | /** 96 | * The controller source to be used is the channel pressure amount that is sent from the MIDI 97 | * channel-pressure command. 98 | */ 99 | ChannelPressure = 13, 100 | 101 | /** 102 | * The controller source to be used is the pitch wheel amount which is sent from the MIDI pitch 103 | * wheel command. 104 | */ 105 | PitchWheel = 14, 106 | 107 | /** 108 | * The controller source to be used is the pitch wheel sensitivity amount which is sent from the 109 | * MIDI RPN 0 pitch wheel sensitivity command. 110 | */ 111 | PitchWheelSensitivity = 16, 112 | 113 | /** 114 | * The controller source is the output of another modulator. This is only supported as `value`, 115 | * not as `valueSource`. 116 | */ 117 | Link = 127 118 | } 119 | 120 | export interface ControllerValue { 121 | /** 122 | * The type of modulator. 123 | */ 124 | type: ControllerType; 125 | 126 | /** 127 | * The polarity of the modulator. 128 | */ 129 | polarity: ControllerPolarity; 130 | 131 | /** 132 | * The direction of the modulator. 133 | */ 134 | direction: ControllerDirection; 135 | 136 | /** 137 | * The controller palette used for the modulator. 138 | */ 139 | palette: ControllerPalette; 140 | 141 | /** 142 | * The index of the general or MIDI controller. If the palette is set to `GeneralController`, 143 | * this refers to a type in the `Controller` type. Otherwise, its a MIDI continuous controller. 144 | */ 145 | index: Controller | number; 146 | } 147 | 148 | export enum TransformType { 149 | /** 150 | * The output value of the multiplier is fed directly to the summing node of the given 151 | * destination. 152 | */ 153 | Linear = 0, 154 | 155 | /** 156 | * The output value of the multiplier is to be the absolute value of the input value, as defined 157 | * by the relationship: 158 | * 159 | * `output = Math.sqrt(input ** 2)` or simply `output = Math.abs(input)` 160 | */ 161 | Absolute = 2 162 | } 163 | 164 | export interface Modulator { 165 | /** 166 | * Destination generator. 167 | */ 168 | id: GeneratorType; 169 | 170 | /** 171 | * Source modulator. 172 | */ 173 | source: ControllerValue; 174 | 175 | /** 176 | * Degree of modulation. 177 | */ 178 | value: number; 179 | 180 | /** 181 | * Source controls value of first. 182 | * 183 | * TODO: Description is unclear. Should be improved. 184 | */ 185 | valueSource: ControllerValue; 186 | 187 | /** 188 | * Transform applied to source. 189 | */ 190 | transform: TransformType; 191 | } 192 | 193 | /** 194 | * The default modulators at instrument level. Implementing these is up to the consumer of this 195 | * library. To override these modulators, other modulators have to be defined explicitly. 196 | */ 197 | export const DEFAULT_INSTRUMENT_MODULATORS: Modulator[] = [ 198 | // MIDI note-on velocity to initial attenuation 199 | { 200 | id: GeneratorType.InitialAttenuation, 201 | source: { 202 | type: ControllerType.Concave, 203 | polarity: ControllerPolarity.Unipolar, 204 | direction: ControllerDirection.Decreasing, 205 | palette: ControllerPalette.GeneralController, 206 | index: Controller.NoteOnVelocity 207 | }, 208 | value: 960, 209 | valueSource: { 210 | type: ControllerType.Linear, 211 | polarity: ControllerPolarity.Unipolar, 212 | direction: ControllerDirection.Increasing, 213 | palette: ControllerPalette.GeneralController, 214 | index: Controller.NoController 215 | }, 216 | transform: TransformType.Linear 217 | }, 218 | 219 | // MIDI note-on velocity to filter cutoff 220 | { 221 | id: GeneratorType.InitialFilterFc, 222 | source: { 223 | type: ControllerType.Linear, 224 | polarity: ControllerPolarity.Unipolar, 225 | direction: ControllerDirection.Decreasing, 226 | palette: ControllerPalette.GeneralController, 227 | index: Controller.NoteOnVelocity 228 | }, 229 | value: -2400, // cents 230 | valueSource: { 231 | type: ControllerType.Linear, 232 | polarity: ControllerPolarity.Unipolar, 233 | direction: ControllerDirection.Increasing, 234 | palette: ControllerPalette.GeneralController, 235 | index: Controller.NoController 236 | }, 237 | transform: TransformType.Linear 238 | }, 239 | 240 | // MIDI channel pressure to vibrato LFO pitch depth 241 | { 242 | id: GeneratorType.VibLFOToPitch, 243 | source: { 244 | type: ControllerType.Linear, 245 | polarity: ControllerPolarity.Unipolar, 246 | direction: ControllerDirection.Increasing, 247 | palette: ControllerPalette.GeneralController, 248 | index: Controller.ChannelPressure 249 | }, 250 | value: 50, // cents / max excursion 251 | valueSource: { 252 | type: ControllerType.Linear, 253 | polarity: ControllerPolarity.Unipolar, 254 | direction: ControllerDirection.Increasing, 255 | palette: ControllerPalette.GeneralController, 256 | index: Controller.NoController 257 | }, 258 | transform: TransformType.Linear 259 | }, 260 | 261 | // MIDI continuous controller 1 to vibrato LFO pitch depth 262 | { 263 | id: GeneratorType.VibLFOToPitch, 264 | source: { 265 | type: ControllerType.Linear, 266 | polarity: ControllerPolarity.Unipolar, 267 | direction: ControllerDirection.Increasing, 268 | palette: ControllerPalette.MidiController, 269 | index: 1 270 | }, 271 | value: 50, 272 | valueSource: { 273 | type: ControllerType.Linear, 274 | polarity: ControllerPolarity.Unipolar, 275 | direction: ControllerDirection.Increasing, 276 | palette: ControllerPalette.GeneralController, 277 | index: Controller.NoController 278 | }, 279 | transform: TransformType.Linear 280 | }, 281 | 282 | // MIDI continuous controller 7 to initial attenuation 283 | { 284 | id: GeneratorType.InitialAttenuation, 285 | source: { 286 | type: ControllerType.Concave, 287 | polarity: ControllerPolarity.Unipolar, 288 | direction: ControllerDirection.Decreasing, 289 | palette: ControllerPalette.MidiController, 290 | index: 7 291 | }, 292 | value: 960, 293 | valueSource: { 294 | type: ControllerType.Linear, 295 | polarity: ControllerPolarity.Unipolar, 296 | direction: ControllerDirection.Increasing, 297 | palette: ControllerPalette.GeneralController, 298 | index: Controller.NoController 299 | }, 300 | transform: TransformType.Linear 301 | }, 302 | 303 | // MIDI continuous controller 10 to pan position 304 | { 305 | id: GeneratorType.InitialAttenuation, 306 | source: { 307 | type: ControllerType.Linear, 308 | polarity: ControllerPolarity.Bipolar, 309 | direction: ControllerDirection.Increasing, 310 | palette: ControllerPalette.MidiController, 311 | index: 10 312 | }, 313 | value: 1000, // tenths of a percent 314 | valueSource: { 315 | type: ControllerType.Linear, 316 | polarity: ControllerPolarity.Unipolar, 317 | direction: ControllerDirection.Increasing, 318 | palette: ControllerPalette.GeneralController, 319 | index: Controller.NoController 320 | }, 321 | transform: TransformType.Linear 322 | }, 323 | 324 | // MIDI continuous controller 11 to initial attenuation 325 | { 326 | id: GeneratorType.InitialAttenuation, 327 | source: { 328 | type: ControllerType.Concave, 329 | polarity: ControllerPolarity.Unipolar, 330 | direction: ControllerDirection.Decreasing, 331 | palette: ControllerPalette.MidiController, 332 | index: 11 333 | }, 334 | value: 960, 335 | valueSource: { 336 | type: ControllerType.Linear, 337 | polarity: ControllerPolarity.Unipolar, 338 | direction: ControllerDirection.Increasing, 339 | palette: ControllerPalette.GeneralController, 340 | index: Controller.NoController 341 | }, 342 | transform: TransformType.Linear 343 | }, 344 | 345 | // MIDI continuous controller 91 to reverb effects send 346 | { 347 | id: GeneratorType.ReverbEffectsSend, 348 | source: { 349 | type: ControllerType.Linear, 350 | polarity: ControllerPolarity.Unipolar, 351 | direction: ControllerDirection.Increasing, 352 | palette: ControllerPalette.MidiController, 353 | index: 91 354 | }, 355 | value: 200, // tenths of a percent 356 | valueSource: { 357 | type: ControllerType.Linear, 358 | polarity: ControllerPolarity.Unipolar, 359 | direction: ControllerDirection.Increasing, 360 | palette: ControllerPalette.GeneralController, 361 | index: Controller.NoController 362 | }, 363 | transform: TransformType.Linear 364 | }, 365 | 366 | // MIDI continuous controller 93 to chorus effects send 367 | { 368 | id: GeneratorType.ChorusEffectsSend, 369 | source: { 370 | type: ControllerType.Linear, 371 | polarity: ControllerPolarity.Unipolar, 372 | direction: ControllerDirection.Increasing, 373 | palette: ControllerPalette.MidiController, 374 | index: 93 375 | }, 376 | value: 200, // tenths of a percent 377 | valueSource: { 378 | type: ControllerType.Linear, 379 | polarity: ControllerPolarity.Unipolar, 380 | direction: ControllerDirection.Increasing, 381 | palette: ControllerPalette.GeneralController, 382 | index: Controller.NoController 383 | }, 384 | transform: TransformType.Linear 385 | }, 386 | 387 | // MIDI pitch wheel to initial pitch controlled by MIDI pitch wheel sensitivity 388 | { 389 | id: GeneratorType.CoarseTune, 390 | source: { 391 | type: ControllerType.Linear, 392 | polarity: ControllerPolarity.Bipolar, 393 | direction: ControllerDirection.Increasing, 394 | palette: ControllerPalette.GeneralController, 395 | index: Controller.PitchWheel 396 | }, 397 | value: 12700, // cents 398 | valueSource: { 399 | type: ControllerType.Linear, 400 | polarity: ControllerPolarity.Unipolar, 401 | direction: ControllerDirection.Increasing, 402 | palette: ControllerPalette.GeneralController, 403 | index: Controller.PitchWheelSensitivity 404 | }, 405 | transform: TransformType.Linear 406 | } 407 | ]; 408 | -------------------------------------------------------------------------------- /src/types/generator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Generator (or modulator) types. These are defined in a specific order, following the spec. The 3 | * enum ID corresponds with the ID in the spec, so the unused and reserved fields should not be 4 | * removed. 5 | */ 6 | export enum GeneratorType { 7 | /** 8 | * The offset in sample data points beyond the `start` sample header. 9 | */ 10 | StartAddrsOffset, 11 | 12 | /** 13 | * The offset in sample data points beyond the `end` sample header. 14 | */ 15 | EndAddrsOffset, 16 | 17 | /** 18 | * The offset in sample data points beyond the `startLoop` sample header to the first sample data 19 | * point to be repeated in the loop for the instrument. 20 | */ 21 | StartLoopAddrsOffset, 22 | 23 | /** 24 | * The offset in sample data points beyond the `endLoop` sample header to the sample data point 25 | * considered equivalent to the `startLoop` sample data point for the loop for this instrument. 26 | */ 27 | EndLoopAddrsOffset, 28 | 29 | /** 30 | * The offset in 32768 sample data point increments beyond the `start` sample header and the 31 | * first sample data point to be played in the instrument. 32 | */ 33 | StartAddrsCoarseOffset, 34 | 35 | /** 36 | * The degree (in cents) to which a full scale excursion of the modulation LFO will influence the 37 | * pitch. 38 | */ 39 | ModLFOToPitch, 40 | 41 | /** 42 | * The degree (in cents) to which a full scale excursion of the vibrato LFO will influence the 43 | * pitch. 44 | */ 45 | VibLFOToPitch, 46 | 47 | /** 48 | * The degree (in cents) to which a full scale excursion of the modulation envelope will 49 | * influence pitch. 50 | */ 51 | ModEnvToPitch, 52 | 53 | /** 54 | * The cutoff and resonant frequency of the lowpass filter in absolute cent units. 55 | */ 56 | InitialFilterFc, 57 | 58 | /** 59 | * The height above DC gain in centibels which the filter resonance exhibits at the cutoff 60 | * latency. 61 | */ 62 | InitialFilterQ, 63 | 64 | /** 65 | * The degree (in cents) to which a full scale excursion of the modulation LFO will influence the 66 | * filter cutoff frequency. 67 | */ 68 | ModLFOToFilterFc, 69 | 70 | /** 71 | * The degree (in cents) to which a full scale excursion of the modulation envelope will 72 | * influence the filter cutoff frequency. 73 | */ 74 | ModEnvToFilterFc, 75 | 76 | /** 77 | * The offset in 32768 sample data point increments beyond the `end` sample header and the last 78 | * sample data point to be played in this instrument. 79 | */ 80 | EndAddrsCoarseOffset, 81 | 82 | /** 83 | * The degree (in centibels) to which a full scale excursion of the modulation LFO will influence 84 | * volume. 85 | */ 86 | ModLFOToVolume, 87 | 88 | /** 89 | * Unused generator. If this generator is encountered, it should be ignored. 90 | */ 91 | Unused1, 92 | 93 | /** 94 | * The degree (in 0.1% units) to which the audio output of the note is sent to the chorus effects 95 | * processor. 96 | */ 97 | ChorusEffectsSend, 98 | 99 | /** 100 | * The degree (in 0.1% units) to which the audio output of the note is sent to the reverb effects 101 | * processor. 102 | */ 103 | ReverbEffectsSend, 104 | 105 | /** 106 | * The degree (in 0.1% units) to which the dry audio output of the note is positioned to the left 107 | * or right output. 108 | */ 109 | Pan, 110 | 111 | /** 112 | * Unused generator. If this generator is encountered, it should be ignored. 113 | */ 114 | Unused2, 115 | 116 | /** 117 | * Unused generator. If this generator is encountered, it should be ignored. 118 | */ 119 | Unused3, 120 | 121 | /** 122 | * Unused generator. If this generator is encountered, it should be ignored. 123 | */ 124 | Unused4, 125 | 126 | /** 127 | * The delay time (in absolute timecents) from key on until the modulation LFO begins its upward 128 | * ramp from zero value. A value of zero indicates a one second delay. 129 | */ 130 | DelayModLFO, 131 | 132 | /** 133 | * The frequency (in absolute cents) of the modulation LFO's triangular period. A value of 0 134 | * indicates a frequency of 8.176 Hz. 135 | */ 136 | FreqModLFO, 137 | 138 | /** 139 | * The delay time (in absolute timecents) from key on until the vibrato LFO begins its upwards 140 | * ramp from zero value. A value of zero indicates a one second delay. 141 | */ 142 | DelayVibLFO, 143 | 144 | /** 145 | * The frequency (in absolute cents) of the vibrato LFO's triangular period. A value of zero 146 | * indicates a frequency of 8.176 Hz. 147 | */ 148 | FreqVibLFO, 149 | 150 | /** 151 | * The delay time (in absolute timecents) between key on and the start of the attack phase on the 152 | * modulation envelope. A value of zero indicates a one second delay. 153 | */ 154 | DelayModEnv, 155 | 156 | /** 157 | * The time (in absolute timecents) from the end of the modulation envelope delay time until the 158 | * point at which the modulation envelope value reaches its peak. 159 | */ 160 | AttackModEnv, 161 | 162 | /** 163 | * The time (in absolute timecents) from the end of the attack phase to the entry into decay 164 | * phase, during which the envelope value is held at its peak. A value of zero indicates a one 165 | * second hold time. 166 | */ 167 | HoldModEnv, 168 | 169 | /** 170 | * The time (in absolute timecents) for a 100% change in the modulation envelope value during 171 | * decay phase. 172 | */ 173 | DecayModEnv, 174 | 175 | /** 176 | * The decrease in level (expressed in 0.1% units) to which the modulation envelope ramps during 177 | * the decay phase. 178 | */ 179 | SustainModEnv, 180 | 181 | /** 182 | * The time (in absolute timecents) for a 100% change in the modulation envelope value during 183 | * release phase. 184 | */ 185 | ReleaseModEnv, 186 | 187 | /** 188 | * The degree (in timecents per key number units) to which the hold time of the modulation 189 | * envelope is decreased by increasing the MIDI key number. 190 | */ 191 | KeyNumToModEnvHold, 192 | 193 | /** 194 | * The degree (in timecents per key number units) to which the hold time of the modulation 195 | * envelope is decreased by increasing the MIDI key number. 196 | */ 197 | KeyNumToModEnvDecay, 198 | 199 | /** 200 | * The delay time (in absolute timecents) between key on and the start of the attack phase of the 201 | * volume envelope. A value of zero indicates a one second delay. 202 | */ 203 | DelayVolEnv, 204 | 205 | /** 206 | * The delay time (in absolute timecents) from the end of the volume envelope delay time until 207 | * the point at which the volume envelope value reaches its peak. 208 | */ 209 | AttackVolEnv, 210 | 211 | /** 212 | * The time (in absolute timecents) from the end of the attack phase to the entry into decay 213 | * phase during which the volume envelope value is held at its peak. A value of zero indicates a 214 | * one second hold time. 215 | */ 216 | HoldVolEnv, 217 | 218 | /** 219 | * The time (in absolute timecents) for a 100% change in the volume envelope value during decay 220 | * phase. 221 | */ 222 | DecayVolEnv, 223 | 224 | /** 225 | * The decrease in level (expressed in centibels) to which the volume envelope value ramps during 226 | * the decay phase. 227 | */ 228 | SustainVolEnv, 229 | 230 | /** 231 | * The time (in absolute centibels) for a 100% change in the volume envelope during release 232 | * phase. A value of zero indicates a one second decay time. 233 | */ 234 | ReleaseVolEnv, 235 | 236 | /** 237 | * The degree (in timecents per key number units) to which the hold time of the volume envelope 238 | * is increased by increasing the MIDI key number. 239 | */ 240 | KeyNumToVolEnvHold, 241 | 242 | /** 243 | * The degree (in timecents per key number units) to which the hold time of the volume envelope 244 | * is decreased by increasing the MIDI key number. 245 | */ 246 | KeyNumToVolEnvDecay, 247 | 248 | /** 249 | * Index of an instrument in the `INST` sub-chunk. This generator and `SampleId` are the only 250 | * Index generators. 251 | */ 252 | Instrument, 253 | 254 | /** 255 | * Unused generator, reserved for future implementation. If this generator is encountered, it 256 | * should be ignored. 257 | */ 258 | Reserved1, 259 | 260 | /** 261 | * The key range that the preset or instrument zone applies to. This generator and `VelRange` are 262 | * the only Range generators. 263 | */ 264 | KeyRange, 265 | 266 | /** 267 | * The velocity range that the preset or instrument zone applies to. This generator and 268 | * `KeyRange` are the only Range generators. 269 | */ 270 | VelRange, 271 | 272 | /** 273 | * The offset in 32768 sample data point increments beyond the `startLoop` sample header and the 274 | * first sample data point to be repeated in this instrument's loop. 275 | */ 276 | StartLoopAddrsCoarseOffset, 277 | 278 | /** 279 | * Forces the MIDI key number to be interpreted as the value given. This can only appear at 280 | * instrument level and must be between 0 and 127. 281 | */ 282 | KeyNum, 283 | 284 | /** 285 | * Forces the MIDI velocity to be interpreted as the value given. This can only appear at 286 | * instrument level and must be between 0 and 127. 287 | */ 288 | Velocity, 289 | 290 | /** 291 | * The attenuation (in centibels) by which a note is attenuated below full scale. 292 | */ 293 | InitialAttenuation, 294 | 295 | /** 296 | * Unused generator, reserved for future implementation. If this generator is encountered, it 297 | * should be ignored. 298 | */ 299 | Reserved2, 300 | 301 | /** 302 | * The offset in 32768 sample data point increments beyond the `endLoop` sample header to the 303 | * sample data point considered equivalent to the `startLoop`. 304 | */ 305 | EndLoopAddrsCoarseOffset, 306 | 307 | /** 308 | * The pitch offset (in semitones) which should be applied to the note. It is additive with 309 | * `FineTune`. 310 | */ 311 | CoarseTune, 312 | 313 | /** 314 | * The pitch offset (in cents) which should be applied to the note. It is additive with 315 | * `CoarseTune`. 316 | */ 317 | FineTune, 318 | 319 | /** 320 | * Index of a sample in the `SHDR` sub-chunk. This generator and `Instrument` are the only Index 321 | * generators. 322 | */ 323 | SampleId, 324 | 325 | /** 326 | * The value which gives a variety of boolean flags describing the sample for the current 327 | * instrument zone. A zero indicates a sound reproduced with no loop, one indicates a sound which 328 | * loops continuously and three indicates a sound which loops for the duration of key depression, 329 | * then proceeds to play the remainder of the sample. 330 | */ 331 | SampleModes, 332 | 333 | /** 334 | * Unused generator, reserved for future implementation. If this generator is encountered, it 335 | * should be ignored. 336 | */ 337 | Reserved3, 338 | 339 | /** 340 | * The degree to which the MIDI key number influences pitch. 341 | */ 342 | ScaleTuning, 343 | 344 | /** 345 | * The capability for key depression in a given instrument to terminate the playback of other 346 | * instruments. 347 | */ 348 | ExclusiveClass, 349 | 350 | /** 351 | * The MIDI key number at which the sample is to be played back at its original sample rate. If 352 | * not present, or if present with a value of -1, the sample header parameter original key is 353 | * used in its place. 354 | */ 355 | OverridingRootKey, 356 | 357 | /** 358 | * Unused generator. If this generator is encountered, it should be ignored. 359 | */ 360 | Unused5, 361 | 362 | /** 363 | * Unused generator. If this generator is encountered, it should be ignored. 364 | */ 365 | EndOper 366 | } 367 | 368 | /** 369 | * All unused generators. 370 | */ 371 | export type UnusedGenerator = 372 | | GeneratorType.Unused1 373 | | GeneratorType.Unused2 374 | | GeneratorType.Unused3 375 | | GeneratorType.Unused4 376 | | GeneratorType.Unused5 377 | | GeneratorType.Reserved1 378 | | GeneratorType.Reserved2 379 | | GeneratorType.Reserved3 380 | | GeneratorType.EndOper; 381 | 382 | /** 383 | * All range generators. 384 | */ 385 | export type RangeGenerator = GeneratorType.KeyRange | GeneratorType.VelRange; 386 | 387 | /** 388 | * All index generators. 389 | */ 390 | export type IndexGenerator = GeneratorType.Instrument | GeneratorType.SampleId; 391 | 392 | /** 393 | * All value generators. 394 | */ 395 | export type ValueGenerator = Exclude< 396 | GeneratorType, 397 | UnusedGenerator | RangeGenerator | IndexGenerator 398 | >; 399 | 400 | /** 401 | * The default value for all generator types (where applicable). 402 | */ 403 | export const DEFAULT_GENERATOR_VALUES: { [key in ValueGenerator]: number } = { 404 | [GeneratorType.StartAddrsOffset]: 0, 405 | [GeneratorType.EndAddrsOffset]: 0, 406 | [GeneratorType.StartLoopAddrsOffset]: 0, 407 | [GeneratorType.EndLoopAddrsOffset]: 0, 408 | [GeneratorType.StartAddrsCoarseOffset]: 0, 409 | [GeneratorType.ModLFOToPitch]: 0, 410 | [GeneratorType.VibLFOToPitch]: 0, 411 | [GeneratorType.ModEnvToPitch]: 0, 412 | [GeneratorType.InitialFilterFc]: 13500, 413 | [GeneratorType.InitialFilterQ]: 0, 414 | [GeneratorType.ModLFOToFilterFc]: 0, 415 | [GeneratorType.ModEnvToFilterFc]: 0, 416 | [GeneratorType.EndAddrsCoarseOffset]: 0, 417 | [GeneratorType.ModLFOToVolume]: 0, 418 | [GeneratorType.ChorusEffectsSend]: 0, 419 | [GeneratorType.ReverbEffectsSend]: 0, 420 | [GeneratorType.Pan]: 0, 421 | [GeneratorType.DelayModLFO]: -12000, 422 | [GeneratorType.FreqModLFO]: 0, 423 | [GeneratorType.DelayVibLFO]: -12000, 424 | [GeneratorType.FreqVibLFO]: 0, 425 | [GeneratorType.DelayModEnv]: -12000, 426 | [GeneratorType.AttackModEnv]: -12000, 427 | [GeneratorType.HoldModEnv]: -12000, 428 | [GeneratorType.DecayModEnv]: -12000, 429 | [GeneratorType.SustainModEnv]: 0, 430 | [GeneratorType.ReleaseModEnv]: -12000, 431 | [GeneratorType.KeyNumToModEnvHold]: 0, 432 | [GeneratorType.KeyNumToModEnvDecay]: 0, 433 | [GeneratorType.DelayVolEnv]: -12000, 434 | [GeneratorType.AttackVolEnv]: -12000, 435 | [GeneratorType.HoldVolEnv]: -12000, 436 | [GeneratorType.DecayVolEnv]: -12000, 437 | [GeneratorType.SustainVolEnv]: 0, 438 | [GeneratorType.ReleaseVolEnv]: -12000, 439 | [GeneratorType.KeyNumToVolEnvHold]: 0, 440 | [GeneratorType.KeyNumToVolEnvDecay]: 0, 441 | [GeneratorType.StartLoopAddrsCoarseOffset]: 0, 442 | [GeneratorType.KeyNum]: -1, 443 | [GeneratorType.Velocity]: -1, 444 | [GeneratorType.InitialAttenuation]: 0, 445 | [GeneratorType.EndLoopAddrsCoarseOffset]: 0, 446 | [GeneratorType.CoarseTune]: 0, 447 | [GeneratorType.FineTune]: 0, 448 | [GeneratorType.SampleModes]: 0, 449 | [GeneratorType.ScaleTuning]: 100, 450 | [GeneratorType.ExclusiveClass]: 0, 451 | [GeneratorType.OverridingRootKey]: -1 452 | }; 453 | 454 | /** 455 | * Describes a range of MIDI key numbers (for the `keyRange` generator) or MIDI velocities (for the 456 | * `velRange` generator) with a minimum (lo) and maximum (hi) value. 457 | */ 458 | export interface Range { 459 | /** 460 | * Low value for the range. 461 | */ 462 | lo: number; 463 | 464 | /** 465 | * High value for the range. 466 | */ 467 | hi: number; 468 | } 469 | 470 | export interface Generator { 471 | /** 472 | * The ID of the generator. 473 | */ 474 | id: GeneratorType; 475 | 476 | /** 477 | * Generator value. If the range is not specified, this should be set. 478 | */ 479 | value?: number; 480 | 481 | /** 482 | * The range of the generator. If the value is not specified, this should be set. 483 | */ 484 | range?: Range; 485 | } 486 | --------------------------------------------------------------------------------