A documented, tested, and simple JSON schema for sharing and distributing data about a show in the marching arts. While this schema is maintained by the OpenMarch team, you don't have to write with OpenMarch to use this! With data from this schema, you can -
- Make your own reader app for marching band shows
- Create a drill-modification script that can import and export into other tools
- Develop a custom app for your band's needs (e.g. complex prop staging or light programming)
Think MusicXML, but for coordinates of coordinates of performers!
OpenMarch is an open-source drill writing software for marching bands. This schema defines the standardized data format for storing and exchanging drill show data, including performer positions, page timing, tempo changes, and field configuration.
The main OpenMarchSchema contains:
omSchemaVersion- Schema version string for compatibilitymetadata- Show settings including performance area configurationperformers- Marcher definitions (id, label, section, name)pages- Set/page definitions with timing and beat indicestempoSections- Tempo changes throughout the showcoordinates- Marcher positions in steps from center/frontmeasures- Optional measure markers with rehearsal marks
The OpenMarch Tempo schema (OpenMarchTempoDataSchema) is a subset of the full schema that includes only timing-related data. It has the same field shapes and validation rules for those fields, but omits performers and coordinates.
omSchemaVersion- Schema version string (same as full schema)metadata- Show settings including performance area configurationpages- Set/page definitions with timing and beat indicestempoSections- Tempo changes throughout the showmeasures- Optional measure markers with rehearsal marks
The tempoSections array contains a list of objects with a tempo and numberOfBeats.
This is the core of the tempo data that everything else is based on.
NOTE - There is/should always be a tempo section with a tempo of
0and 1 beat at the start. This is because the first page always has no duration.
This is a simple example of tempo groups:
{
"tempoSections": [
// First Page
{
"tempo": 0,
"numberOfBeats": 1
},
// 8 bars of 120 BPM, assuming 4/4
{
"tempo": 120,
"numberOfBeats": 32
},
// 30 bars of 120 BPM, assuming 4/4
{
"tempo": 144,
"numberOfBeats": 120
}
]}During a tempo change, like an accelerando or ritardando, the tempo typically shifts gradually and then settles at the new tempo -
{
"tempoSections": [
{
"tempo": 0,
"numberOfBeats": 1
},
{
"tempo": 120,
"numberOfBeats": 21
},
// Start ritardando
{
"tempo": 112,
"numberOfBeats": 1
},
{
"tempo": 104,
"numberOfBeats": 1
},
{
"tempo": 96,
"numberOfBeats": 1
},
// Settle into new tempo
{
"tempo": 88,
"numberOfBeats": 36
}
]}It is important to note that the tempo change may not always be exactly even like this. It's up to the developer how much control they want to give the user, but from OpenMarch, tempos like this can be expected from a manual user entry -
{
"tempoSections": [
{
"tempo": 0,
"numberOfBeats": 1
},
{
"tempo": 120,
"numberOfBeats": 20
},
// Start ritardando, unevenly
{
"tempo": 115,
"numberOfBeats": 21
},
{
"tempo": 106,
"numberOfBeats": 1
},
{
"tempo": 97,
"numberOfBeats": 1
},
{
"tempo": 92,
"numberOfBeats": 1
},
// Settle into new tempo
{
"tempo": 88,
"numberOfBeats": 36
}
]}The measures array contains a list of objects with a startBeatIndex, name, and optionally rehearsalMark.
The index is from the 0-based index of the beats from tempoSections.
Note that since the first beat has a tempo of 0, we start the measures from index 1.
This is a simple example of six 4/4 measures -
{
"measures": [
{
"startBeatIndex": 1,
"startBeatIndex": "Measure 1"
},
{
"startBeatIndex": 5,
"name": "Measure 2"
},
{
"startBeatIndex": 9,
"name": "Measure 3"
},
{
"startBeatIndex": 13,
"name": "Measure 4"
},
{
"startBeatIndex": 17,
"name": "Measure 5",
// Optional rehearsal mark. Can be any string
"rehearsalMark": "A"
},
{
"startBeatIndex": 21,
"name": "Measure 6"
}
]}Note how the startBeatIndex's are spaced out by 4.
This is what makes the measures 4/4 in the OpenMarch Universal Standard!
For maximum flexibility, this schema does not enforce the concept of "time signatures."
Rather, it only denotes the tempo of beats + how many of those beats there are (tempoSections), and where the measures start measures.
From there the time signature can be inferred.
You may ask "How can I tell the difference between something like 3/4 and 3/2 then?" The simple answer is - you don't. This schema is not designed to be used in a notation software or DAW, but rather the simplest information needed for a marching arts show.
If a designer wants to change a 4/4 measure to have steps on every half note, they should denote the measure as having two beats per measure.
Mixed meters are supported in this schema, but they may look a bit strange at first glance. Once you see a few examples, it's quite simple.
Say we have 4 measures of 4/4 at 120 BPM, then 4 bars of 7/8 (2+2+3) where the quarter note is 100 BPM -
{
"tempoSections": [
// Starting empty beat
{
tempo: 0,
numberOfBeats: 1
},
// 4 bars of 4/4
// -- m1-m4
{
tempo: 120,
numberOfBeats: 16
},
// 4 bars of 7/8, QN=100
// -- m5
{
tempo: 100,
numberOfBeats: 2
},
{
tempo: 150,
numberOfBeats: 1
},
// -- m6
{
tempo: 100,
numberOfBeats: 2
},
{
tempo: 150,
numberOfBeats: 1
},
// -- m7
{
tempo: 100,
numberOfBeats: 2
},
{
tempo: 150,
numberOfBeats: 1
},
// -- m8
{
tempo: 100,
numberOfBeats: 2
},
{
tempo: 150,
numberOfBeats: 1
}
],
"measures": [
{
startBeatIndex: 1,
name: "Measure 1"
},
{
startBeatIndex: 5,
name: "Measure 2"
},
{
startBeatIndex: 9,
name: "Measure 3"
},
{
startBeatIndex: 13,
name: "Measure 4"
},
// Start of 7/8
{
startBeatIndex: 17,
name: "Measure 5"
},
// three beats after, since 7/8 is technically three "big beats"
{
startBeatIndex: 20,
name: "Measure 6"
},
{
startBeatIndex: 23,
name: "Measure 7"
},
{
startBeatIndex: 26,
name: "Measure 8"
},
]
}When building a detector for if a measure is mixed meter or a tempo change, we use a two-step check. A measure is mixed-meter when -
- There are two distinct tempos in a measure -
t1&t2 - The tempos have a relationship of
n * 1.5- E.g. assuming
t1is the shorter tempo - A measure is mixed meter givent1 * 1.5 == t2
- E.g. assuming
Coordinates use a step-based system. The positive and negative directions are the same as an algebraic coordinate plane.
xSteps: Horizontal position from center- (0 = 50 yard line, negative = audience's left)
ySteps: Vertical position from the y-origin- Configurable as
0 = front sidelineor0 = center - Positive = towards back-field always
- Configurable as
rotation_degrees: Optional facing direction (0 = toward front sideline)
The performance area defines the field layout including:
- Checkpoints for x/y axes (yard lines, hashes, sidelines)
- Step size in inches (e.g., 22.5" for 8-to-5)
- Field dimensions
The OpenMarch Public Schema uses these file formats for saving and sharing shows:
| Extension | Name | Description |
|---|---|---|
.om |
OpenMarch | Uncompressed JSON file containing show data |
.omz |
OpenMarch Zipped | Compressed JSON file for smaller file sizes |
.omt |
OpenMarch Tempo | Timing data only (metadata, pages, tempo, measures); no performers or coordinates |
.omp |
OpenMarch Package (Coming soon) | Compressed archive containing show data plus audio files and images |
Note - the OpenMarch desktop app uses the
.dotsfile extension which is not the same thing. A.dotsan SQLite database file that is highly optimized for editing and interactivity. The main OpenMarch repo includes scripts for converting.dotsfiles to-and-from.omfiles.
npm install @openmarch/schemaThe package supports both .om (raw JSON) and .omz (gzipped) formats. Format is detected automatically when reading.
Reading — pass file bytes as an ArrayBuffer; the reader decompresses gzip when needed and validates the result:
import { fromOpenMarchSchemaFile } from '@openmarch/schema';
// Get bytes from your environment (e.g. Node, Bun, or browser)
const bytes = await file.arrayBuffer(); // or readFileSync(..., { encoding: null })
const schema = await fromOpenMarchSchemaFile(bytes);
// Access the data from the file
console.log(schema.performers, schema.coordinates);Writing — use toOpenMarchFile with compressed: false for .om or compressed: true for .omz:
import { toOpenMarchFile } from '@openmarch/schema';
const bytes = await toOpenMarchFile(schema, { compressed: true }); // .omz
// or
const bytes = await toOpenMarchFile(schema, { compressed: false }); // .om
// Write bytes with your environment's API (e.g. fs.writeFile, Bun.write, or Blob/File in browser)
await Bun.write('show.omz', bytes);The .omt format stores only timing data (metadata, pages, tempo sections, optional measures). Use it when you need to share or edit timing without performer or coordinate data.
import { fromOpenMarchTempoDataFile, toOpenMarchTempoDataFile } from '@openmarch/schema';
// Read .omt file (supports raw JSON or gzipped)
const bytes = await file.arrayBuffer();
const tempo = await fromOpenMarchTempoDataFile(bytes);
// Write .omt file
const out = await toOpenMarchTempoDataFile(tempo, { compressed: false });
await Bun.write('show.omt', out);import { parseOpenMarch, safeParseOpenMarch, isOpenMarch } from '@openmarch/schema';
// Throws on invalid data
const show = parseOpenMarch(jsonData);
// Returns { success: true, data } or { success: false, error }
const result = safeParseOpenMarch(jsonData);
// Type guard
if (isOpenMarch(data)) {
// data is typed as OpenMarch
}import type { OpenMarch, Performer, Page, Coordinate } from '@openmarch/schema';import { OpenMarchSchema, PerformerSchema, PageSchema } from '@openmarch/schema';
// Use for custom validation logic
const performer = PerformerSchema.parse(data);import {
parseOpenMarchTempoDataSchema,
safeParseOpenMarchTempoDataSchema,
isValidOpenMarchTempoData,
} from '@openmarch/schema';
// Throws on invalid data
const tempo = parseOpenMarchTempoDataSchema(jsonString);
// Returns { success: true, data } or { success: false, error }
const result = safeParseOpenMarchTempoDataSchema(jsonString);
// Type guard
if (isValidOpenMarchTempoData(data)) {
// data is typed as OpenMarchTempoData
}Generated JSON Schemas are available at schema.json (full show) and schema-tempo.json (tempo-only) for use with other languages and tools.
bun install # Install dependencies
bun test # Run tests
bun run build # Build to dist/
bun run generate-schema # Regenerate schema.json and schema-tempo.json