benwiley4000 / volca-sampler

Send a new sound to your volca sample!
https://volcasampler.com
MIT License
11 stars 0 forks source link

Implement plugin UI and support user params #104

Closed benwiley4000 closed 11 months ago

benwiley4000 commented 1 year ago

We should expose params as a global samplePluginParams in a plugin. We can accept boolean or number params. Not sure if we should specify ranges or not.

The params should remain editable if a plugin unloads so they can be fixed before reloading.

benwiley4000 commented 1 year ago

We should also support sampleImportPlugin, which can return a list of audio buffers.

benwiley4000 commented 1 year ago

All sampleparam numbers are values between 0 and 1

benwiley4000 commented 1 year ago

Plugins can return either AudioBuffer or AudioNode. If the return value is an AudioNode we pipe it to a ScriptProcessorNode that records the output.

We also need to enforce for samplePlugin that the length of the output is the same as the input. Otherwise it gets complicated to track duration.

If the user changes the sample rate of the returned audio buffer for some reason (possible), we should read it into the audio context to get a resampling. We should test to make sure this works by the way. Otherwise use the wave-resampler library.

benwiley4000 commented 1 year ago

I decided not to enforce that the returned audiobuffer needs to have the same length. This would proclude that a plugin that could, for example, timestretch audio, which might be desired.

In order to make this all work, I need to remove the duration cached field, which can be computed at runtime, and replace it with a pluginDurationCoefficient which can be multiplied against the duration to get the untrimmed duration of the processed sample. Whenever this coefficient changes we also need to update the trim frames to a rounded new value as a function of the ratio between the old and new coefficients. One advantage of storing this ratio is that it makes it simpler to decide later to apply plugins post-trim, but to start we can apply plugins before any other processing.

Plugins will be javascript files and we can use their filename as the plugin name. We can uniquely identify plugins with a content hash.

We will need a plugin indexeddb store that includes ids (hashes), filenames, and file content. We can validate hashes against their content after lookup as needed. The plugins should be included in a subdirectory of the export zip.

To the sample metadata, besides the coefficient in the cachedData we will want to add a list of plugins. It can include filenames (unused for lookup but helpful for reading), id hashes, and plugin params for each plugin.

Finally perhaps as a runtime field on the SampleContainer we might want a field for checking if there are any broken plugins. We will avoid allowing playback or transfer until the plugin is fixed or removed.

benwiley4000 commented 1 year ago

For demo sample transform plugins we can have four:

benwiley4000 commented 1 year ago

So for now I'm testing without enabling the audionode return value, instead it's the user's responsibility to use the web audio api. However I have a handful of problems I need to solve:

benwiley4000 commented 1 year ago

I also need to implement the ability to override sample param values

benwiley4000 commented 12 months ago

Other things left to consider are graceful updates when new samples are imported (for names, replacement, params in UI, params in code) to make sure nothing breaks. And we also want to handle batches of many samples sent to the iframe at once (probably)

benwiley4000 commented 12 months ago
When we import a new plugin, we save the filename to a variable PLUGIN_NAME

Now we check if the PLUGIN_NAME exists in the store.

label CONFLICT_RESOLUTION: If the a plugin with the name PLUGIN_NAME exists already:
  If the content hash matches the existing plugin:
    We tell the user that the plugin is already added.
    Exit.

  If the content hash does NOT match:
    We ask if they want to RELACE the existing plugin or else ADD NEW.

    If they want to REPLACE:
      We have to go through all existing references to PLUGIN_NAME and update their content ids.
      Exit.

    If they want to ADD NEW:
      Update PLUG_NAME to NOT match the previous filename.
      Continue.

Now we have a dialog for confirming PLUGIN_NAME and possibly updating it.

Now we check again if a plugin with name PLUGIN_NAME exists.

Go back to CONFLICT_RESOLUTION and repeat as many times as necessary.
benwiley4000 commented 12 months ago

For now I'm not implementing the import plugins yet and not supporting non-number params yet. We'll see what feedback there is from users.

benwiley4000 commented 12 months ago

What's left to do is:

benwiley4000 commented 12 months ago

Fatal flaw of "pluginDurationCoefficient" plan is that it's dependent on the result of running the plugins which happens after the metadata changes, so it will cause the audio computation to run in a loop (possibly stopping after two times if we deal with updates intelligently). Also we will have the same problem with waveformPeaks because it will now rely on the plugin result instead of the source data. The simplest solution is to get rid of the cachedData property and store this data separately.

We could have all of this data returned by the getTargetWavForSample function. The src duration is trivial to have since we already read the source buffer (although at this point we still haven't used it, so we could drop it for now). The waveformPeaks can be computed from the plugin result during the function, and the final duration is easy to know by dividing the final samples length by the sample rate.

It's worth thinking about when each of these two pieces of data is needed. The cached duration is needed for estimating transfer time and file size before a transfer (before all the audio is computed). The cached waveformPeaks are needed by the WaveformDisplay (it also needs the normalize boolean, which we could cache along with the cached info so it's consistent).

There are a few times this cached data needs to be saved:

We need to make sure the SampleList knows when cached data is updated, and the thing that computes sample time, but ideally other parts of the UI aren't disturbed. So we might want a React Context for this.

The final sample metadata upgrade should just correct the old duration value by taking the pitch adjustment into account, save it, and pull that info out of the metadata.

benwiley4000 commented 12 months ago

So to address the previously mentioned issue, we'll need to create a new store sort of like SampleMetadata but called SampleCache. It will contain the cachedInfo property as well as a method called getTargetWavData (or similar) that either returns a cached copy of the wav data or recomputes it, saves it to the cache and returns it. The audio cache will be similar to the one for source file data. We will construct the object using cachedInfo, and its update method, which is async, will take a SampleMetadata, recompute the wav data, cache it, and return a new object with the new cached info.

The map from sample ID to cachedInfo will be managed at the top of the app like the regular samples. We will no longer use the react hook for getting target WAV data, and the getTargetWavData function should be called only from syro utils and from SampleCache.

benwiley4000 commented 12 months ago

A few other notes:

benwiley4000 commented 12 months ago

Looks like we can remove the normalizationCoefficient from waveform peaks, it's not always correct anyway. We'll just compute the sample peaks after normalization in the audio computation function.

benwiley4000 commented 12 months ago

We need to use a pluginless waveform peaks and duration as a backup when there is an error computing the audio. It's important to handle this case when first creating a sample (which will be the default since we won't have any plugins or pitch adjustment), as well as when importing samples (in this case we will try with plugins first, then fall back if needed).

In the case of an update we want to update the failedPluginIndex on fail but otherwise keep the old cachedInfo.

Before I had mentioned maybe needing a property to flag that plugins failed to run. Instead of including this on sample metadata I will include it as part of cacheddata. It can be set on sample update or on import.

benwiley4000 commented 11 months ago

When multi transferring we need to handle an error case for failedPluginIndex.

benwiley4000 commented 11 months ago

Taken care of here https://github.com/benwiley4000/volca-sampler/issues/107