gillesdemey / node-record-lpcm16

:microphone: Records a 16-bit signed-integer linear pulse modulation code encoded audio file.
ISC License
212 stars 59 forks source link

Partial Typescript Port #66

Closed RickCarlino closed 1 year ago

RickCarlino commented 1 year ago

Hi folks,

I only needed this package to use one recorder (rec). I ported the project to typescript because I could not find typings, and I am now using it in my project as a standalone file rather than an NPM module.

The implementation in typescript is listed below. Please note that I only ported one recorder (rec). You will need to adapt the code if you want to use other recorder types.

import assert from "assert";
import { ChildProcessWithoutNullStreams, spawn } from "child_process";
const DEFAULT_OPTS = {
  sampleRate: 16000,
  channels: 1,
  compress: false,
  threshold: 0.5,
  thresholdStart: undefined,
  thresholdEnd: undefined,
  silence: "1.0",
  recorder: "rec",
  endOnSilence: false,
  audioType: "wav",
};

type RecorderOpts = typeof DEFAULT_OPTS;

const defaultRecorder = (options: Partial<RecorderOpts> = {}) => {
  const cmd = "rec";

  let args = [
    "-q", // show no progress
    "-r",
    options.sampleRate ?? "", // sample rate
    "-c",
    options.channels ?? "", // channels
    "-e",
    "signed-integer", // sample encoding
    "-b",
    "16", // precision (bits)
    "-t",
    options.audioType ?? "", // audio type
    "-", // pipe
  ];

  if (options.endOnSilence) {
    args = args.concat([
      "silence",
      "1",
      "0.1",
      options.thresholdStart || options.threshold + "%",
      "1",
      options.silence ?? "",
      options.thresholdEnd || options.threshold + "%",
    ]);
  }

  return { cmd, args, spawnOptions: {} };
};

class Recording {
  public options = DEFAULT_OPTS;
  public cmd: string;
  public args: (string | number)[];
  public cmdOptions: {};
  public process?: ChildProcessWithoutNullStreams;
  public _stream?: NodeJS.ReadableStream;

  constructor(options: Partial<typeof DEFAULT_OPTS> = {}) {
    this.options = Object.assign(DEFAULT_OPTS, options);

    const recorder = defaultRecorder;
    const { cmd, args, spawnOptions = {} } = recorder(this.options);

    this.cmd = cmd;
    this.args = args;
    this.cmdOptions = Object.assign(
      { encoding: "binary", stdio: "pipe" },
      spawnOptions
    );

    console.log(`Started recording`);
    console.log(this.options);
    console.log(` ${this.cmd} ${this.args.join(" ")}`);

    return this.start();
  }

  start() {
    const { cmd, args, cmdOptions } = this;

    const cp = spawn(
      cmd,
      args.map((x) => "" + x),
      cmdOptions
    );
    const rec = cp.stdout;
    const err = cp.stderr;

    this.process = cp; // expose child process
    this._stream = rec; // expose output stream

    cp.on("close", (code) => {
      if (code === 0) return;
      rec.emit(
        "error",
        `${this.cmd} has exited with error code ${code}.

Enable debugging with the environment variable DEBUG=record.`
      );
    });

    err.on("data", (chunk) => {
      console.log(`STDERR: ${chunk}`);
    });

    rec.on("data", (chunk) => {
      console.log(`Recording ${chunk.length} bytes`);
    });

    rec.on("end", () => {
      console.log("Recording ended");
    });

    return this;
  }

  stop() {
    assert(this.process, "Recording not yet started");

    this.process.kill();
  }

  pause() {
    assert(this.process, "Recording not yet started");

    this.process.kill("SIGSTOP");
    this._stream?.pause();
    console.log("Paused recording");
  }

  resume() {
    assert(this.process, "Recording not yet started");

    this.process.kill("SIGCONT");
    this._stream?.resume();
    console.log("Resumed recording");
  }

  isPaused() {
    assert(this.process, "Recording not yet started");

    return this._stream?.isPaused();
  }

  stream() {
    assert(this._stream, "Recording not yet started");

    return this._stream;
  }
}

export const record = (x: Partial<typeof DEFAULT_OPTS>) => new Recording(x);
RickCarlino commented 1 year ago

I am closing this issue since it is for reference only.