├── .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 |
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 | 
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 | 
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 |
--------------------------------------------------------------------------------