Skip to content

Project files and REAPER ​

@toneflix/studio-exchange saves a whole session, audio included, and opens it again. It has two formats:

FormatFilePluginsOpens in
Your app's own.tfstudio, or the extension you chooseKept, with their settingsYour app, or any app built on Toneflix Studio
REAPER.zip with an .rpp and the audioLeft outREAPER, 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 .rpp and its audio, or an .rpp picked 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.

ts
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 formatREAPER
Tracks: names, colours, volume, pan, mute, soloYesYes
Clips: position, length, offset, gain, fadesYesYes
Volume and pan automationYesYes
MarkersYesYes; a REAPER region comes back as a marker at its start
Tempo, meter and their changesYesYes
Master volume and panYesYes
Note clipsYesYes, as MIDI items; drums on MIDI channel 10
Instruments and their samplesYesNo; a MIDI track comes back on the built-in drums or synth
Plugins on tracks and the master, with their settingsYesNo
Who saved itapp in project.jsonProject 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 ​

ts
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.

ts
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.

Released under the MIT License.