Skip to content

Commit

Permalink
#782 Support Adaptive Multi-Rate (AMR) audio codec
Browse files Browse the repository at this point in the history
  • Loading branch information
Borewit committed Sep 8, 2024
1 parent cbcc831 commit 8046107
Show file tree
Hide file tree
Showing 10 changed files with 135 additions and 2 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ If you find this project useful and would like to support its development, consi
| ------------- |---------------------------------| -------------------------------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------:|
| AIFF / AIFF-C | Audio Interchange File Format | [:link:](https://wikipedia.org/wiki/Audio_Interchange_File_Format) | <img src="https://upload.wikimedia.org/wikipedia/commons/8/84/Apple_Computer_Logo_rainbow.svg" width="40" alt="Apple rainbow logo"> |
| AAC | ADTS / Advanced Audio Coding | [:link:](https://en.wikipedia.org/wiki/Advanced_Audio_Coding) | <img src="https://svgshare.com/i/UT8.svg" width="40" alt="AAC logo"> |
| AMR | Adaptive Multi-Rate audio codec | [:link:](https://en.wikipedia.org/wiki/Adaptive_Multi-Rate_audio_codec) | <img src="https://foreverhits.files.wordpress.com/2015/05/ape_audio.jpg" width="40" alt="Monkey's Audio logo"> |
| APE | Monkey's Audio | [:link:](https://wikipedia.org/wiki/Monkey's_Audio) | <img src="https://foreverhits.files.wordpress.com/2015/05/ape_audio.jpg" width="40" alt="Monkey's Audio logo"> |
| ASF | Advanced Systems Format | [:link:](https://wikipedia.org/wiki/Advanced_Systems_Format) | |
| BWF | Broadcast Wave Format | [:link:](https://en.wikipedia.org/wiki/Broadcast_Wave_Format) | |
Expand Down
7 changes: 6 additions & 1 deletion lib/ParserFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { musepackParserLoader } from './musepack/MusepackLoader.js';
import { oggParserLoader } from './ogg/OggLoader.js';
import { wavpackParserLoader } from './wavpack/WavPackLoader.js';
import { riffParserLoader } from './wav/WaveLoader.js';
import { amrParserLoader } from './amr/AmrLoader.js';

const debug = initDebug('music-metadata:parser:factory');

Expand Down Expand Up @@ -81,7 +82,8 @@ export class ParserFactory {
wavpackParserLoader,
musepackParserLoader,
dsfParserLoader,
dsdiffParserLoader
dsdiffParserLoader,
amrParserLoader
].forEach(parser => this.registerParser(parser));
}

Expand Down Expand Up @@ -220,6 +222,9 @@ function getParserIdForMimeType(httpContentType: string | undefined): ParserType

case 'dsf':
return 'dsf';

case 'amr':
return 'amr';
}
break;

Expand Down
12 changes: 12 additions & 0 deletions lib/amr/AmrLoader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { IParserLoader, ITokenParser } from '../ParserFactory.js';
import type { INativeMetadataCollector } from '../common/MetadataCollector.js';
import type { ITokenizer } from 'strtok3';
import type { IOptions } from '../type.js';

export const amrParserLoader: IParserLoader = {
parserType: 'amr',
extensions: ['.amr'],
async load(metadata: INativeMetadataCollector, tokenizer: ITokenizer, options: IOptions): Promise<ITokenParser> {
return new (await import('./AmrParser.js')).AmrParser(metadata, tokenizer, options);
}
};
57 changes: 57 additions & 0 deletions lib/amr/AmrParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { BasicParser } from '../common/BasicParser.js';
import { AnsiStringType } from 'token-types';
import initDebug from 'debug';
import { FrameHeader } from './AmrToken.js';

const debug = initDebug('music-metadata:parser:AMR');

/**
* There are 8 varying levels of compression. First byte of the frame specifies CMR
* (codec mode request), values 0-7 are valid for AMR. Each mode have different frame size.
* This table reflects that fact.
*/
const m_block_size = [12, 13, 15, 17, 19, 20, 26, 31, 5, 0, 0, 0, 0, 0, 0, 0];

/**
* Adaptive Multi-Rate audio codec
*/
export class AmrParser extends BasicParser {

public async parse(): Promise<void> {
const magicNr = await this.tokenizer.readToken(new AnsiStringType(5));
if (magicNr !== '#!AMR') {
throw new Error('Invalid AMR file: invalid MAGIC number');
}
this.metadata.setFormat('container', 'AMR');
this.metadata.setFormat('codec', 'AMR');
this.metadata.setFormat('sampleRate', 8000);
this.metadata.setFormat('bitrate', 64000);
this.metadata.setFormat('numberOfChannels', 1);

let total_size = 0;
let frames = 0;

const assumedFileLength = this.tokenizer.fileInfo?.size ?? Number.MAX_SAFE_INTEGER;

if (this.options.duration) {
while (this.tokenizer.position < assumedFileLength) {

const header = await this.tokenizer.readToken(FrameHeader);

/* first byte is rate mode. each rate mode has frame of given length. look it up. */
const size = m_block_size[header.frameType];

if(size>0) {
total_size += size + 1;
if (total_size > assumedFileLength) break;
await this.tokenizer.ignore(size);
++frames;
} else {
debug(`Found no-data frame, frame-type: ${header.frameType}. Skipping`);
}

}
this.metadata.setFormat('duration', frames * 0.02);
}
}
}
23 changes: 23 additions & 0 deletions lib/amr/AmrToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { IGetToken } from 'strtok3';
import { isBitSet, getBitAllignedNumber } from '../common/Util.js';

interface IFrameHeader {
frameType: number;
fqi: boolean;
}

/**
* ID3v2 header
* Ref: http://id3.org/id3v2.3.0#ID3v2_header
* ToDo
*/
export const FrameHeader: IGetToken<IFrameHeader > = {
len: 1,

get: (buf: Uint8Array, off: number): IFrameHeader => {
return {
frameType: getBitAllignedNumber(buf, off, 1, 4),
fqi: isBitSet(buf, off, 0)
};
}
};
3 changes: 2 additions & 1 deletion lib/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -596,7 +596,8 @@ export type ParserType =
| 'dsf'
| 'dsdiff'
| 'adts'
| 'matroska';
| 'matroska'
| 'amr';

export interface IOptions {

Expand Down
Binary file added test/samples/amr/ff-16b-1c-8000hz.amr
Binary file not shown.
Binary file added test/samples/amr/gs-16b-1c-8000hz.amr
Binary file not shown.
Binary file added test/samples/amr/sample.amr
Binary file not shown.
34 changes: 34 additions & 0 deletions test/test-file-amr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { assert } from 'chai';
import path from 'node:path';
import { Parsers } from './metadata-parsers.js';
import { samplePath } from './util.js';

const amrPath = path.join(samplePath, 'amr');

describe('Adaptive Multi-Rate (AMR) audio file', () => {

Parsers.forEach(parser => {

describe('parser.description', () => {

it('parse: sample.amr', async function () {
const {format} = await parser.initParser(() => this.skip(), path.join(amrPath, 'sample.amr'), 'audio/amr', {duration: true});
assert.strictEqual(format.sampleRate, 8000, 'format.sampleRate');
assert.strictEqual(format.numberOfChannels, 1, 'format.numberOfChannels');
assert.strictEqual(format.bitrate, 64000, 'format.bitrate');
assert.approximately(format.duration, 35.36, 0.0005, 'format.duration');
});


it('parse: ff-16b-1c-8000hz.amr', async function () {
const {format} = await parser.initParser(() => this.skip(), path.join(amrPath, 'ff-16b-1c-8000hz.amr'), 'audio/amr', {duration: true});
assert.strictEqual(format.sampleRate, 8000, 'format.sampleRate');
assert.strictEqual(format.numberOfChannels, 1, 'format.numberOfChannels');
assert.strictEqual(format.bitrate, 64000, 'format.bitrate');
assert.approximately(format.duration, 187.56, 0.0005, 'format.duration');
});

});
});

});

0 comments on commit 8046107

Please sign in to comment.