Project files and REAPER
@toneflix/studio-exchange saves a whole session, audio included, and opens it again. It has two formats:
| Format | File | Plugins | Opens in |
|---|---|---|---|
| Your app's own | .tfstudio, or the extension you choose | Kept, with their settings | Your app, or any app built on Toneflix Studio |
| REAPER | .zip with an .rpp and the audio | Left out | REAPER, on Windows, macOS and Linux, and back in your app |
Pick the app format to keep working later, and REAPER to finish a song in a desktop studio. The format decides whether plugins go along: REAPER hosts VST, AU and CLAP plugins rather than Web Audio Modules, so a REAPER project carries none.
From the toolbar
The toolbar's File menu has:
- Open project…, which takes either format back: an app project, a zip with an
.rppand its audio, or an.rpppicked together with its audio files. - Save as {app} project and Save as REAPER project, which download the file.
- Export mix (WAV).
The openProject and saveProject items put the same actions on buttons of their own; with more than one format, the save button asks which.
new StudioToolbar('#toolbar', {
timeline,
engine,
plugins,
app: {
name: 'Toneflix Music',
id: 'toneflix-music',
version: '2.1',
description: 'Songs made on Toneflix Music',
copyright: '© Toneflix',
url: 'https://toneflix.net',
extension: 'tfmusic',
},
projectName: 'My song',
// Draw waveforms for the audio of an opened project
onProjectSource: (id, buffer) => void waveforms.get(`project:${id}`, () => buffer).then((peaks) => timeline.setSource({ id }, peaks)),
onProjectOpen: ({ format, missing }) => missing.length && alert(`Audio not found: ${missing.join(', ')}`),
});app says who is saving. Its name labels your format in the save menu, its extension names the file, and every detail is written into each project: into project.json in your format, and into REAPER's project notes (View > Project notes). Without it the format is called Toneflix Studio and the files end in .tfstudio. projectFormats: ['app'] or ['reaper'] saves straight to one format with no menu.
What carries over
| App format | REAPER | |
|---|---|---|
| Tracks: names, colours, volume, pan, mute, solo | Yes | Yes |
| Clips: position, length, offset, gain, fades | Yes | Yes |
| Volume and pan automation | Yes | Yes |
| Markers | Yes | Yes; a REAPER region comes back as a marker at its start |
| Tempo, meter and their changes | Yes | Yes |
| Master volume and pan | Yes | Yes |
| Note clips | Yes | Yes, as MIDI items; drums on MIDI channel 10 |
| Instruments and their samples | Yes | No; a MIDI track comes back on the built-in drums or synth |
| Plugins on tracks and the master, with their settings | Yes | No |
| Who saved it | app in project.json | Project notes |
Plugins are saved by URL with their settings, and load again from that URL when the project opens, so the plugin files must still be reachable. Opening a REAPER project clears the plugins that were loaded.
In REAPER, give a MIDI track a virtual instrument to hear it. Coming back, a track whose MIDI items use channel 10 plays the drum kit and any other MIDI track plays the synth. Note times follow the tempo map both ways.
From code
import { openProject, saveProject, fromReaper, toReaper } from '@toneflix/studio';
const { blob, fileName } = await saveProject('app', timeline, engine, { app, name: 'My song', plugins });
const { format, missing } = await openProject(file, timeline, engine, { plugins });
// Or the REAPER project text alone, without audio, on a server or in a worker
const rpp = toReaper(state, { bpm: 120, meter: [4, 4], mixer: engine.mixer.snapshot(), notes: 'Made with Toneflix Music' });
const { state, bpm, meter, mixer, files, notes } = fromReaper(rpp);saveAppProject and saveReaperProject save one format each, and openReaperProject opens REAPER projects only. An app project is a zip with project.json (a ProjectManifest) beside an audio folder of WAV files.
toReaper names each source's file audio/<source>.wav unless you pass file: (source) => path. fromReaper gives every audio file a source id made from its file name, and files maps those ids to the paths in the project. zip(entries) and unzip(bytes) are there too, for packing your own bundles.
MIDI files
Instrument tracks also go out and come back as Standard MIDI Files (.mid), which every DAW and notation app opens. The toolbar's File menu has Export MIDI (.mid) when there is an instrument track, and Import MIDI… when people can add tracks.
Export writes one MIDI track per instrument track with its note clips, drum tracks on channel 10 and the others on channels of their own. The tempo map, meters and markers go into the file's first track. Only the notes a clip plays are written, so the hidden parts of a trimmed clip stay behind, and a clip's gain scales its velocities.
Import adds an instrument track with one note clip for each track and channel in the file, from the playhead. Channel 10 plays the drum kit and the rest play the synth. The notes keep their bars and beats on the project's tempo, so a file made at 90 BPM plays at the project's 120 BPM in step with the rest.
import { exportMidi, importMidi, fromMidi, toMidi, midiClip } from '@toneflix/studio';
const { blob, fileName } = exportMidi(timeline, { name: 'My song' });
const tracks = await importMidi(file, timeline, { at: timeline.time, synth: { id: wamUrl } });
// Or the bytes alone, for example on a server
const bytes = toMidi(state, { bpm: 120, meter: [4, 4], tracks: ['bass'] });
const { bpm, meter, tempo, markers, parts, duration } = fromMidi(bytes);
const clip = midiClip(parts[0], 'bass', 0, timeline.grid, duration);fromMidi times notes in seconds on the file's own tempo map, which comes back as bpm, meter and tempo; pass { grid } to time them on another grid instead. Each part has a name, its channel, drums for channel 10, and its notes. Files timed in SMPTE frames are not read.