Skip to content

A better API

Larpoux edited this page Jan 1, 2021 · 25 revisions

A better API

Overview

The current τ API is pretty good. Simple and powerful. But some times has passed since the introduction of V5.0.0, and it is now time to clean some points.

The main point that must be addressed is probably the concept of Audio Session. Audio Session in τ was a bright idea, ... and finally not a good idea at all 👎 . The major problem is that iOS does support ONLY ONE SESSION PER APP.

Because of that, when the App open several Player, the Audio Session parameters specified in openAudioSession() overwrites the parameters specified by the previous sessions.

Same for the Recorder. Many App will want to open both a Recorder and a Player. But on iOS, only the last openAudioSession() parameters will be kept. This is bad and we must compose with the iOS limitations.

Another point, (always around the concept of Audio Session) is that during openAudioSession() of a Recorder, the App can specify things like SetAudioFocus(DuckOthers) which is completely stupid. DuckOthers, KeepOthers, StopOthers are for the Player. Not the Recorder.

A third, minor point, with openAudioSession() is that this verb returns a Future. The application must not do anything with his Player or Recorder until this Future is completed. Almost ALL the App will have to insert some code to handle that. It would be much more smart for the App not having to manage this inconvenient behavior. Specially when the openAudioSession() is done during initState() (which is very often the case), because initState() cannot await the completion of a Future.

In Fine (this is latin 😆 ) I think that we must throw away this f***ing naughty verb : openAudioSession().


TauRecorder

The constructor

Actually, the constructor does nothing. And I think that it is OK like that. @bsutton insisted that the constructor must open() the Recorder.

I do not agree with him. In Dart there is no Destructors. But a Recorder must be closed when finished with it. It is much more clean to pair close() with open() and not pair close() with a constructor.

Also, much of the time, the constructor is called during a variable initialization. I do not like that a variable initialization does much work (like opening a Flutter Channel and creating a corresponding object in iOS or Android). It is easier to handle exceptions inside a procedure and not in a variable initialization.

And if an App wants to create and open a Recorder, because open() does not return a future, it can now do something very simple :

     myRecorder = TauRecorder().open();

The constructor can have an optional parameter : logLevel. This parameter specify the kind of logs the App is interested by.

It is really important that this parameter is at least "Informations" before submitting a Problem Report to us. We really need logs to understand (and debug) your issues.

open() and close()

open() returns the Recorder itself. So, it is convenient for the App to do cascading :

     myRecorder = TauRecorder().open();
     ...
     ...
     myRecorder.close();

open() and close() have no parameters. As you have already understood, each opened Recorder must be closed. What does open() do is a secret and the App does not need to know that. But I will tell you the truth: the only purpose of open() is to create a new corresponding object inside iOS or Android. That's it. It has nothing to do with Audio Session, Audio Focus and things like that.

It is safe to close() a Recorder in any state. The App can use it to recover a clean state, or to clean everything when leaving a Form.

  • close() will stop any recording in progress. It is not necessary to stop() it before.
  • close() on a already close Recorder is a NO-OP.

closeAllRecorders()

This verb loops on all the open Recorders and closes them. Most of the time, the App has only one recorder, so this verb is not really useful. But it can be elegant to do :

void dispose()
{
      TauRecorder.closeAllRecorders();
      TauPlayer.closeAllPlayers();
      super.dispose();
}

without any supposition on which recorders or players really exist.

This procedure is a static function and is one of the very few verbs which does not imply an open Recorder.

record()

  • If the Recorder is close, an exception is thrown.
  • If the Recorder is currently doing open(), τ will await silently the end of opening.
  • If the Recorder is already recording, τ will stop() it before anything else.
  • On iOS, if the current Session Mode is not Play and Record, the Session Mode is silently changed. It will be restored during stop().
  • τ does not take care of the recording permission. It is the App responsibility to check or require the Recording permission. Permission_handler is probably useful to do that. An exception is thrown on Android if the App does not have the recording permission.

record() parameters :

1. The Encoder

The encoder is now a real object and not any more the Codec enum.

We keep the V5 semantic for this parameter : it is still a combination of a File Format and a real codec. We will change that, later if it will be better to separate the two notions. Actually I think that the current meaning is OK.

Example :

TauEncoder anEncoder = OpusEncoder(bitRate: 12000, format: TFormat.ogg, quality: tQuality.high);
myRecorder.recorder.record
(
     encoder: anEncoder,
     ...
);

2. The input object

On V5, we had several parameters :

  • audioSource
  • bitRate (but not for PCM)
  • sampleRate (but only for PCM)
  • numChannels

And we certainly would had to introduce new parameters to record from WebRTC for example. It is much more clean to pass an object as a record() parameter, and create new object classes when we will want to add new possible inputs. It better now.

Now :

    myRecorder.record
    (
        ...
        from: Microphone(),
        ...
    );

3. The output object

On V5, we had several parameters :

  • toFile:
  • toStream:

And we would had to introduce new parameters when we will want to record to the speaker, for example. It was not OK.

Now :

    myRecorder.record
    (
        ...
        to: TauOutputFile(path: aPath),
        ...
    );

5. The reporting intervals

V5 used a specific procedure setSubscriptionDuration() to specify the delay between two reports. It was not clear for the developer that he/she had to call this verb before starting his/her Recorder. It is more clean to add this parameter during starting the recorder. The default is 0, which means "No reports".

6. The report callback

In V7, we do not use Streams anymore. Using a callback is simpler : the App does not have to manage a subscriptions and be sure to close it when finished. If the App really wants to use a Stream, it will feed his Stream inside the callback. Very simple.

Note: If the App sub-classes the Recorder class, it can override the procedure void report(Recorder theRecorder, Interval progress, Interval duration) instead of using a callback.

isEncoderSupported()

This function does not exist any more. The Encoder constructors return NULL if the encoder is not supported.

Example :

    TauEncoder anEncoder = OpusEncoder(bitRate: 12000, format: TFormat.ogg);
    if (anEncoder == null)
    {
          ...
    }

stop()

Idem V5 stopRecorder(). This verb can be used everywhere. If the Recorder is not open, or if the Recorder is not Recording or Paused it will be a NO-OP. It is safe to call it everywhere, for example when the App is not sure of the current Audio State and want to recover a clean reset state.

pause()

Idem V5 pauseRecorder()

resume()

Idem V5 resumeRecorder()

state, isRecording, isPaused, isStopped, isOpen, getRecordingState()

Idem V5.

If the Recorder is not open, then state will be close. This is a new possible state.

getInputDevices()

This is a static function which returns a list of the possible input devices. The elements of this list can be used as the source for the verb 'record()`.


TauPlayer

The constructor

Actually, the constructor does nothing. And I think that it is OK like that. @bsutton insisted that the constructor must open() the Recorder.

I do not agree with him. In Dart there is no Destructors. But a Recorder must be closed when finished with it. It is much more clean to pair close() with open() and not pair close() with a constructor.

Also, much of the time, the constructor is called during a variable initialization. I do not like that a variable initialization does much work (like opening a Flutter Channel and creating a corresponding object in iOS or Android). It is easier to handle exceptions inside a procedure and not in a variable initialization.

And if an App wants to create and open a Recorder, because open() does not return a future, it can now do something very simple :

     myPlayer = TauPlayer().open();

The constructor can have an optional parameter : logLevel. This parameter specify the kind of logs the App is interested by.

It is really important that this parameter is at least "Informations" before submitting a Problem Report to us. We really need logs to understand (and debug) your issues.

setAudioFocus()

setAudioFocus() is a static function. This verb apply to all the players currently open or which will be further open.

Most of the time, this procedure will be called just before the first TauPlayer.open() to get the focus, and when the last Player is closed to abandon it.

`setAudioFocus has just one parameter, specifying if the App wants to get or to abandoned the focus. If it want to acquire the Focus, this parameter specify what to do with others App :

  • Stop them
  • Duck them
  • Play over them

open() and close()

open() returns the Recorder itself. So, it is convenient for the App to do cascading :

     myPlayer = TauPlayer().open();
     ...
     ...
     myPlayer.close();
  • open() has an optional parameter specifying what to do with the focus

    • Acquire it and stop others
    • Acquire it and duck others
    • Acquire it and play with others
    • Do not acquire the focus
  • open() has an optional parameter : nowPlaying. It is a boolean specifying if τ can show something on the lock screen or on the notification screen when starting the Player.

close() have no parameters. As you have already understood, each opened Recorder must be closed.

If the App does not acquire the Audio Focus, the Audio Focus will be automatically acquire with default values and will be abandoned when the last open Player will close().

It is safe to close() a Recorder in any state. The App can use it to recover a clean state, or to clean everything when leaving a Form.

  • close() will stop any recording in progress. It is not necessary to stop() it before.
  • close() on a already close Recorder is a NO-OP.

closeAllPlayers()

This verb loops on all the open Players and closes them. The App can use it for example when leaving a Form without any supposition on which recorders or players really exist.

void dispose()
{
      TauRecorder.closeAllRecorders();
      TauPlayer.closeAllPlayers();
      super.dispose();
}

This procedure is a static function and is one of the very few verbs which does not imply an open Player.

play()

  • If the Player is close, an exception is thrown.
  • If the Player is currently doing open(), τ will await silently the end of opening.
  • If the Player is already playing, τ will stop() it before anything else.
  • If the Audio Focus does not have been acquired, it will be silently acquired. Then it will be released during stop() of the last Player.

play() returns a Duration Future, which is the record duration (or NULL if the duration cannot be determined).

play() parameters :

1. The Decoder

The Decoder is an object and not anymore a Codec enum. Very often, the codec: parameter is not useful. Flutter Sound will adapt itself depending on the real format of the file provided. But this parameter is necessary when Flutter Sound must do format conversion (for example to play opusOGG on iOS).

Example :

TauDecoder aDecoder = OpusDecoder(format: TFormat.ogg);
myPlayer.play
(
     decoder: aDecoder,
     ...
);

2. The input object

This is either

  • A TauInputFile
  • A TauInputStream
  • A TauInputBuffer

Example

      myPlayer.play
      (
            from: TauInputFile(aPath);
            ...
      );

3. The output object

This is the output device

Example :

      myPlayer.play
      (
          ...
          to: Speaker();
      );

4. The reporting intervals

V5 used a specific procedure setSubscriptionDuration() to specify the delay between two reports. It was not clear for the developer that he/she had to call this verb before starting his/her Recorder. It is more clean to add this parameter during starting the player. The default is 0, which means "No reports".

5. The report callback

In V7, we do not use Streams anymore. Using a callback is simpler : the App does not have to manage a subscriptions and be sure to close it when finished. If the App really wants to use a Stream, it will feed his Stream inside the callback. Very simple.

Note: If the App sub-classes the Player class, it can override the procedure void report(Player thePlayer, Interval progress, Interval duration) instead of using a callback.

6. Seek To

An optional parameter which specify an Interval offset before starting to play.

nowPlaying()

This verb correspond to the V5 function startPlayerFromTrack().

This verb has the same parameters as play() plus the following parameters :

1. Several optional callbacks

  • whenFinished( TauPlayer thePlayer )
  • onSkipForward( TauPlayer thePlayer )
  • onSkipBackward( TauPlayer thePlayer )
  • onPaused( TauPlayer thePlayer )
  • onResumed( TauPlayer thePlayer )

Those callbacks are very convenient, but sometimes it is more elegant to sub-class the TauPlayer class and override the following functions :

  • finished( TauPlayer thePlayer )
  • skipForward( TauPlayer thePlayer )
  • skipBackward( TauPlayer thePlayer )
  • paused( TauPlayer thePlayer )
  • resumed( TauPlayer thePlayer )
  1. removeUIWhenStopped

This a boolean to specify if the UI on the lock screen must be removed when the sound is finished or when the App does a stopPlayer(). Most of the time this parameter must be true. It is used only for the rare cases where the App wants to control the lock screen between two playbacks. Be aware that if the UI is not removed, the button Pause/Resume, Skip Backward and Skip Forward remain active between two playbacks. If you want to disable those button, use the API verb nowPlaying(). Remark: actually this parameter is implemented only on iOS.

3.defaultPauseResume

This a boolean value to specify if Flutter Sound must pause/resume the playback by itself when the user hit the pause/resume button. Set this parameter to FALSE if the App wants to manage itself the pause/resume button. If you do not specify this parameter and the onPaused parameter is specified then Flutter Sound will assume FALSE. If you do not specify this parameter and the onPaused parameter is not specified then Flutter Sound will assume TRUE. Remark: actually this parameter is implemented only on iOS.

Note: if the source is NULL, this verb will display the "nowPlaying" screen without starting the real player.

`getInputDevices()'

This is a static function which returns a list of the possible input devices. The elements of this list can be used as the source for the verb play()

setUIProgressBar()

This verb is used if the App wants to control itself the Progress Bar on the lock screen. By default, this progress bar is handled automaticaly by τ. Remark setUIProgressBar() is implemented only on iOS.

setIosParameters()

This is a static function which can set some iOS parameters. This function is a NO-OP on Android.

Parameters :

  • Category
  • Mode
  • Flags

stop()

Idem V5 stopPlayer(). This verb can be used everywhere. If the Player is not open, or if the Player is not Playing or Paused it will be a NO-OP. It is safe to call it everywhere, for example when the App is not sure of the current Audio State and want to recover a clean reset state.

pause()

Idem V5 pausePlayer()

resume()

Idem V5 resumePlayer()

state, isPlaying, isPaused, isStopped, isOpen, getPlayingState()

Idem V5.

If the Player is not open, then state will be close. This is a new possible state.

seek()

Idem V5

setVolume()

Idem V5

state, isPlaying, isPaused, isStopped, getState()

Idem V5.

If the Player is not open, then state will be close. This is a new possible state.

isDecoderSupported()

This function does not exist any more. The Decoder constructors return NULL if the decoder is not supported.

Example :

    TauDecoder aDecoder = OpusDecoder(format: TFormat.ogg);
    if (aDecoder == null)
    {
          ...
    }

TauHelper

Actually, no change is planned in this module.


TauWidget

My conviction is that this module must be completely rewritten.

Unfortunately, actually I am not very motivated to do that. We will keep the current module for now, but someone must do something soon. Not me.