CARVE WAVE EDITOR · LISA RECORDS — MANUAL ← back to CARVE
Contents
  1. Basics
  2. Navigation & selection
  3. Playback
  4. Markers
  5. Editing
  6. Exporting
  7. OGG loop tags
  8. Keyboard shortcuts
  9. For programmers: implementing loop playback

1. Basics

CARVE is a wave editor that runs entirely in your browser. Files are processed only on your device — nothing is uploaded or stored on a server. No install, no account.

2. Navigation & selection

3. Playback

4. Markers

5. Editing

Operations apply to the selection, or to the whole file when nothing is selected (where noted).

6. Exporting

Pick a format in the status bar, then press Export.

FormatDetails
WAV 16-bit / 24-bitPCM with TPDF dither (plus first-order noise shaping at 16-bit).
WAV 32-bit floatBit-exact copy of the internal data.
OGG VorbisVBR, quality q2–q9.9. Embeds loop tags when markers 1–2 are set.
MP3LAME CBR, 128–320kbps, 1–2 channels.

OGG/MP3 encoding downloads a small WASM encoder on first use, so those two need a network connection once.

7. OGG loop tags

CARVE reads and writes the de-facto standard loop metadata for game audio: LOOPSTART and LOOPLENGTH stored as Vorbis comments, measured in samples.

Recommended workflow: set marker 1 at the loop start and marker 2 at the loop end (zero-cross snapping keeps them clean) → double-click between them to select the loop → Seam to audition the join → export as OGG. The file is ready for RPG Maker, Godot, or any engine that honors these tags.

These tags are a community convention (popularized by RPG Maker), not part of the Vorbis specification. Players that don't understand them simply ignore them — the file remains a perfectly ordinary OGG.

8. Keyboard shortcuts

KeyAction
SpacePlay / stop (loops the selection if one exists)
EnterPlay from the start of the file
Nudge cursor (Shift = ×10)
Vertical zoom in / out
MAdd marker at cursor
⇧MRemove marker nearest the cursor
⌘C / ⌘VCopy selection / insert-paste at cursor
DelDelete selection
⌘ZUndo
⇧⌘Z / ⌘YRedo
EscClear selection
Shift+dragSelect without zero-cross snapping

On Windows/Linux, use Ctrl in place of .

9. For programmers: implementing loop playback

9.1 The tag format

Loop points live in the Vorbis comment header (the same metadata block as ARTIST or TITLE) as plain KEY=VALUE strings:

LOOPSTART=132300      ; loop start, in samples
LOOPLENGTH=2646000    ; loop length, in samples

Some tools write LOOPEND instead of LOOPLENGTH; they are related by LOOPEND = LOOPSTART + LOOPLENGTH. A robust reader should accept both. Keys are case-insensitive per the Vorbis spec, but writing them in upper case is the convention.

Values are sample positions in the decoded PCM stream, at the file's own sample rate. To convert to seconds, divide by the sample rate. Because Vorbis is gapless (unlike MP3), sample positions in the source audio survive the encode exactly — sample 132300 in the OGG is sample 132300 in the original WAV.

9.2 The playback model

Intro-plus-loop playback, the standard game-BGM pattern:

play [0 .......... LOOPSTART .......... LOOPSTART+LOOPLENGTH)
                       ^                          |
                       └──────── jump back ───────┘
  1. Play from sample 0 (the intro plays once).
  2. When the play position reaches LOOPSTART + LOOPLENGTH, jump to LOOPSTART.
  3. Repeat step 2 forever (until fade-out / stop).

The jump must be sample-accurate. Timer-based seeking ("check position every frame, seek when past the end") drifts and clicks; use your audio API's native loop-region support, or do the wraparound yourself inside the audio callback / buffer-fill loop.

9.3 Web Audio API (JavaScript)

The easiest target — AudioBufferSourceNode has native sample-accurate loop regions. Full working example, including a minimal tag reader:

// 1. Read LOOPSTART / LOOPLENGTH from the raw ogg bytes.
//    The comment header is near the start of the file; this scans Ogg pages
//    for the packet beginning "\x03vorbis" and walks the comment list.
function readLoopTags(bytes) {                      // bytes: Uint8Array
  const td = new TextDecoder();
  let off = 0, guard = 0;
  while (off + 27 <= bytes.length && guard++ < 16) {
    if (td.decode(bytes.subarray(off, off + 4)) !== 'OggS') return null;
    const nsegs = bytes[off + 26];
    let bodyLen = 0;
    for (let i = 0; i < nsegs; i++) bodyLen += bytes[off + 27 + i];
    const body = bytes.subarray(off + 27 + nsegs, off + 27 + nsegs + bodyLen);
    if (body[0] === 3 && td.decode(body.subarray(1, 7)) === 'vorbis') {
      const dv = new DataView(body.buffer, body.byteOffset, body.byteLength);
      let p = 7;
      p += 4 + dv.getUint32(p, true);               // skip vendor string
      const count = dv.getUint32(p, true); p += 4;
      let ls = null, ll = null, le = null;
      for (let i = 0; i < count; i++) {
        const len = dv.getUint32(p, true); p += 4;
        const s = td.decode(body.subarray(p, p + len)); p += len;
        const [k, v] = [s.slice(0, s.indexOf('=')).toUpperCase(),
                        parseInt(s.slice(s.indexOf('=') + 1), 10)];
        if (k === 'LOOPSTART') ls = v;
        if (k === 'LOOPLENGTH') ll = v;
        if (k === 'LOOPEND') le = v;
      }
      if (ls != null && ll == null && le != null) ll = le - ls;
      return ls != null && ll > 0 ? { loopStart: ls, loopLength: ll } : null;
    }
    off += 27 + nsegs + bodyLen;
  }
  return null;
}

// 2. Decode and play with a native loop region.
async function playLoopedBgm(url) {
  const ctx = new AudioContext();
  const raw = await (await fetch(url)).arrayBuffer();
  const tags = readLoopTags(new Uint8Array(raw));
  const buf = await ctx.decodeAudioData(raw);       // note: detaches `raw`

  const src = ctx.createBufferSource();
  src.buffer = buf;
  if (tags) {
    const sr = buf.sampleRate;                      // use the DECODED rate (see 9.6)
    src.loop = true;
    src.loopStart = tags.loopStart / sr;            // Web Audio wants seconds
    src.loopEnd = (tags.loopStart + tags.loopLength) / sr;
  } else {
    src.loop = true;                                // no tags: loop the whole file
  }
  src.connect(ctx.destination);
  src.start();                                      // intro plays once, then loops
  return src;                                       // keep it to call .stop() later
}

Read the tags before calling decodeAudioData — it detaches the ArrayBuffer.

9.4 C / C++ (libvorbisfile or stb_vorbis)

Get the comments, then wrap the read position yourself:

// --- reading tags with libvorbisfile ---
vorbis_comment *vc = ov_comment(&vf, -1);
long loop_start = -1, loop_len = -1, loop_end = -1;
for (int i = 0; i < vc->comments; i++) {
    const char *c = vc->user_comments[i];
    if (!strncasecmp(c, "LOOPSTART=", 10))  loop_start = atol(c + 10);
    if (!strncasecmp(c, "LOOPLENGTH=", 11)) loop_len   = atol(c + 11);
    if (!strncasecmp(c, "LOOPEND=", 8))     loop_end   = atol(c + 8);
}
if (loop_start >= 0 && loop_len < 0 && loop_end > 0)
    loop_len = loop_end - loop_start;

// --- streaming decode with a sample-accurate wrap ---
// Track the absolute sample position yourself. When the next read would
// cross loop_start + loop_len, read only up to the boundary, then:
ov_pcm_seek(&vf, loop_start);   // sample-accurate seek, then keep reading

With stb_vorbis the pattern is identical: parse stb_vorbis_get_comment() output, count samples as you pull them, and call stb_vorbis_seek(v, loop_start) at the boundary. Decode-ahead and buffer the seam so the jump happens inside your mixer, not on the audio thread's deadline.

9.5 Engines

9.6 Pitfalls