DTX Indies

Browser-side batch converter for Smash Drums .indies — drop DTX song zips or folders, convert all, download each. Everything runs locally; nothing is uploaded.

Drop DTX zips or folders here
click to browse zip files
or browse folder
⚠ If a download doesn’t start in this embedded preview, open the file in a standalone browser tab. The conversion itself already completed locally.

What is a .indies file?

An .indies file is a ZIP archive (just renamed) that Smash Drums reads from its /Indies/ folder. It contains exactly four files at the archive root:

FilePurposeHow this tool produces it
cover.pngSong selection artworkPNG passthrough → transcode other formats → generated if none
audio.oggFull song audioOGG copied byte-for-byte (lossless); MP3/WAV/MP4/FLAC/WebM auto-transcoded to OGG Vorbis (lossy)
preview.wavShort menu previewDecoded from #PREVIEW, or a slice of the song, re-encoded to WAV
meta.jsonAll chart & timing dataBuilt from the DTX headers, notes and BPM map

DTX drum channel → Indies instrument

Smash Drums has six note lanes. DTXMania channels are mapped by their drum-kit semantics:

Indies IdInstrumentDTX channels
0Bass (blue)13, 1C
1Snare (red)12
2Cymbal (yellow)16, 19, 1A
3Tom (green)14, 15, 17
4Hi-Hat (pink)11, 18, 1B
5Clapfire— (not auto-mapped)

meta.json key fields

Field names are case-sensitive (the game deserializes them directly).

  • NameArtist / NameSong / NameCharter — from DTX #ARTIST / #TITLE / #COMMENT
  • SongTiming — beat anchors: each has an integer beat and a float timer (seconds). At least two, ascending.
  • SongPhases — visual intensity phases. A single default Main phase is emitted (phase 2 = verse, power 1).
  • ChartEasy/Normal/Hard/Extreme — note lists. Only ChartExtreme is required; the game generates missing easier charts itself. Which of these are written is governed by the Difficulty slotting selector (see below) — Extreme only emits ChartExtreme alone and lets the game synthesize the rest.

Each note has a float Beat, a Strength (0 crystal / 1 neutral / 2 burning — all notes are emitted as 1) and an Id (0–5 as above).

Difficulty slotting

DTX packs often ship several difficulty files. The toolbar Difficulty slotting selector controls how they map to the four Indies slots. The choice applies to every file in the queue and is reflected live in each card’s preview.

Auto (default) sorts by #DLEVEL and spreads the charts so the hardest is always Extreme:

  • 1 file → Extreme
  • 2 files → Normal Extreme
  • 3 files → Easy Normal Extreme
  • 4 files → Easy Normal Hard Extreme
  • 5+ → four representatives picked across the difficulty range

Extreme only writes just the single hardest chart into ChartExtreme and leaves Easy/Normal/Hard out of meta.json entirely. Because the game generates any missing easier charts itself, this hands the lower difficulties to Smash Drums’ own down-scaling instead of pinning a DTX’s middle charts into fixed slots. Regardless of how many difficulty files the pack ships, the output carries only your top hand-authored chart:

  • any number of files → Extreme (hardest only) — Easy / Normal / Hard generated by the game
Use Extreme only when you trust the game’s generated difficulties more than the pack’s intermediate charts, or when a pack’s easier DTX files are low quality. Use Auto to preserve the charter’s hand-authored easier difficulties. Audio, timing, cover and packaging are identical between modes — only which charts land in meta.json differs.

Audio & timing notes

Smash Drums anchors notes to beats, not seconds, and its SongTiming must start at {beat:0, timer:0.0} — a negative timer is rejected, so a DTX’s delayed-BGM start cannot be expressed as a negative offset. Since v1.6.2 this tool keeps every note’s beat coordinate exactly as authored and preserves the chart’s intended run-up by padding the BGM delay as silence in front of audio.ogg: the timeline then uses plain absolute DTX seconds (timer(b) = beatToSeconds(b)), the first anchor is naturally {beat:0, timer:0.0}, and notes before the BGM onset keep a valid position (no more E20 dropping). The cost is a lossy re-encode when the delay is non-zero (surfaced as E21). For the common no-delay case the OGG is still copied byte-for-byte — no silence padding, no re-encoding, byte-identical output. Verifiable either way. One caveat the tool can only warn about (E22): if the DTX itself uses a fractional #xxx02 bar length (4×len non-integer, e.g. an auto-generated lead-in bar), every later bar line lands on a non-integer beat — notes then sit off the editor’s integer-beat grid even though they play at the correct time. Coordinates are never altered; fix the DTX source if grid alignment matters.
The game only accepts OGG Vorbis for audio.ogg. If a DTX’s BGM is MP3/WAV/MP4/etc., the tool decodes it with the Web Audio API and re-encodes it to OGG Vorbis automatically — surfaced as a lossy re-encode (E07), not done silently. For guaranteed lossless audio, supply a native .ogg BGM. If the browser cannot decode the source, you’ll be asked to convert it manually (E19).

Installing on Quest

Copy your .indies files into Android/data/com.PotamWorks.SmashDrums/files/Indies/ on the headset (via USB/Explorer or SideQuest’s file manager), then pick the “Indies” category in the song menu. Full instructions live at discord.smashdrums.com.

You are responsible for the rights to any audio you distribute as an Indies file. Licensing for shared music is on you, not the game.

Changelog

This page doubles as the version log for the tool. Newest first.

v1.7.2 2026-07-11 · E22 diagnostic: bar lines off the integer-beat grid
  • Added (E22): a new warning fires when a DTX’s #xxx02 bar length makes 4×len non-integer — typically an auto-generated lead-in bar like #00002: 0.577375 (2.3095 beats) used to align measure 001’s downbeat with the audio. Every later bar line then falls on a non-integer beat, so every note’s Beat carries a constant fractional offset (e.g. +0.3095) and sits visibly off the integer-beat anchors/grid in the Smash Drums editor. Audio timing is unaffectedSongTiming interpolates linearly, so notes still play at the correct time; this is a chart-grid cosmetic/authoring issue, not a sync bug.
  • The diagnostic reports the offending bar, its effective length, the resulting grid offset in beats, and how many notes are affected — shown in the parse preview card and the conversion log. Ordinary odd meters are untouched (0.75 = 3 beats, 0.5 = 2 beats are still integer). Detection only; note coordinates are never altered (the keep-beats-as-authored principle since v1.4.0). The proper fix belongs in the DTX source: keep bars full-length and express the audio offset as a delayed BGM chip (absorbed as padded silence, E21) or as leading silence in the audio file itself.
  • Output bytes are unchanged — this release only adds the DtxParser.getBarGridMisalignment() detector, the E22 table entry, warning plumbing in preview/convert, and unit tests for fractional, odd-meter, and realigned-grid cases.
v1.7.1 2026-07-06 · batch memory-peak fix (incremental queue rendering)
  • Fixed (memory, the real one): large batches still spiked Task Manager into the multiple GBs — measured 2.1 GB peak on an 89-file batch in a headless-Chrome harness reading the renderer’s VmRSS (same number Task Manager shows). Crucially the peak scaled with queue length, not per-file work: an 8-file batch of 226 MB packages peaked at only 272 MB, while 89 smaller files hit 2.1 GB — and after a GC the process fell back to ~217 MB (baseline ~150 MB), so it was never a resident leak but a transient-allocation pileup inside GC windows. The cause was never the audio (200 MB×N BGA video never entered memory — the on-demand zip reader works; the transcode path itself showed no growth). It was renderQueue(): on every file completion it did queueEl.innerHTML='' and rebuilt every card in the queue — re-decoding all N cover-image base64 data-URIs into bitmaps and re-emitting all N×4 timeline SVGs. Cost was O(whole queue) per completed file; across 89 files converting every 4–7 s, GC couldn’t keep up with the short-lived strings and decoded bitmaps. Earlier “memory” fixes (v1.5 OfflineAudioContext, v1.6.3 AudioBuffer churn + on-demand zip + blob revoke) all targeted resident memory and the transcode path, so the render-driven peak never moved — which is why they felt like they did nothing on Task Manager.
  • Fix: queue rendering is now incremental. renderQueue() (full rebuild) is called only on structural changes — adding files, removing, clearing, run-state toggles, downloads. During conversion each file’s status change goes through a new upsertCard() that updates only that one card, dropping per-file DOM cost from O(queue) to O(1). Cover <img> nodes are preserved across updates when their src is unchanged (the already-decoded bitmap is reused instead of forcing a re-decode). Cards also get content-visibility:auto so off-screen covers skip decode entirely, and covers carry decoding="async". Output bytes are unchanged — this is a pure rendering-path change, no effect on the produced .indies.
v1.7.0 2026-07-05 · configurable difficulty slotting (exploit the game’s own chart generation)
  • Added: a Difficulty slotting selector in the toolbar with two modes. Auto is the previous behavior — DTX difficulty files are spread across Easy/Normal/Hard/Extreme by #DLEVEL with the hardest always on Extreme (output byte-identical to v1.6.5). Extreme only writes just the single hardest chart into ChartExtreme and omits the easier slots from meta.json, so Smash Drums generates Easy/Normal/Hard itself (meta.json only requires ChartExtreme). This lets a pack’s intermediate DTX charts be discarded in favor of the game’s own down-scaling — useful when the pack’s easier files are weak or absent.
  • The mode applies to the whole queue and is reflected live in every card’s preview: in Extreme only, the Easy/Normal/Hard rows show game-generated from Extreme instead of a note count, so you see the exact charts that will ship before converting. Changing the selector re-parses queued items in place; the selector locks during an active conversion.
  • Only which charts land in meta.json changes between modes — the audio.ogg (still byte-for-byte passthrough when lossless), preview.wav, cover.png, SongTiming and archive packaging are unaffected. The assignDifficultySlots core takes a mode option and defaults to Auto when unspecified (backward compatible); the shared unit suite’s existing slotting assertions are unchanged and a new set covers the Extreme only path.
v1.6.5 2026-07-03 · preview via Ogg page slicing (kills the linear multi-GB ramp)
  • Fixed (memory): after v1.6.4, Task Manager still showed memory ramping linearly into the GBs before a big collection during large batches. The remaining per-file cost was the passthrough preview: to cut a 12s preview.wav, the whole BGM was run through decodeAudioData — a full-length AudioBuffer (~44MB for a 120s stereo track) plus the decoder’s internal transient buffers, per file. Previews are now cut at the Ogg page layer: pages carry absolute granulepos (for Vorbis, the PCM sample index at page end), so the tool splices [header pages] + [pages covering 30%→30%+12s] — verbatim byte copies, CRCs untouched — and decodes only that few-hundred-KB stream. Verified against ffmpeg (the same decoder family Chrome uses for decodeAudioData): the spliced stream decodes warning-free with correct duration, and its samples are bit-exact with the original from 1s into the slice onward, on both a real libvorbis file and a web-encoder-produced file (the first packet is truncated by design — MDCT overlap — absorbed by a one-page lead plus trimming). Falls back to full decode if slicing fails (non-Vorbis, malformed pages). Preview and #PREVIEW decoding also now use the shared OfflineAudioContext.
v1.6.4 2026-07-02 · on-demand zip reads (the actual multi-GB fix for large batches)
  • Fixed (memory + speed): v1.6.3 cut the audio-buffer churn but a real 89-file batch still peaked at ~6GB — because the dominant cost was never the audio: it was JSZip.loadAsync expanding every package in full, once at parse and again at convert, while most of a DTX package’s bytes (keysounds, BGA video) are never used by this converter (measured sample: 45% unused in a 6.4MB pack; video-bearing packs are far worse). Each load produced package-sized short-lived allocations; across a large batch those stack inside GC windows into the multi-GB Task-Manager peak. Zip reading now goes through an embedded on-demand reader: it reads only the end-of-file central directory (KBs) and then File.slices just the requested entries (.dtx, cover, BGM), inflating DEFLATE via the native DecompressionStream('deflate-raw'). Node reproduction, 64MB pack × 89 files: full-expand path 33.8s / +129MB RSS peak; on-demand path 0.3s / +0MB. Entry bytes verified md5-identical to python zipfile for every entry of the real sample (27/27).
  • Compatibility: ZIP64, encrypted or otherwise non-conforming archives automatically fall back to JSZip.loadAsync (previous behavior). Output packaging still uses JSZip. Filenames honor the UTF-8 flag, then strict UTF-8, then Shift-JIS — strictly more correct than the previous UTF-8-only decode for the common Shift-JIS-named DTX packs.
v1.6.3 2026-07-02 · memory-peak reduction (multi-GB peak fix)
  • Fixed (memory): the transcode path allocated up to three full-length AudioBuffers per file (decode → padded copy → another full decode of the just-encoded OGG for the preview slice) plus redundant ArrayBuffer copies. Per 130s stereo file that was ~155MB of short-lived audio buffers — and since these live on Blink’s side (Oilpan GC, reclaimed lazily), batch-converting many files stacked them into a multi-GB process peak in Chrome’s Task Manager. Now: lead-in silence is fed to the encoder as zero blocks (no padded-copy buffer; PCM output verified byte-identical to the copy approach via Node + ffmpeg decode), the preview is sliced from the already-decoded BGM (no second full decode), and source ArrayBuffers are handed to decodeAudioData without a defensive .slice(0) (it detaches its input; byte counts are read beforehand). Net: one full-length AudioBuffer per file instead of three, ~106MB less transient allocation per 130s file.
  • Fixed (resident): parse no longer keeps every queued file’s whole BGM ArrayBuffer in meta for the preview button (N files = N resident BGMs); it stores the zip-entry path and loads lazily on play (files are disk-backed). Re-converting an item now revokes its previous blob URL. A yield is inserted between queue items so the engine gets a collection window. Decoding now reuses a single shared OfflineAudioContext instead of newing one per decode (never-rendered contexts pile up graph/handler resources; measured live on Chrome 151, the per-decode-context pipeline degraded from 15.3s to 32.3s to 45s+ per identical file across a batch).
  • Added: a JS-heap meter in the toolbar (performance.memory, Chrome-only) showing live usage and the session peak during conversion — note it covers V8 heap + ArrayBuffers but not Blink-side audio buffers or blob storage, so Task Manager will read higher. High-frequency encode-progress updates now patch the single card badge instead of re-rendering the whole queue (which re-parsed every base64 cover each tick).
v1.6.2 2026-07-02 · DTX bar-length semantics fix + lead-in preservation
  • Fixed (timing-critical): #xxx02 bar-length now persists until the next change, per the DTX spec — the old parser fell back to 1.0 for every measure without an explicit setting (BMS semantics). Charts that set a length once and let it run (e.g. a 22-measure 0.75 section) had their beat axis inflated, drifting every note off the music — up to 8+ seconds on affected charts — and pushing the final notes past the end of audio.ogg. Charts that never change bar length, or re-state it every measure, were unaffected (which is why most conversions looked fine).
  • Changed: delayed-BGM handling now pads the lead-in as silence in front of audio.ogg instead of clamping it away. A chart that starts its BGM at the tail of measure 000 (a common DTX idiom to give players a run-up) keeps that run-up: the first note no longer arrives at t≈0. Costs a lossy re-encode when the source is OGG (surfaced as E21); the common no-delay case is untouched and stays byte-identical. E20 note-dropping is retired — pre-onset notes now have a valid position in the padded timeline.
v1.6.1 2026-06-27 · folder scan now picks up nested .zip song packages
  • Fixed: dropping or browsing a folder that contained .zip song packages (instead of loose SET.def/.dtx files) silently ignored every nested zip. The recursive walk did read them into the file map, but the song-package detector (findSongPackages) only treated a folder as a song when it directly held a SET.def or a .dtx.zip was never a recognised package type, so a folder full of zips resolved to zero packages and surfaced as E17 No DTX song found in folder.
  • The folder and zip code paths only ever forked at the top-level dropped item; once a directory was entered, nothing looked at .zip again. The folder branch now peels every .zip out of the file map first and enqueues each as its own standalone zip package (loaded through JSZip.loadAsync, identical to a top-level dropped zip); the remaining non-zip files are then passed to the loose SET.def/.dtx detector. Loose song folders and nested zips can now coexist in a single drop — both are picked up, and E17 only fires when neither is present.
  • Because zips are removed before loose detection runs, a nested zip is never absorbed into a sibling loose folder’s file list. The DTX parsing, conversion pipeline, audio handling and output bytes are unchanged from v1.6.0 — this is purely an input-dispatch fix.
v1.6.0 2026-06-26 · MessageChannel yield (encode no longer throttled in background)
  • The encode loop’s event-loop yield was switched from setTimeout(0) to a MessageChannel message. Background tabs clamp setTimeout to ~1s (and to ~once/min after 5 minutes hidden); with the encoder yielding every 32 blocks, v1.5.0 fixed decode but encoding still crawled to the point of being unusable in a background tab.
  • MessageChannel message tasks are not on the background wake-up throttling list (the same trick React’s scheduler uses). The yield still hands back a macrotask boundary so a foreground tab stays responsive, while a background tab runs at full speed.
  • With this, the convert path has no remaining background-throttled step: decode runs on OfflineAudioContext’s decode thread, encode yields via MessageChannel, and JSZip compresses synchronously without timers. A backgrounded tab now drains the whole queue. Output bytes are unchanged from v1.5.0 (swapping the yield mechanism doesn’t affect the encoded result). The toolbar batch-download still uses a 400ms setTimeout gap (background downloads are separately limited by the browser) — unrelated to conversion, left as-is.
v1.5.0 2026-06-26 · OfflineAudioContext decode (background-tab fix)
  • BGM transcode now decodes via OfflineAudioContext instead of the shared realtime AudioContext. A backgrounded tab has its realtime context auto-suspended by the browser, so the old path could stall on await ctx.resume() (that promise never resolves without a user gesture while the tab is hidden) and on decodeAudioData against a suspended context — freezing the whole convert queue until the tab returned to the foreground.
  • An OfflineAudioContext has no “waiting for hardware resume” state and runs decoding on a separate decode thread, so it is not subject to Page Visibility throttling. The convert queue now keeps draining in a background tab through the decode stage.
  • The offline context reuses the hardware output sampleRate (read as a property, which does not require resume); decodeAudioData resamples to the context rate exactly as before, so on a given machine the decoded PCM — and therefore the OGG bytes — match v1.4.0. Decode-only fix: the encoder’s setTimeout(0) yields are still on the main thread and still throttled in the background, so encoding stays slow (not frozen) while the tab is hidden.
v1.4.0 2026-06-26 · delayed-BGM timing fix (anchor shift)
  • Reworked the delayed-BGM fix from a chart shift into an anchor shift. The v1.2–v1.3 approach moved the whole chart in beat space (every note’s Beat minus β, the BGM’s first-chip beat) so the BGM onset landed on beat 0. That mutated note coordinates and — whenever β wasn’t a whole number of beats — knocked on-grid notes onto fractional beats, corrupting the authored rhythm grid.
  • Now note beats are never moved; they stay at their original DTX absolute beat. The delay is absorbed purely into the SongTiming map: timer(b) = beatToSeconds(b) − δ (δ = BGM start time). The first anchor is still exactly {beat:0, timer:0.0}; pre-onset timers (which would be negative) are clamped to 0, and a ⌊β⌋ zero-anchor plus a ⌈β⌉ exact-anchor compress the bridge segment so post-onset interpolation is exact (zero error for whole-beat β).
  • Notes before the BGM onset are still filtered out and reported as E20 (no audio to play against), but surviving notes keep their coordinates. The no-delay case (β=0) is a no-op — output is byte-identical to v1.3.0. Audio pipeline, CJK romaji prefixing and batch download are unchanged.
v1.3.0 2026-06-26 · batch download button
  • Added a Download all button to the toolbar. It fires a download for every converted item in turn, with a 400 ms gap between each so the browser doesn’t collapse the burst into a single file. Each song is delivered as its own standalone .indies — nothing is re-zipped into one combined archive.
  • Because it triggers multiple downloads, an embedded preview / Artifact iframe sandbox will block all but (at most) the first one — open this file in a standalone browser tab for the batch download to work, exactly as the existing download notice already advises for single files.
  • Pure UI/download change: the DTX parsing, conversion pipeline, audio handling and output bytes are unchanged from v1.2.0.
v1.2.0 2026-06-26 · delayed-BGM timing fix (chart shift)
  • Fixed delayed-start BGM timing. Smash Drums requires SongTiming to begin at {beat:0, timer:0.0} and rejects a negative timer; the previous build encoded a BGM start offset directly into the anchors, which produced a negative first timer for any delayed BGM and was refused by the game.
  • The offset is now absorbed by shifting the entire chart in beat space so the instant the BGM starts becomes the new beat 0. audio.ogg is still copied byte-for-byte (no silence padding, no re-encode), and the first anchor is always exactly {beat:0, timer:0.0} with all subsequent timers non-negative.
  • Notes scheduled before the BGM onset have no audio to play against and are dropped, reported as E20 with a count. The no-delay case is a no-op — output is byte-identical to v1.1.0.
v1.1.0 2026-06-24 · auto-transcode non-OGG BGM
  • Non-OGG BGM (MP3/WAV/MP4/FLAC/WebM) is now transcoded automatically to OGG Vorbis instead of being rejected: decode via the Web Audio API, encode with an inlined libvorbis (asm.js) build — fully client-side, no extra network requests.
  • The lossy re-encode is always surfaced explicitly: an amber warning in the parse preview, an E07 warning in the conversion log, and a transcoded-size readout. OGG BGM still passes through byte-for-byte (lossless), unchanged.
  • If the browser cannot decode the source audio, conversion falls back to a clear E19 message asking you to convert to .ogg manually, rather than emitting an unplayable Indies file.
v1.0.0 2026-06-24 · initial release
  • DTX → Smash Drums .indies conversion: meta.json, audio.ogg, preview.wav, cover.png.
  • Beat-accurate SongTiming anchors with full per-segment BPM-change support; BGM offset handled via anchors (no audio re-encode).
  • OGG BGM is copied byte-identical (lossless); non-OGG BGM (MP3/WAV/MP4/FLAC/WebM) is decoded with the Web Audio API and re-encoded to OGG Vorbis in-browser, clearly flagged (E07) as a lossy re-encode. If decoding fails, you are asked to convert it manually (E19) instead of shipping a broken file.
  • Configurable multi-difficulty slotting (toolbar selector): Auto spreads charts across Easy/Normal/Hard/Extreme by #DLEVEL (hardest always Extreme), or Extreme only emits just the hardest chart and lets the game generate the easier ones.
  • preview.wav rendered with the Web Audio API (decode → PCM → WAV); preview playback uses an AudioBufferSourceNode, no <audio> element.
  • Cover handling: PNG passthrough, transcode of other image formats, or a generated gradient cover when none is present.
  • Batch queue: drag-and-drop multiple zips or whole folders (SET.def and loose .dtx both supported), parse-on-add, pause/resume conversion, duplicate detection.
  • Runs entirely client-side; JSZip and the libvorbis OGG Vorbis encoder load from CDN (cdn.jsdelivr.net/npm).
Conversion core test status

The DTX parsing and meta-building logic ships as a single shared module covered by an automated suite (81 unit assertions) plus an end-to-end pass that builds a real .indies from a generated OGG/DTX zip and verifies the archive structure, OGG passthrough and WAV header with ffmpeg (48 assertions).