Is it still possible to open a .tracktionedit created with Waveform?

I’m trying to open a .tracktionedit file exported with Waveform 11.5.18 into the PlaybackDemo and encountering some issues. So far, things unrelated to audio clips seem to work OK (automation, LFO, plugins), but audio clips definitely do not play “out-of-the-box”.

I did some poking around and of course the .tracktionedit from Waveform is using the .tracktion project file which stores the path to the audio clip’s file, which is then referenced in the edit.

I manually edited the .tracktionedit and updated the source attribute of the audio clip to the path of the file on disk. I’m getting some new error of a StretchSegment’s timestretcher not being initialized. I’m not sure if related but its AudioClipBase::ProxyRenderingInfo.mode is set to disabled, even though it appeared to be enabled in Waveform… but, I’m probably getting ahead of myself here. Disabling time stretching for now at least got the edit loading with the audio clip playing properly.

Ideally for my use case I would like to be able to offer Waveform as a creation/editing tool for edits that will be loaded and used with a purpose-built Tracktion Engine app that is a specialized player of sorts.

Is there any way to load edits created in Waveform with audio clip support? I haven’t looked deeply into it, but it looks like originally the Tracktion Engine used the project approach, and then deprecated it, instead using an edit-only approach.

Is there still a supported way to properly open edits from Waveform and will it continue to be supported/function into the future even if it is not the recommended use? If not, is there a way to tell Waveform to write the actual file paths into the .tracktionedit instead of referencing them from the project file?

Thanks for any help with this, it’s actually a big part of my use case.

1 Like

We’re planning to add relative file support to Waveform in the future and transition to that but it’s difficult as we’ll still need to support the old Project based approach as we have thousands of legacy projects in the wild. Figuring out this workflow is actually the tricky bit as we don’t want to add confusion to out customer base.

You can add support to your TE based app though if you support loading .tracktion project files. If you do that, the Project will be able to resolve the source IDs.

Sorry I can’t give any firmer idea of when we’ll get this fully sorted, we’ve currently got a bunch of stuff higher up in the priority list. We could possible add an “Export to relative edit file” in a semi-hidden place to Waveform if you don’t mind telling your customers to use that?

Having a save/export feature that writes paths directly into the edit file from Waveform would be fine for customers as I think I’m going to need an extra step in the workflow to prepare the content for playback in the TE player regardless and they will need to be educated on what that is.

That said, it seems like these two options would suffice if project files can still be opened in TE:

  • the TE player actually just loads a Waveform project + edit
  • I would write a custom “packaging” app that would load the project + edit, read the project’s asset paths and swap them in the edit, and then save the edit out, discarding the project file after this step

My main concern with the idea of project file + edit file would be if some time in the future it is fully deprecated and I’m stuck with a dead end solution. But I’m assuming since Waveform still requires this approach, it will be supported in TE as long as it is needed by Waveform, is that correct?

If so, I can totally find an approach that works as-is. But, my only final question is: is there any example code for what loading a project file + edit file looks like in TE? I found source reference to the ProjectManager class here, but after searching Google, this forum, and the tutorials I didn’t see any working examples.

Cheers.

OK I think I figured this out by poking around in source code and trial and error. Does this seem like a reasonable way to convert edits from Waveform/project-based to edit-only TE?

  const File projectFile = File("...path to project file..");
  auto project = projectManager.createNewProject(projectFile);
  bool filePathsUpdated = false;
  
  // go through all tracks
  auto& trackList = edit->getTrackList();
    
  for (int i = 0; i < trackList.size(); i++)
  {
      auto* track = trackList[i];
  
      // skip non-audio tracks...
      if (!track->isAudioTrack()) continue;
  
      // get the current list of clips for the track
      auto* audioTrack = dynamic_cast<te::AudioTrack*>(track);
      jassert(audioTrack != nullptr);
      auto& clips = audioTrack->getClips();
  
      // go through each clip...
      for (int c = 0; c < clips.size(); c++)
      {
          auto* clip = clips.getReference(c);
          jassert(clip != nullptr);
  
          // if the clip doesn't use a source file, there is nothing to update...
          if (!clip->usesSourceFile()) continue;
          auto& sourceFile = clip->getSourceFileReference();
  
          // get the source file's ProjectItemID and get the ProjectItem from the associated project...
          auto pId= sourceFile.getSourceProjectItemID();
          auto pItem = project->getProjectItemForID(pId);
          jassert(pItem != nullptr);
  
          // update the edit's source file using the project's file path
          sourceFile.setToDirectFileReference(File(pItem->getRawFileName()), true);
  
          // we found at least one path that needed to be updated...
          filePathsUpdated = true;
      }
  }
  
  // If any paths needed updating...
  if (filePathsUpdated)
  {
      // Create a copy of the edit with the directly substituted file paths
      File f = File("...new TE-ready edit file path...");
      auto efo = te::EditFileOperations(*edit);
      efo.saveAs(f, true);
  }

It’s working, but are there any gotchas to look out for here?

I don’t think we’ll be able to remove Project any time soon. If we do, we’ll need a way to smoothly transition old Projects anyway so that should be covered in TE.

Your code looks correct to me. I can’t think of any obvious gotchas but it can probably be written a little more succinctly like:

  const File projectFile ("...path to project file..");
  auto project = projectManager.createNewProject (projectFile);
  bool filePathsUpdated = false;
  
  // go through all tracks
  for (auto audioTrack : te::getAudioTracks (*edit))
  {
    for (auto clip : audioTrack->getClips())
    {
          // if the clip doesn't use a source file, there is nothing to update...
          if (! clip->usesSourceFile())
            continue;

          // get the source file's ProjectItemID and get the ProjectItem from the associated project...
          auto& sourceRef = clip->getSourceFileReference();
          auto pID= sourceRef.getSourceProjectItemID();

          if (auto pItem = project->getProjectItemForID (pID))
          {
            // update the edit's source file using the project's file path
            sourceRef.setToDirectFileReference (File (pItem->getRawFileName()), true);
  
            // we found at least one path that needed to be updated...
            filePathsUpdated = true;
          }
    }
  }
  
  // If any paths needed updating...
  if (filePathsUpdated)
  {
      // Create a copy of the edit with the directly substituted file paths
      File f ("...new TE-ready edit file path...");
      te::EditFileOperations(*edit).saveAs(f, true);
  }

Hope that works (I didn’t actually run it).

1 Like

Thanks for the info. The cleaned up code worked great. I’m happy to start actually digging into the Tracktion Engine; I’ve got a few things I would like to prototype, and it’s been on my roadmap for a bit!

1 Like

Cool thread … as I understand it, this means that if I want to use a similar technique to create Edits with Waveform and then use them (“seamlessly”) in my custom application, I have to include the project folder/ in my app bundle, and when loading the project (.tracktion file?), fixup/convert the paths to relative paths in the EDIT (not the .tracktion project object) before passing into TE for use in my custom application …

Hmm… seems to me that this would be (maybe) an appropriate additional “helper method” to Utilities.h, somewhere? This way it sort of ‘officially’ comes from TE and continues to get supported/maintained/used/refactored as the Project/Edit file distinction gets further work by the TE/Waveform devs?

Anyway, I’m following closely in these footsteps - the plan is to use Waveform to create EDIT’s for each of our 3 plugins, and then load those EDIT’s directly into a custom Plugin host for experimenting … lets see how far away we are from using a Waveform → EDIT → CustomPluginApp export pipeline …

I ended up creating a background thread loader class that inherits juce::Thread and juce::AsyncUpdater so I could load the .tracktionedit file in the background, to avoid the UI taking a huge hit during loading on the message thread. The background loader class looks like this:

    //==============================================================================
    /* Used to load a Tracktion edit in a background thread.
    */
    class TemplateEditLoader : public juce::Thread, public juce::AsyncUpdater
    {
    public:
        TemplateEditLoader(PrismPlayer& p) : juce::Thread("Prism Player Template Loader"), player(p) {}

        void load(const juce::File file, bool playOnLoad = true, const juce::String projectPath = {})
        {
            autoPlay = playOnLoad;
            editFile = file;
            projectFilePath = projectPath;
            jassert(editFile.existsAsFile());
            startThread();
        }

        void run() override
        {
            player.templateEdit = te::loadEditFromFile(player.engine, editFile);
            triggerAsyncUpdate();
        }

        void handleAsyncUpdate() override
        {
            player.finalizeEditTemplateAsync(autoPlay, projectFilePath);
        }

    private:
        PrismPlayer& player;
        juce::File editFile;
        juce::String projectFilePath = {};
        bool autoPlay = true;
        //==============================================================================
        JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(TemplateEditLoader)
    };

The player is a custom class that hosts the te::Engine instance, the edit, and wraps interaction with the loaded edit, which is specific to my project. It has an instance of the background loader class as a member, and calls load() to load a .tracktionedit file.

The main edit file is loaded during run() but some post-load initialization needs to take place back on the message thread, so triggerAsyncUpdate() is called, which invokes handleAsyncUpdate() - this in turn calls a method on the player that does some post-load init:


// Below is a section of player.finalizeEditTemplateAsync() called async after the edit loads in the background thread

...

    // Finish Loading the edit
    jassert(templateEdit != nullptr);
    auto& transport = templateEdit->getTransport();
    transport.setLoopRange({ 0.0, templateEdit->getLength() });
    transport.looping = true;
    transport.ensureContextAllocated();

    // If a project file exists
	if (projectPath.isNotEmpty())
	{
		projectFile = File(projectPath);

		if (!projectFile.existsAsFile())
		{
			juce::AlertWindow::showMessageBoxAsync(juce::MessageBoxIconType::WarningIcon, "Project File Error", "Project file not found:\n" + projectPath);
			return;
		}

		auto project = projectManager.createNewProject(projectFile);

		// go through all clips with source files
		for (auto clip : findAllClipsWithSourceFiles(*templateEdit))
		{
			DBG("Track: " + clip->getTrack()->getName()+ ", Clip: " + clip->getName());
			// get the source file's ProjectItemID and get the ProjectItem from the associated project...
			auto& sourceRef = clip->getSourceFileReference();
			auto pID = sourceRef.getSourceProjectItemID();

			if (auto pItem = project->getProjectItemForID(pID))
			{
				// update the edit's source file using the project's file path
				auto sFilePath = pItem->getRawFileName();

				// Was this a relative path?
				auto sFile = projectFile.getParentDirectory().getChildFile(sFilePath);
				jassert(sFile.existsAsFile());
				sourceRef.setToDirectFileReference(sFile, true);
			}
		}

		projectManager.clearProjects();
	}
...

Those should be the most relevant bits.

If your edit file uses time stretching on any audio clips you’ll need to enable that when you build your TE project or you’ll get the time stretch error I mentioned earlier in the thread.

If you want to use your plug-ins correctly in your TE project, make sure they are on your enabled plug-ins list or they won’t load in your TE project/app:

    // Reference the TE plugin manager
	auto& pluginManager = engine.getPluginManager();
	
	// Get the known plugin list
	auto& knownPluginList = engine.getPluginManager().knownPluginList;

You can add them programmatically through the known plug-in list as seen in the code above, or you can use the build-in UI, similar to AudioPluginHost, which will save an xml settings file for your app with the allowed plug-ins list. Below is a utility function I used to let the user edit that list:

void PlayerToolbar::showPluginList()
{
    auto& engine = player.getEngine();

    DialogWindow::LaunchOptions o;
    o.dialogTitle = TRANS("Plugins");
    o.dialogBackgroundColour = Colours::black;
    o.escapeKeyTriggersCloseButton = true;
    o.useNativeTitleBar = true;
    o.resizable = true;
    o.useBottomRightCornerResizer = true;

    auto v = new PluginListComponent(engine.getPluginManager().pluginFormatManager,
        engine.getPluginManager().knownPluginList,
        engine.getTemporaryFileManager().getTempFile("PluginScanDeadMansPedal"),
        te::getApplicationSettings());
    v->setSize(800, 600);
    o.content.setOwned(v);
    o.launchAsync();
}

FWIW, this was all done before Tracktion Engine 2.0 release, so I’m not sure if there are any breaking changes to get this all to work with 2.0.

I also had to do a bit more custom stuff to be able to open and render plug-in windows within my TE app. DM me if you need some code examples of that, as it’s a bit much to post in the thread.

Cheers.

1 Like

Great thread - thanks for the information, Cymatic.

One (potentially stupid) question though - I don’t see anywhere in the code above where the edit is created/loaded from a projectFile … though that seems to be a pretty important missing step. Are you creating this edit object somewhere else in non-quoted code, by chance?

That is where the edit is created in the run() method of the background thread loader. The loadEditFromFile() method is defined in tracktion_EditFileOperations.h and is a utility method that the Tracktion Engine provides, and it returns a std::unique_ptr<Edit>.

My project actually has quite a bit of project-specific code that I didn’t want to post here so I tried to grab just the most relevant bits to share.

1 Like

Ah, understood - thanks for the detail. I’m avidly consuming the full details of what you’ve sent me, very much appreciated - will follow up as I make progress, too.