Skip to content

Commit

Permalink
feat(ffmpeg): added tempo and playback timecode selection
Browse files Browse the repository at this point in the history
  • Loading branch information
OnkelTem committed Apr 18, 2021
1 parent 668ed04 commit bc77dd5
Show file tree
Hide file tree
Showing 4 changed files with 72 additions and 7 deletions.
53 changes: 51 additions & 2 deletions lib/audio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,16 +87,38 @@ export async function getOpusReader(
};
}

// http://ffmpeg.org/ffmpeg-utils.html#Time-duration
export type FFmpegDuration = string;

const reFFmpegDuration = /^((\d{2}:)?\d{2}:\d{2}(\.\d+)?|\d+(\.\d+)?(s|ms|us)?)$/gm;

function isFFmpegDuration(arg: string): arg is FFmpegDuration {
// Either [-][HH:]MM:SS[.m...] or [-]S+[.m...][s|ms|us]
return !!arg.match(reFFmpegDuration);
}

export type FFmpegTempo = number;

function isFFmpegTempo(arg: number): arg is FFmpegTempo {
return arg >= 0.5 && arg <= 2.0;
}

export type FileStreamOptions = {
samplingRate?: SamplingRate;
volumeFactor?: number;
tempoFactor?: FFmpegTempo;
startAt?: FFmpegDuration | null;
endAt?: FFmpegDuration | null;
ffmpegArgs?: FFmpegArgs;
};

const FILE_STREAM_DEFAULT_FFMPEG_ARGS: FFmpegArgs = ['-channel_layout', 'mono'];
const FILE_STREAM_DEFAULT_OPTIONS: Required<FileStreamOptions> = {
samplingRate: 48000,
volumeFactor: 0.5,
tempoFactor: 1,
startAt: null,
endAt: null,
ffmpegArgs: FILE_STREAM_DEFAULT_FFMPEG_ARGS,
};

Expand Down Expand Up @@ -219,9 +241,34 @@ export function getAudioFileStream(path: string, parentLogger: Logger, options?:
export function getAutoDecodeStream(stream: Readable, parentLogger: Logger, options?: FileStreamOptions): Duplex {
const logger = parentLogger.child({ facility: 'getAutoDecodeStream' });

const { samplingRate, volumeFactor, ffmpegArgs }: Required<FileStreamOptions> =
const { samplingRate, volumeFactor, tempoFactor, startAt, endAt, ffmpegArgs }: Required<FileStreamOptions> =
options != null ? { ...FILE_STREAM_DEFAULT_OPTIONS, ...options } : FILE_STREAM_DEFAULT_OPTIONS;

// From ... to ...
let startAtArg: [string, string] | [] = [];
let endAtArg: [string, string] | [] = [];
if (startAt != null) {
if (isFFmpegDuration(startAt)) {
startAtArg = ['-ss', startAt];
} else {
logger.warn(`Wrong startAt format: ${startAt}`);
}
}
if (endAt != null) {
if (isFFmpegDuration(endAt)) {
endAtArg = ['-t', endAt];
} else {
logger.warn(`Wrong endAt format: ${endAt}`);
}
}

const filters: string[] = [];
filters.push(`aresample=${samplingRate}`);
filters.push(`volume=${volumeFactor}`);
if (tempoFactor !== 1 && isFFmpegTempo(tempoFactor)) {
filters.push(`atempo=${tempoFactor}`);
}

// Create transcoder stream
const transcoderArguments = [
'-analyzeduration',
Expand All @@ -230,10 +277,12 @@ export function getAutoDecodeStream(stream: Readable, parentLogger: Logger, opti
'0',
'-i',
'-',
...startAtArg,
...endAtArg,
'-f',
's16le',
'-filter:a',
`aresample=${samplingRate},volume=${volumeFactor}`,
filters.join(','),
...ffmpegArgs,
];
const transcodeStream = new prism.FFmpeg({
Expand Down
3 changes: 2 additions & 1 deletion lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from './zello';
export { default } from './zello';
export * from './zello';
export * from './api';
export * from './types';
export * from './audio';
export * from './utils';
export * from './logger';
15 changes: 13 additions & 2 deletions lib/logger.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
import { isEnvVar } from './utils';
import { LoggerOptions } from 'pino';
import { LoggerOptions as LoggerOptionsBase } from 'pino';

export const loggerLevels = ['fatal', 'error', 'warn', 'info', 'debug', 'trace'] as const;
export type LoggerLevel = typeof loggerLevels[number];

export interface LoggerOptions extends Omit<LoggerOptionsBase, 'level'> {
level: LoggerLevel;
}

export function isLoggerLevel(arg?: string): arg is LoggerLevel {
return arg != null && loggerLevels.includes(arg as LoggerLevel);
}

export const DEFAULT_LOGGER_OPTIONS: LoggerOptions = {
level: process.env.LOGGER_LEVEL != null ? process.env.LOGGER_LEVEL : 'info',
level: isLoggerLevel(process.env.LOGGER_LEVEL) ? process.env.LOGGER_LEVEL : 'info',
...(isEnvVar(process.env.LOGGER_PRETTY) && {
prettyPrint: {
ignore: 'pid,hostname',
Expand Down
8 changes: 6 additions & 2 deletions lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,10 @@ type OpusInfo = {
frameSize: number;
};

type SamplingRate = 8000 | 12000 | 16000 | 24000 | 48000;
type FrameSize = 2.5 | 5 | 10 | 20 | 40 | 60;
const samplingRates = [8000, 12000, 16000, 24000, 48000] as const;
type SamplingRate = typeof samplingRates[number];
const frameSizes = [2.5, 5, 10, 20, 40, 60] as const;
type FrameSize = typeof frameSizes[number];
type Channels = 1 | 2;

type FFmpegArgs = string[];
Expand Down Expand Up @@ -214,8 +216,10 @@ export {
StreamGetter,
OpusInfo,
DeferredPromise,
samplingRates,
SamplingRate,
FFmpegArgs,
frameSizes,
FrameSize,
Channels,
StreamGetterOptions,
Expand Down

0 comments on commit bc77dd5

Please sign in to comment.