Audio Playback with Tone.js
Overview
Tone.js provides browser-based MIDI playback with synthesis, allowing users to hear their notation.
Why Tone.js?
- High-level WebAudio API wrapper
- Built-in Transport for timing and tempo control
- Multiple synthesis methods (samplers, synths)
- Scheduling for precise timing
- Easy MIDI playback
Basic Playback Implementation
import * as Tone from 'tone';
class PlaybackEngine {
private sampler: Tone.Sampler;
private isPlaying: boolean = false;
constructor() {
// Load piano samples
this.sampler = new Tone.Sampler({
urls: {
A0: "A0.mp3",
C1: "C1.mp3",
// ... more samples across piano range
C8: "C8.mp3",
},
baseUrl: "https://tonejs.github.io/audio/salamander/",
}).toDestination();
}
async play(notes: Note[]) {
await Tone.start(); // Required for browser autoplay policy
const now = Tone.now();
notes.forEach((note, index) => {
const time = now + index * 0.5; // 0.5s between notes
this.sampler.triggerAttackRelease(note.pitch, note.duration, time);
});
this.isPlaying = true;
}
stop() {
Tone.Transport.stop();
this.isPlaying = false;
}
}
Playback Controls Component
export const PlaybackControls: React.FC = () => {
const [isPlaying, setIsPlaying] = useState(false);
const [tempo, setTempo] = useState(120); // BPM
const [currentBeat, setCurrentBeat] = useState(0);
const playback = useRef(new PlaybackEngine());
const handlePlay = async () => {
const { score } = useNotationStore.getState();
await playback.current.play(score.measures.flatMap(m => m.notes));
setIsPlaying(true);
};
const handlePause = () => {
playback.current.stop();
setIsPlaying(false);
};
const handleTempoChange = (newTempo: number) => {
setTempo(newTempo);
Tone.Transport.bpm.value = newTempo;
};
return (
<div className="playback-controls">
<button onClick={isPlaying ? handlePause : handlePlay}>
{isPlaying ? 'Pause' : 'Play'}
</button>
<label>
Tempo: {tempo} BPM
<input
type="range"
min="40"
max="240"
value={tempo}
onChange={(e) => handleTempoChange(parseInt(e.target.value))}
/>
</label>
<div>Beat: {currentBeat} / {totalBeats}</div>
</div>
);
};
Timing with Transport
// Schedule notes using Transport for better timing control
Tone.Transport.bpm.value = 120;
notes.forEach((note) => {
Tone.Transport.schedule((time) => {
sampler.triggerAttackRelease(note.pitch, note.duration, time);
}, note.startTime);
});
Tone.Transport.start();
Visual Sync (Cursor Following Playhead)
function syncCursorWithPlayback() {
Tone.Transport.scheduleRepeat((time) => {
const currentPosition = Tone.Transport.seconds;
const currentMeasure = positionToMeasure(currentPosition);
// Update UI to highlight current measure
setActiveMeasure(currentMeasure);
}, "16n"); // Update every 16th note
}
Next Steps
See Data Flow for state management integration.