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.
Operations apply to the selection, or to the whole file when nothing is selected (where noted).
Pick a format in the status bar, then press Export.
| Format | Details |
|---|---|
| WAV 16-bit / 24-bit | PCM with TPDF dither (plus first-order noise shaping at 16-bit). |
| WAV 32-bit float | Bit-exact copy of the internal data. |
| OGG Vorbis | VBR, quality q2–q9.9. Embeds loop tags when markers 1–2 are set. |
| MP3 | LAME 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.
CARVE reads and writes the de-facto standard loop metadata for game audio: LOOPSTART and LOOPLENGTH stored as Vorbis comments, measured in samples.
LOOPSTART = marker1 and LOOPLENGTH = marker2 − marker1. No markers → a plain OGG with no tags.LOOPSTART/LOOPLENGTH (or the LOOPEND variant) restores them as markers 1 and 2 automatically — a full round trip.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.
| Key | Action |
|---|---|
| Space | Play / stop (loops the selection if one exists) |
| Enter | Play from the start of the file |
| ← → | Nudge cursor (Shift = ×10) |
| ↑ ↓ | Vertical zoom in / out |
| M | Add marker at cursor |
| ⇧M | Remove marker nearest the cursor |
| ⌘C / ⌘V | Copy selection / insert-paste at cursor |
| Del | Delete selection |
| ⌘Z | Undo |
| ⇧⌘Z / ⌘Y | Redo |
| Esc | Clear selection |
| Shift+drag | Select without zero-cross snapping |
On Windows/Linux, use Ctrl in place of ⌘.
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.
Intro-plus-loop playback, the standard game-BGM pattern:
play [0 .......... LOOPSTART .......... LOOPSTART+LOOPLENGTH)
^ |
└──────── jump back ───────┘
LOOPSTART + LOOPLENGTH, jump to LOOPSTART.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.
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.
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.
LOOPSTART/LOOPLENGTH natively. CARVE's output works as-is; this convention originated here.AudioSources / AudioClip.SetData, or use an asset-store plugin. AudioSource.loop alone loops the whole clip including the intro, which is not what you want.loopStart).newRate / origRate and round — and beware that rounding can move the point off its zero crossing. Best practice: loop first at the native rate, or author at the target rate.decodeAudioData implementations resample to the context rate. Compare the decoded buffer's sampleRate against your expectation; scale tag values if they differ (the §9.3 code divides by buf.sampleRate, which handles the seconds-conversion side, but sample-domain logic must scale positions too).