---
name: chipvoice
description: Compose, import, arrange, evaluate and publish complete multi-instrument music for emulated retro sound chips. Exact-tick projects, machine capabilities and explicit adaptation reports.
compatibility: HTTP discovery and publication require a network client. Local composition and rendering require Node.js and the chipvoice npm package. Publishing projects requires an account key.
homepage: https://chipvoice.dev
metadata: {"version":"0.11.0","updated":"2026-09-08","engineVersion":"0.17.0","author":"gwendall"}
---

# Compose complete music with chipvoice

Use **MusicProject version 1 with a Performance source** for new multi-instrument music and MIDI imports. Parts are independent musical lines; roles (lead, chord, bass, perc) describe their purpose, not a four-part limit. Each part can contain overlapping notes. Physical voices are limited by the selected chip.

Create locally without an account. Publish only when asked, with an authenticated account. A valid project or a deterministic render does not prove musical quality or fidelity to an original game. Generic programs are approximations, not a complete orchestral sample library.

## Discover before composing

1. GET https://chipvoice.dev/api/v1/capabilities: supported targets, current engine version, actual voices, resource conflicts, generic instrument mappings and the project JSON Schema. Do not hard-code the number of consoles.
2. GET https://chipvoice.dev/.well-known/openapi.json: exact bodies, path/query/header parameters, authentication and response schemas.
3. Use https://chipvoice.dev/create for interactive editing, https://chipvoice.dev/docs for SDK examples and https://chipvoice.dev/explore for public music.
4. Detailed guide: https://github.com/gwendall/chipvoice/blob/main/docs/AGENT-COMPOSITION.md (Japanese: AGENT-COMPOSITION_ja.md).

The catalogue is generated at build time from the same chip definitions, palette and allocation helpers as the renderer. Its contentHash identifies this capability document. It is not an original-game audio fingerprint.

| Target | System | Declared voices | Shared resource pairs |
| --- | --- | --- | --- |
| `2a03` | NES / Famicom | p1 (pulse), p2 (pulse), tri (triangle), noi (noise), dmc (sample) | None declared |
| `dmg` | Game Boy | ch1 (pulse), ch2 (pulse), ch3 (wavetable), ch4 (noise) | None declared |
| `md` | Mega Drive / Genesis | fm1 (fm), fm2 (fm), fm3 (fm), fm4 (fm), fm5 (fm), fm6 (fm), psg1 (pulse), psg2 (pulse), psg3 (pulse), noise (noise) | psg3 / noise |
| `snes` | Super Nintendo / Super Famicom | v0 (sample), v1 (sample), v2 (sample), v3 (sample), v4 (sample), v5 (sample), v6 (sample), v7 (sample) | None declared |
| `c64` | Commodore 64 | v1 (pulse), v2 (pulse), v3 (pulse) | None declared |

Read each target's melodicPalette: programs are zero-based General MIDI numbers. voices lists eligible destinations; preservesInstrument=false means fallback substitution. pitchHz is the base register range before modulation; null means unknown. excludedPerformanceVoices may exist in raw/native APIs without being allocated by Performance. percussionVoices describes the generic drum path, not every custom patch. A voice count is not a promise that all combinations fit. The planner's report is decisive.

## Compose deliberately

- Decide an original musical brief: mood, form, tempo, tonal centre, foreground and supporting lines. Develop complete phrases and an ending or intentional loop. Vary rhythm, register and instrumentation across sections.
- Write melody, harmony, bass, counterpoint and percussion only when useful. For transcriptions, preserve identifiable source parts and rests; never invent backing or claim a repeated fragment is the full source.
- One chord tone consumes one voice. A held triad plus melody plus bass consumes five pitched voices before drums and counterpoint. Instrument names do not create additional hardware.
- Use priority to protect musically essential notes when voices run out; higher values win. Priority is allocation order, not volume. mix.importance (0–1) controls prominence, mix.gainDb is an explicit trim, velocity (0–127) is note expression. Lower priority does not automatically lower volume.
- Keep melody intelligible, avoid unnecessary unison doubling and crowded low-register chords, and leave rhythmic space. These are starting points, not universal genre rules. Automatic mixing cannot repair a poor arrangement.
- On constrained targets, make an explicit adaptation: remove redundant doublings, alternate accompaniment with fills, use two-note voicings or write a timed arpeggio. The Performance allocator reports omitted notes; it does not invent arpeggios or choose an artistically optimal reduction.
- Percussion uses note.drum: 35/36 kick, 38/40 snare, 46 open hat; other keys currently use the generic closed-hat fallback. Preserve imported keys, but do not claim a complete GM drum kit.
- Inspect the catalogue for each build. Sample voices can suggest chamber/orchestral textures, FM voices offer synthetic timbres, pulses have a narrow palette. Realistic strings/brass or a game's exact patches require appropriate explicit instruments/sample memory; a GM number alone does not provide them.

## Exact source units

Performance has version, title, ticksPerBeat, endTick, tempos, parts and notices. All ticks are absolute integers, not milliseconds. At ticksPerBeat=480, one quarter note lasts 480 ticks. microsecondsPerBeat=500000 means 120 BPM. Note endTick is exclusive and must exceed tick. An intentional rest is the absence of notes, not a fake zero-velocity voice. Distinct part IDs and per-part note IDs make losses traceable.

Each part needs id, name, role, priority and notes. Set part.program for its default instrument; note.program overrides it. Notes need id, tick, endTick, pitch (MIDI semitones), velocity and optionally drum or absolute-tick expression. Do not attach origin to an original GM composition: native patch IDs have different semantics.

Project settings select chip, mix ('auto' or 'authored'), allowLoss, tempoScale, transpose and gain. Full contracts and limits: https://github.com/gwendall/chipvoice/blob/main/docs/CREATION.md.

## Executable original ensemble

In a new directory run npm install chipvoice@0.17.0. Save the following JavaScript as compose.mjs and run node compose.mjs TARGET, using a target ID from the catalogue. It writes project.json, preview.wav and evaluation.json. The eight-bar source has six parts; overlapping strings consume separate voices. No network publication occurs.

```js
// Original eight-bar chamber theme. Run with Node after `npm install chipvoice`.
import { writeFile } from "node:fs/promises";
import {
  projectFromPerformance,
  validateProject,
  renderProject,
  toWav,
  projectCapabilities,
} from "chipvoice";

const ppq = 480;
const part = (id, role, program, priority, importance) => ({
  id,
  name: id,
  role,
  program,
  priority,
  mix: { importance },
  notes: [],
});
const melody = part("melody", "lead", 73, 100, 1);
const strings = part("strings", "chord", 48, 50, 0.6);
const brass = part("brass", "chord", 60, 40, 0.5);
const counter = part("countermelody", "lead", 11, 60, 0.6);
const bass = part("bass", "bass", 42, 90, 0.7);
const drums = part("drums", "perc", 0, 70, 0.5);
function note(p, beat, length, pitch, velocity = 90, drum) {
  p.notes.push({
    id: `${p.id}-${p.notes.length}`,
    tick: Math.round(beat * ppq),
    endTick: Math.round((beat + length) * ppq),
    pitch,
    velocity,
    ...(drum === undefined ? {} : { drum }),
  });
}
const chords = [
  [60, 64, 67],
  [57, 60, 64],
  [53, 57, 60],
  [55, 59, 62],
  [60, 64, 67],
  [53, 57, 60],
  [55, 59, 62],
  [60, 64, 67],
];
const phrases = [
  [72, 76, 79, 76],
  [69, 72, 76, 74],
  [72, 69, 65, 69],
  [71, 74, 79, 77],
  [76, 79, 84, 79],
  [77, 76, 72, 69],
  [74, 71, 67, 71],
  [72, 76, 72, 72],
];
for (let bar = 0; bar < chords.length; bar++) {
  const at = bar * 4,
    chord = chords[bar];
  phrases[bar].forEach((pitch, i) =>
    note(
      melody,
      at + i,
      bar === 7 && i === 3 ? 1 : 0.8,
      pitch,
      i === 0 ? 100 : 86,
    ),
  );
  chord.forEach((pitch) => note(strings, at, 3.7, pitch, 65)); // Three simultaneous voices.
  note(bass, at, 1.8, chord[0] - 24, 82);
  note(bass, at + 2, 1.8, chord[0] - 17, 76);
  if (bar % 2 === 1) note(brass, at + 2, 1.4, chord[2], 72);
  if (bar >= 4 && bar < 7) {
    note(counter, at + 0.5, 0.8, chord[2] + 12, 66);
    note(counter, at + 2.5, 0.8, chord[1] + 12, 60);
  }
  if (bar < 7)
    for (let beat = 0; beat < 4; beat++)
      note(drums, at + beat, 0.2, beat % 2 ? 38 : 36, 58, beat % 2 ? 38 : 36);
}
const performance = {
  version: 1,
  title: "Lantern procession",
  ticksPerBeat: ppq,
  endTick: 32 * ppq,
  tempos: [{ tick: 0, microsecondsPerBeat: 500000 }],
  parts: [melody, strings, brass, counter, bass, drums],
  notices: [],
};
const targets = projectCapabilities();
const chip = process.argv[2] ?? targets[0].id;
if (!targets.some((target) => target.id === chip))
  throw Error("Unsupported chip; inspect projectCapabilities()");
const project = projectFromPerformance(performance, chip);
project.settings.mix = "auto";
project.settings.allowLoss = true; // Audition only: inspect omissions before publishing.
const validation = validateProject(project);
if (!validation.ok) throw Error(JSON.stringify(validation.issues));
const { audio, plan } = renderProject(project, { sampleRate: 44100 });
await writeFile("project.json", JSON.stringify(project, null, 2));
await writeFile("preview.wav", toWav(audio));
await writeFile(
  "evaluation.json",
  JSON.stringify(
    {
      chip,
      seconds: audio.seconds,
      peak: audio.peak,
      sourceNotes: performance.parts.reduce((n, p) => n + p.notes.length, 0),
      playedNotes: plan.notes.length,
      losses: plan.losses,
      mix: plan.mix,
    },
    null,
    2,
  ),
);
console.log(
  "Read evaluation.json and listen to preview.wav. Do not publish unreviewed omissions.",
);
```

The example deliberately enables allowLoss for **audition**, so it runs on small machines and reveals their compromises. Read evaluation.json before sharing. For strict production, set allowLoss=false; voice omissions then reject rendering. This flag does not reject every timbre substitution or certify fidelity.

## Evaluate and iterate

1. Validate the complete source. POST /api/v1/validate takes the **raw project**, not {project}. SDK validateProject works offline.
2. Render an audition with renderProject, or prepareProject in a browser worker (progress/cancellation). Read plan.losses and plan.mix; prepared results expose losses and mix too. HTTP jobs currently expose status/progress/audio, not the full arrangement report: retain your local evaluation.
3. Count omissions by part, identify substitutions, out-of-range and uncalibrated-mix diagnostics. Protect the melody and intentional bass line. Do not hide warnings by merely setting allowLoss=true.
4. Make one explicit musical change, record why, and rerender. Keep the original project as the canonical source and target-specific reductions as separate documents with attribution.
5. Check finite samples, non-silent RMS, clipping, complete duration, section transitions and endings. Compare identical inputs at the same sample rate/engine for determinism. Bit equality across different engines or consoles is not expected.
6. Listen to the full mix and isolated parts, especially dense passages. Measurements do not measure taste. If no listening tool is available, say that auditory judgement remains unverified.
7. For an existing game, compare against an independently identified native reference. Imported MIDI is a transcription, not proof of original instruments or register timing. Untouched native projects preserve original commands; transposition, tempo changes or part isolation create adaptations.

## Validate, publish and render over HTTP

Save the source as project.json. These commands use jq for response extraction. Use a Bearer key already provided by the user; never put it in source control or output it in logs. Read auth routes below if an account needs setup; sending a login email is a separate user-authorized action.

```bash
set -eu
# CHIPVOICE_API_KEY must already contain the user's account key.
: "${CHIPVOICE_API_KEY:?Provide an account key through the environment}"
API_BASE="${CHIPVOICE_URL:-https://chipvoice.dev}"
# Validate the raw project. A 422 response contains path/code/message/level issues.
curl --fail-with-body -sS "$API_BASE/api/v1/validate" \
  -H 'Content-Type: application/json' --data-binary @project.json

# Keep this file for retries of THIS revision. For an intentional new revision,
# choose a fresh working directory or remove request-key.txt before running.
if [ ! -s request-key.txt ]; then
  node -e "console.log(require('node:crypto').randomUUID())" > request-key.txt
fi
REQUEST_KEY=$(cat request-key.txt)
jq '{project: ., visibility: "unlisted"}' project.json > publication.json
curl --fail-with-body -sS "$API_BASE/api/v1/projects" \
  -H "Authorization: Bearer $CHIPVOICE_API_KEY" \
  -H "Idempotency-Key: $REQUEST_KEY" \
  -H 'Content-Type: application/json' --data-binary @publication.json > published.json
PROJECT_ID=$(jq -r '.id' published.json)
curl --fail-with-body -sS "$API_BASE/api/v1/projects/$PROJECT_ID/render" \
  -H "Authorization: Bearer $CHIPVOICE_API_KEY" \
  -H 'Content-Type: application/json' --data '{"kind":"preview"}' > job.json
JOB_ID=$(jq -r '.id' job.json)
ATTEMPTS=0
while [ "$ATTEMPTS" -lt 300 ]; do
  STATUS=$(jq -r '.status' job.json)
  case "$STATUS" in
    ready) break ;;
    failed|cancelled) jq '{status,error}' job.json; exit 1 ;;
    queued|rendering|cancelling) ;;
    *) echo 'Unknown job state'; exit 1 ;;
  esac
  sleep 1
  curl --fail-with-body -sS "$API_BASE/api/v1/jobs/$JOB_ID" \
    -H "Authorization: Bearer $CHIPVOICE_API_KEY" > job.json
  ATTEMPTS=$((ATTEMPTS + 1))
done
[ "$(jq -r '.status' job.json)" = ready ] || { echo 'Polling deadline reached; retain the job ID'; exit 1; }
curl --fail-with-body -sS "$API_BASE/api/v1/jobs/$JOB_ID/audio" \
  -H "Authorization: Bearer $CHIPVOICE_API_KEY" -o published.wav
```

Never assume the ready state immediately. queued, rendering and cancelling are nonterminal; ready, failed and cancelled are terminal. Owner polling also advances the cooperative queue. Honour Retry-After on 429/503; retain the same idempotency key for uncertain identical publication retries. A 409 means a conflicting key/body or state: inspect it, do not blindly change keys and duplicate a publication. A 401 needs authentication; a 422 needs corrected input.

public appears in Explore; unlisted is accessible by link; private is owner-only. To remix, GET the accessible project's full document, edit a copy, and publish with parentId set to the original ID. Source/ready audio are immutable; publishing another revision gets another ID. Software licensing grants no rights to imported music. Keep source credits and set the music's reuse licence deliberately. Never execute somebody else's stored generator code.

Server bodies are capped at 4 MB. preview covers at most 30 seconds; full source is bounded to ten minutes, while server output additionally has a 40 MB ceiling and 240-second worker deadline. Full WAV can fail before ten minutes. This is a bounded cooperative queue, not an unlimited render farm. Browser preparation retains prior audio during updates and applies ready buffers with a crossfade; it is not zero-latency live synthesis.

## Compact legacy songs

The existing /api/validate and /api/songs endpoints accept a different format: bpm, patterns, order, optional chip, title, author and intent. Each pattern has equally long lead/chord/bass/perc token strings and chordShape. Four steps per beat is the default; stepsPerBeat:12 supports triplets. Notes use C4/F#3/Bb2; '.' holds, '=' cuts; percussion uses K/S/H/O. The bass token count determines pattern length. A mistyped note is silent in direct legacy playback, so validate before publishing.

Legacy /api/songs can publish anonymously and provides MP3/WAV URLs. Complete /api/v1/projects requires authentication and pins WAV through jobs. Their bodies, ownership and audio persistence differ: do not mix them. Preserve an existing compact Score with projectFromScore; do not flatten a polyphonic Performance into tracker lines. See OpenAPI for the complete legacy schema. Revalidated legacy audio URLs can change after an engine deployment; only ready project renditions are pinned to stored bytes.

## Endpoint reference

| Method | Path | Purpose |
| --- | --- | --- |
| `GET` | `/api/v1/capabilities` | Discover generated machine voices, instruments and project schema |
| `POST` | `/api/v1/validate` | Validate a complete project without saving |
| `GET` | `/api/v1/projects` | Search public active publications |
| `POST` | `/api/v1/projects` | Publish an immutable project revision |
| `GET` | `/api/v1/projects/{id}` | Fetch an accessible complete publication |
| `DELETE` | `/api/v1/projects/{id}` | Withdraw your publication |
| `PUT` | `/api/v1/projects/{id}/favourite` | Save a public song by another author |
| `DELETE` | `/api/v1/projects/{id}/favourite` | Remove your favourite |
| `POST` | `/api/v1/projects/{id}/report` | Report an accessible publication |
| `POST` | `/api/v1/projects/{id}/render` | Queue an immutable preview or complete WAV |
| `GET` | `/api/v1/jobs/{id}` | Read job status, progress and pinned audio URL |
| `DELETE` | `/api/v1/jobs/{id}` | Cancel your queued or running render |
| `GET` | `/api/v1/jobs/{id}/audio` | Download ready WAV after checking publication access |
| `GET` | `/api/v1/profile` | Your public profile, without email or authentication IDs |
| `PUT` | `/api/v1/profile` | Reserve a unique handle and edit your profile |
| `POST` | `/api/validate` | Check a song without storing it |
| `POST` | `/api/songs` | Store a song and get its links |
| `GET` | `/api/songs/{id}` | Fetch a song |
| `DELETE` | `/api/songs/{id}` | Withdraw a song you published |
| `POST` | `/api/songs/{id}/fork` | Copy a song with changes |
| `GET` | `/s/{id}.mp3` | The audio, rendered on request |
| `POST` | `/api/auth/signin` | Email a single-use browser sign-in link |
| `GET` | `/api/auth/redeem` | Consume the link and set an HttpOnly session cookie |
| `DELETE` | `/api/auth/session` | Revoke the current browser session |
| `DELETE` | `/api/keys/{id}` | Revoke one of your API keys; retain song ownership |
| `GET` | `/api/keys` | List up to 100 account keys without secrets |
| `POST` | `/api/keys` | Get a key, by email |
| `GET` | `/api/me` | The latest 50 songs this account has published |
| `GET` | `/s/{id}.wav` | The same audio, lossless |
