A 3D chart preview player for rhythm games like Clone Hero. Renders chart files as an interactive video-like preview using THREE.js.
- Renders
.chartand.midfiles as a 3D highway visualization - Supports 5-fret guitar, 6-fret (GHL) guitar, and drums
- Plays audio files in sync with the visual preview
- Video player-like controls (play, pause, seek, volume, fullscreen)
- Keyboard shortcuts for easy control
- Framework-agnostic - works with React, Angular, Vue, or vanilla JS
- Web Component that can be dropped into any project
- Multiple instance support - run several players simultaneously
- Simple URL-based loading - just provide a URL to a .sng file
- Animated note textures - supports animated WebP textures
npm install chart-previewThe simplest way to use chart-preview is with the Web Component and URL-based loading:
<chart-preview-playerid="player"></chart-preview-player><scripttype="module">constplayer=document.getElementById("player");awaitplayer.loadFromUrl({url: "https://files.enchor.us/abc123.sng",instrument: "guitar",difficulty: "expert",});</script>That's it! The component handles fetching, parsing, texture loading, and rendering.
Load directly from a URL to a .sng file:
import"chart-preview";// Registers the web componentconstplayer=document.querySelector("chart-preview-player");awaitplayer.loadFromUrl({url: "https://files.enchor.us/abc123.sng",instrument: "guitar",difficulty: "expert",initialSeekPercent: 0.25,// Optional: start at 25%});When you've already fetched the .sng file:
constresponse=awaitfetch("https://files.enchor.us/abc123.sng");constsngData=newUint8Array(awaitresponse.arrayBuffer());awaitplayer.loadFromSngFile({sngFile: sngData,instrument: "guitar",difficulty: "expert",});When loading from a folder or file picker:
// From a file input or folder selectionconstfiles=[{fileName: "notes.chart",data: chartFileData},{fileName: "song.ogg",data: audioFileData},{fileName: "guitar.ogg",data: guitarAudioData},];awaitplayer.loadFromChartFiles({
files,instrument: "guitar",difficulty: "expert",});For maximum control, you can pre-process the data yourself:
import{ChartPreview,ChartPreviewPlayer,getInstrumentType,areAnimationsSupported,}from"chart-preview";import{parseChartFile}from"scan-chart";// 1. Parse your chart fileconstparsedChart=parseChartFile(chartData,"chart",modifiers);// 2. Load textures (cache and reuse for same instrument type)consttextures=awaitChartPreview.loadTextures(getInstrumentType("guitar"),{animationsEnabled: areAnimationsSupported(),});// 3. Load the chartawaitplayer.loadChart({
parsedChart,
textures,audioFiles: [audioData],instrument: "guitar",difficulty: "expert",startDelayMs: 0,audioLengthMs: 180000,});import{Component,ViewChild,ElementRef,CUSTOM_ELEMENTS_SCHEMA,}from"@angular/core";importtype{ChartPreviewPlayer}from"chart-preview";import"chart-preview";// Register web component
@Component({selector: "app-chart-preview",template: ` <chart-preview-player #player [attr.volume]="volume" (player-statechange)="onStateChange($event)" (player-error)="onError($event)" > </chart-preview-player> `,schemas: [CUSTOM_ELEMENTS_SCHEMA],})exportclassChartPreviewComponent{
@ViewChild("player")player: ElementRef<ChartPreviewPlayer>;volume=50;asyncloadChart(chartUrl: string,instrument: string,difficulty: string){awaitthis.player.nativeElement.loadFromUrl({url: chartUrl,
instrument,
difficulty,});}onStateChange(event: CustomEvent){console.log("State:",event.detail.state);}onError(event: CustomEvent){console.error("Error:",event.detail.error);}}import{useRef,useEffect}from"react";importtype{ChartPreviewPlayer}from"chart-preview";import"chart-preview";functionChartPreview({ chartUrl, instrument, difficulty }){constplayerRef=useRef<ChartPreviewPlayer>(null);useEffect(()=>{constplayer=playerRef.current;if(!player||!chartUrl)return;player.loadFromUrl({url: chartUrl, instrument, difficulty });consthandleError=(e: CustomEvent)=>console.error(e.detail.error);player.addEventListener("player-error",handleError);return()=>{player.removeEventListener("player-error",handleError);player.dispose();};},[chartUrl,instrument,difficulty]);return<chart-preview-playerref={playerRef}volume="50"/>;}<template>
<chart-preview-playerref="player":volume="volume"@player-statechange="onStateChange"@player-error="onError"
/>
</template>
<script setup>import { ref, onMounted, onUnmounted } from"vue";import"chart-preview";constplayer=ref(null);constvolume=ref(50);asyncfunctionloadChart(url, instrument, difficulty) {awaitplayer.value.loadFromUrl({ url, instrument, difficulty });}functiononStateChange(event) {console.log("State:", event.detail.state);}functiononError(event) {console.error("Error:", event.detail.error);}onUnmounted(() => {player.value?.dispose();});</script>A complete chart preview player with built-in controls.
| Attribute | Type | Default | Description |
|---|---|---|---|
volume | string | "50" | Initial volume (0-100) |
| Property | Type | Description |
|---|---|---|
state | PlayerState | Current player state |
isPlaying | boolean | Whether currently playing |
volume | number | Current volume (0-100) |
currentTimeMs | number | Current playback position in ms |
durationMs | number | Total duration in ms |
isFullscreen | boolean | Whether in fullscreen mode |
| Method | Description |
|---|---|
loadFromUrl(config) | Load from a URL to a .sng file |
loadFromSngFile(config) | Load from raw .sng file data |
loadFromChartFiles(config) | Load from individual files |
loadChart(config) | Load from pre-processed data |
togglePlayPause() | Toggle play/pause |
play() | Start playback |
pause() | Pause playback |
seek(percent) | Seek to position (0-1) |
seekRelative(deltaMs) | Seek relative to current position |
setVolume(volume) | Set volume (0-100) |
toggleMute() | Toggle mute |
toggleFullscreen() | Toggle fullscreen mode |
dispose() | Clean up resources |
| Event | Detail | Description |
|---|---|---|
player-statechange | { state, previousState } | State changed |
player-progress | { percent, currentMs, totalMs } | Playback progress |
player-end | - | Playback ended |
player-error | { error } | Error occurred |
typePlayerState=|"idle"// No chart loaded|"loading"// Loading chart/audio|"ready"// Ready to play|"playing"// Currently playing|"paused"// Paused|"seeking"// Seeking|"ended"// Playback ended|"error";// Error occurred| Key | Action |
|---|---|
Space | Play/Pause |
← | Seek backward 5s |
→ | Seek forward 5s |
↑ | Volume up 10% |
↓ | Volume down 10% |
M | Toggle mute |
F | Toggle fullscreen |
Escape | Exit fullscreen |
The library supports multiple simultaneous players on the same page:
<chart-preview-playerid="player1"></chart-preview-player><chart-preview-playerid="player2"></chart-preview-player><chart-preview-playerid="player3"></chart-preview-player><scripttype="module">constplayers=document.querySelectorAll("chart-preview-player");// Each player can load a different chartawaitplayers[0].loadFromUrl({url: "chart1.sng",instrument: "guitar",difficulty: "expert",});awaitplayers[1].loadFromUrl({url: "chart2.sng",instrument: "drums",difficulty: "hard",});awaitplayers[2].loadFromUrl({url: "chart3.sng",instrument: "bass",difficulty: "medium",});// All can play simultaneouslyplayers.forEach((p)=>p.play());</script>The library uses a shared AudioContext internally to support many players without hitting browser limits.
interfaceLoadFromUrlConfig{/** URL to the .sng file */url: string;/** The instrument to display */instrument: Instrument;/** The difficulty level to display */difficulty: Difficulty;/** Initial seek position (0-1). Defaults to 0 */initialSeekPercent?: number;/** AbortSignal to cancel the fetch operation */signal?: AbortSignal;/** Whether to enable animated textures. Defaults to true */animationsEnabled?: boolean;}interfaceLoadFromSngFileConfig{/** Raw .sng file data */sngFile: Uint8Array;/** The instrument to display */instrument: Instrument;/** The difficulty level to display */difficulty: Difficulty;/** Initial seek position (0-1). Defaults to 0 */initialSeekPercent?: number;/** Whether to enable animated textures. Defaults to true */animationsEnabled?: boolean;}interfaceLoadFromChartFilesConfig{/** Array of files with their names and data */files: {fileName: string;data: Uint8Array}[];/** The instrument to display */instrument: Instrument;/** The difficulty level to display */difficulty: Difficulty;/** Initial seek position (0-1). Defaults to 0 */initialSeekPercent?: number;/** Whether to enable animated textures. Defaults to true */animationsEnabled?: boolean;}interfaceChartPreviewPlayerConfig{parsedChart: ParsedChart;textures: Awaited<ReturnType<typeofChartPreview.loadTextures>>;audioFiles: Uint8Array[];instrument: Instrument;difficulty: Difficulty;startDelayMs: number;audioLengthMs: number;initialSeekPercent?: number;}| Value | Description |
|---|---|
'guitar' | Lead Guitar (5-fret) |
'guitarcoop' | Co-op Guitar |
'rhythm' | Rhythm Guitar |
'bass' | Bass Guitar |
'drums' | Drums |
'keys' | Keys |
'guitarghl' | GHL Guitar (6-fret) |
'guitarcoopghl' | GHL Co-op Guitar |
'rhythmghl' | GHL Rhythm Guitar |
'bassghl' | GHL Bass |
| Value | Description |
|---|---|
'expert' | Expert |
'hard' | Hard |
'medium' | Medium |
'easy' | Easy |
For advanced use cases, you can use the ChartPreview class directly:
import{ChartPreview,getInstrumentType,areAnimationsSupported,}from"chart-preview";// Load textures (cache for reuse)// Optionally disable animations for better performanceconsttextures=awaitChartPreview.loadTextures(getInstrumentType("guitar"),{animationsEnabled: areAnimationsSupported(),// or set to false to always use static textures});// Create previewconstpreview=awaitChartPreview.create({
parsedChart,
textures,
audioFiles,instrument: "guitar",difficulty: "expert",startDelayMs: 0,audioLengthMs: 180000,container: document.getElementById("container"),});// Control playbackawaitpreview.togglePaused();awaitpreview.seek(0.5);preview.volume=0.8;// Listen to eventspreview.on("progress",(percent)=>console.log(`${percent*100}%`));preview.on("end",()=>console.log("Ended"));// Clean uppreview.dispose();The library exports helper utilities for advanced use cases:
import{extractSngFile,fetchSngFile,prepareChartData,findChartFile,findAudioFiles,isVideoFile,areAnimationsSupported,}from"chart-preview";// Check if animated textures are supported (ImageDecoder API)if(areAnimationsSupported()){console.log("Animated note textures will be used");}// Fetch and extract a .sng fileconstsngData=awaitfetchSngFile("https://example.com/chart.sng");constfiles=awaitextractSngFile(sngData);// Find specific filesconstchartFile=findChartFile(files);// Returns .chart or .mid fileconstaudioFiles=findAudioFiles(files);// Returns audio file data// Check if a file is a video (to exclude from processing)constnonVideoFiles=files.filter((f)=>!isVideoFile(f.fileName));// Prepare all data for playbackconstpreparedData=awaitprepareChartData(files,"guitar","expert");# Install dependencies
npm install
# Start dev server
npm run dev
# Build for production
npm run build
# Type check
npm run lint- Chrome 80+
- Firefox 75+
- Safari 14+
- Edge 80+
Requires support for:
- Web Components (Custom Elements v1)
- Web Audio API
- WebGL
Animated Textures: Requires the ImageDecoder API (Chromium-based browsers only: Chrome, Edge, Opera). Use areAnimationsSupported() to check. Other browsers fall back to static textures.
three- 3D renderingscan-chart- Chart parsingparse-sng- .sng file extractioneventemitter3- Event handling
MIT