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 5b1cb0e
Show file tree
Hide file tree
Showing 7 changed files with 105 additions and 1 deletion.
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
3 changes: 3 additions & 0 deletions lib/ParserFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,9 @@ function getParserIdForMimeType(httpContentType: string | undefined): ParserType

case 'dsf':
return 'dsf';

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

Expand Down
58 changes: 58 additions & 0 deletions lib/amr/AmrParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { BasicParser } from '../common/BasicParser';
import { AnsiStringType } from 'token-types';
import * as initDebug from 'debug';
import { FrameHeader } from './AmrToken';

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 ? this.tokenizer.fileInfo.size : Number.MAX_SAFE_INTEGER;

if (this.options.duration) {
while (this.tokenizer.position < assumedFileLength) {
const header = await this.tokenizer.readToken(FrameHeader);
if (header.frameType === 15 || (header.frameType > 8 && header.frameType < 15)) {
debug(`Found no-data frame, ft: ${header.frameType}. Skipping`);
}
else {
debug(`Found data frame, ft: ${header.frameType}, frames: ${frames}, size: ${m_block_size[header.frameType]}`);
/* increase number of frames */
++frames;
}
/* first byte is rate mode. each rate mode has frame of given length. look it up. */
const size = m_block_size[header.frameType];

/* FIXME: there is something inherently broken with how I calculate frames here. Need to revise it. */
total_size += size + 1;
if (total_size > assumedFileLength) break;
await this.tokenizer.ignore(size);
}
this.metadata.setFormat('duration', frames * 0.02);
}
}
}
24 changes: 24 additions & 0 deletions lib/amr/AmrToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { IGetToken } from 'strtok3/lib/core';
import common from '../common/Util';

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, off): IFrameHeader => {
return {
// ID3v2/file identifier "ID3"
frameType: common.getBitAllignedNumber(buf, off, 4, 4),
fqi: common.strtokBITSET.get(buf, off, 3)
};
}
};
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/sample.amr
Binary file not shown.
17 changes: 17 additions & 0 deletions test/test-file-amr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { assert } from 'chai';
import * as mm from '../lib';
import * as path from 'path';

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

const amrSamplePath = path.join(__dirname, 'samples', 'amr');

it('Decode AMR file', async () => {
const {format} = await mm.parseFile(path.join(amrSamplePath, 'sample.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.340, 0.0005, 'format.duration');
});

});

0 comments on commit 5b1cb0e

Please sign in to comment.