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 Apr 21, 2021
1 parent 1faab9b commit da0930c
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 1 deletion.
8 changes: 8 additions & 0 deletions lib/ParserFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { WavPackParser } from './wavpack/WavPackParser';
import { DsfParser } from './dsf/DsfParser';
import { DsdiffParser } from './dsdiff/DsdiffParser';
import { MatroskaParser } from './matroska/MatroskaParser';
import { AmrParser } from './amr/AmrParser';

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

Expand Down Expand Up @@ -175,6 +176,9 @@ export class ParserFactory {
case '.mks':
case '.webm':
return 'matroska';

case '.amr':
return 'amr';
}
}

Expand All @@ -195,6 +199,7 @@ export class ParserFactory {
case 'riff': return new WaveParser();
case 'wavpack': return new WavPackParser();
case 'matroska': return new MatroskaParser();
case 'amr': return new AmrParser();
default:
throw new Error(`Unknown parser type: ${moduleName}`);
}
Expand Down Expand Up @@ -285,6 +290,9 @@ export class ParserFactory {

case 'dsf':
return 'dsf';

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

Expand Down
60 changes: 60 additions & 0 deletions lib/amr/AmrParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { BasicParser } from '../common/BasicParser';
import { AnsiStringType } from 'token-types';
import * as Token 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)
};
}
};
2 changes: 1 addition & 1 deletion lib/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -573,7 +573,7 @@ export interface IAudioMetadata extends INativeAudioMetadata {
/**
* Corresponds with parser module name
*/
export type ParserType = 'mpeg' | 'apev2' | 'mp4' | 'asf' | 'flac' | 'ogg' | 'aiff' | 'wavpack' | 'riff' | 'musepack' | 'dsf' | 'dsdiff' | 'adts' | 'matroska';
export type ParserType = 'mpeg' | 'apev2' | 'mp4' | 'asf' | 'flac' | 'ogg' | 'aiff' | 'wavpack' | 'riff' | 'musepack' | 'dsf' | 'dsdiff' | 'adts' | '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 da0930c

Please sign in to comment.