Skip to main content

Plan an Audio Trim in Sample Frames with Node.js

Updated 4 min readMarkdown

When preparing speech for a talking-head shot, the editor may give you an in-point and out-point in milliseconds. The audio reader works on a grid of sample frames. A trim planner needs to say how it maps a requested time between two grid points.

This tutorial writes that mapping as a small Node.js program. It returns the start index, exclusive end index, and frame count for a local manifest. An audio pipeline can use those numbers when it performs the actual trim.

Define the coordinates

Use the sample rate and frame count from the same decoded track. A sample frame contains one sample per channel at a single instant. At 44,100 Hz, one second contains 44,100 frames whether the track is mono or stereo. Interleaved stereo would store twice as many individual sample values.

The manifest uses integer milliseconds from the start of the decoded track. Frame indices start at zero. The end index is exclusive, so an end equal to totalFrames is valid. An end greater than totalFrames is an error.

Round outward to cover the requested time

Multiply each time by the sample rate and divide by 1,000. Round the start down and the end up. This selects the frame cells covering the requested interval, possibly including a fraction of a frame period on either side.

In the example, 123 ms falls at frame coordinate 5,424.3, so the trim starts at 5,424. The out-point at 987 ms is 43,526.7, so the exclusive end is 43,527. The output count is the difference, 38,103.

Use BigInt for the multiplication so the intermediate result stays exact. On a nonnegative integer, division truncates down. Adding 999 before division by 1,000 gives the ceiling, including when the input already lands on an exact frame boundary.

Implement the planner

Save this as plan-trim.mjs. It rejects invalid coordinates and ranges that extend beyond the decoded track. The resulting frame indices can be converted back to Number because they fit within the validated safe integer frame count.

export function planTrim({ sampleRate, totalFrames, startMs, endMs }) {
  for (const [name, value] of Object.entries({
    sampleRate, totalFrames, startMs, endMs
  })) {
    if (!Number.isSafeInteger(value) || value < 0) {
      throw new RangeError(`${name} must be a nonnegative safe integer`);
    }
  }
  if (sampleRate === 0 || totalFrames === 0) {
    throw new RangeError("sampleRate and totalFrames must be positive");
  }
  if (endMs <= startMs) throw new RangeError("endMs must exceed startMs");

  const rate = BigInt(sampleRate);
  const start = (BigInt(startMs) * rate) / 1000n;
  const end = (BigInt(endMs) * rate + 999n) / 1000n;
  if (end > BigInt(totalFrames)) {
    throw new RangeError("Requested interval exceeds the decoded track");
  }

  return {
    startFrame: Number(start),
    endFrameExclusive: Number(end),
    frameCount: Number(end - start)
  };
}

Run the planner on a local manifest

Save this file as trim.mjs beside the planner. It reads one JSON file, prints the plan on success, and returns exit status 1 on invalid input.

import { readFile } from "node:fs/promises";
import { planTrim } from "./plan-trim.mjs";

try {
  const args = process.argv.slice(2);
  if (args.length !== 1) throw new Error("Usage: node trim.mjs track.json");
  const manifest = JSON.parse(await readFile(args[0], "utf8"));
  console.log(JSON.stringify(planTrim(manifest)));
} catch (error) {
  console.error(error.message);
  process.exitCode = 1;
}

Save this fixture as track.json. It describes a synthetic one-second timeline at 44,100 Hz.

{"sampleRate":44100,"totalFrames":44100,"startMs":123,"endMs":987}

Run the command in the directory containing the three files.

node trim.mjs track.json
# {"startFrame":5424,"endFrameExclusive":43527,"frameCount":38103}

Decide whether adjacent trims should share a frame

Outward rounding is useful for covering one isolated interval. If adjacent intervals both round outward at a shared time that falls between frame boundaries, they will share one frame. For clips that must fit together without overlap, resolve each shared time to one frame index and use that index as both the previous end and the next start.

Check a cut on an exact boundary, a cut ending at the last valid boundary, and a request beyond the track before connecting the planner to a decoder. Use decoded frame coordinates for the handoff, especially when an encoded input has padding or other timing details that differ from the decoded sequence.

Sources and disclosure

See the ECMAScript BigInt division specification for integer division behavior.

The workflow context comes from PixMind's guide to preparing audio for a talking-head shot. The manifest and outward rounding policy here are local editing choices.

AI disclosure. AI assisted with drafting this article. The Node.js program was checked with synthetic manifests. The example does not decode, cut, listen to, or generate any audio or video.

About the author

  • The PixMind team shares practical AI image and video workflows. PixMind brings text-to-image, reference-based creation, editing, and video tools into one workspace. Website: https://www.pixmind.io/

No comments yet. Be the first to comment.

More from this blog