audiojs / audio

Class for high-level audio manipulations [NOT MAINTAINED]
MIT License
240 stars 9 forks source link

Anync vs sync API #24

Closed dy closed 7 years ago

dy commented 7 years ago

We are facing presumably a problem of sync/async API here.

For example we create audio from remote url. What should happen if we instantly apply manipulations?

let audio = Audio(url);
audio.trim();
//is audio trimmed here? obviously not, as it is not loaded yet.
//should we plan it to be trimmed once it is loaded?
//or should we return error because we cannot trim not loaded data?

Planning reminds of jQuery pattern, where things get queued and applied in turn. The difference is that we aren’t (necessarily) bound to RT queue, therefore can do things at instant. Unless we expand Audio to a stream-of-chunks wrapper, which is a different story. Planning forces us to provide a callback for every manipulation method, which is bad for simple use and good for worker use.

1. Async way

jQuery/webworker classical async processing way.

let audio = Audio(url);
audio.trim().fadeIn(.5, (audio) => {
  //audio is trimmed/faded here
});
//here audio is not ready yet, but trim/fade are queued

✔ enables webworker mode, freeing UI from heavy processing of large data ✔ does not break natural workflow, the code style is sync but running of it is async ✔ enables partially loaded data, like streams (potentially) ✘ makes workflow more difficult ✘ mb a tiny bit slower than sync way ✘ a bit unconventional API, considering possible promises style:

Audio(url)
  .then(a => a.trim())
  .then(a => a.fadeIn(.5))
  .then(a => a.download())

2. Sync way

This way is suggested in the zelgo article.

let audio = Audio(url);
audio.trim(); //throws error
audio.on('ready', () => {
  audio.trim().fadeIn(.5);
});

✔ easy API ✘ blocking processing, esp. in case of large audio files

@jamen how do you think which one is better?

ahdinosaur commented 7 years ago

Unless we expand Audio to a stream-of-chunks wrapper, which is a different story.

:smile:

jamen commented 7 years ago

Hmm. I think the webworker mode would allow for some really cool stuff, but at the same time like how the sync API looks. I would probably be more towards the async way.

dy commented 7 years ago

Actually rethinking that idea of a wrapper over a sequence of chunks, actually I like it. That allows for (weird?) things like distributed storage, faster processing, mb parallel processing in some cases, partially loaded data. Also that makes API easier and more natural. I’ve updated how basic use-cases may look in readme - pretty terse.

dy commented 7 years ago

Afterthoughts.

Real use-cases help to understand natural expectation of the API.

Async API makes simple things vague. Audio('./src.mp3').fadeIn(1) in not obvious construct and raises questions: will fadeIn be applied once data is loaded, to empty file or be queued? how we can track if source has loaded? What if loaded length is less than indicated by fadeIn? Natural expectation is that the emty audio instance will be faded in instantly, and then the resource will be loaded and overwrite it. Better make this mechanism explicit: Audio().load('./src.mp3', audio => audio.fadeIn(1)), or promise.

Also async API profits do not overweight hurdles using it. For webworker mode it is better to create Audio instance within webworker and provide some AudioStorage or alike interface. Reference implementation is in gl-waveform/storage, practically ok.

dy commented 7 years ago

So let's better make async API a wrapper to initial simple container, if it ever needed. Closing for now.

jamen commented 7 years ago

Sounds good.

I was thinking, what if we provide the audio-* components nearly unaltered as methods, except the .load method would get the buffer behind the scenes, and they could just check the buffer var to see if it is ready first:

var audioPlay = require('audio-play')

function audio (source, ...) {
  // We store buffer here
  let buf = null

  // load buffer depending on `source`
  function load () {
    buf = ...
  }

  // Wrap modules (in this example audio-play)
  function play (...args) {
    // Check that buffer is loaded
    if (buf) return audioPlay(buf, ...args)
    else throw new Error('Buffer is not loaded yet')
  }

  // ... wrap other modules same way.

  return { load, play, ... }
}

Something similar to this with more async stuff.

jamen commented 7 years ago

I kind of overlooked that with audio-play.

But with the components where you call it with var fn = audioComponent(options), then change buffer with fn(buf), you could do that.

dy commented 7 years ago

Yes, most methods are likely be just direct audio-* components. Though I would create empty buffer rather than null, because it might be useful to create audio from scratch, feels more natural not having states (ready/not ready).