@knod/playback

1.0.1 • Public • Published

PLAYBACK

An event-based tool that allows control of the playback of parsed text. Sends out word fragments at intervals, pauses, rewinds, jumps by sentences, and does other playback jazz.


About


A module for an RSVP (rapid serial visual presentation) reader, mainly for the purpose of displaying the text of a website's article in an accessible way. It has the potential to help people with visual impairment and some kinds of reading difficulties. It can also be an aide to speed reading. The final product will do something like this: http://the-digital-reader.com/wp-content/uploads/2015/09/word-runner-gif.gif


This Module


An event-based tool that allows control of the playback of parsed text.

Given an array of arrays of strings (which represents sentences made up of words), will send out various events with relevant data. The main event is 'newWordFragment' which, among other things, delivers a string of the word fragment* that should be displayed. Using a state object or defaults, calculates a delay between each fragment transmission based on the characteristics of that fragment. It can move forward, backward, and jump to positions. It can move by sentence, word, or fragment.

* When needed, a word gets broken into fragments so it can fit in an element. Some people, for example, have a limited field of vision, so they'll need to set the options of the tool so that words are broken up into smaller fragments.


Getting Started


Downloading


You can use this module in a browser by linking to it in your html page, but, unless you build custom objects, it means you have to get a couple of other modules too. You'll need the module that organizes and keeps track of the positions in the text (https://github.com/knod/prose-stepper) which uses a second module (https://github.com/knod/hyphenaxe) to split words into fragments. You'll need a script that figures out the delay between each new fragment (https://github.com/knod/string-time) and you'll need a fourth module that can emit events (https://github.com/Olical/EventEmitter). Once you've downloaded all the scripts, you can put this in the <head> element of your html page:

<script src='/filepath-to-script/EventEmitter/EventEmitter.js'></script>
<script src='/filepath-to-script/hyphenaxe/lib/hyphenaxe.js'></script>
<script src='/filepath-to-script/prose-stepper/dist/prose-stepper.js'></script>
<script src='/filepath-to-script/string-time/dist/string-time.js'></script>
<script src='/filepath-to-script/playback/dist/playback.js'></script>

If you're using npm then first open your terminal or command prompt, navigate to the right directory (folder), and install the project there with:

npm install @knod/playback --save

Instantiation


Now you're going to need a state object and some parsed text.

var myEmitter = new EventEmitter();

// Instantiate the stuff that state is required to have
var state = {
	emitter: myEmitter,
	stepper: {},
	delayer: {},
	playback: {}
};

// You can get your parsed text from elsewhere as long as it's in
// this format when you hand it to `playback` for processing -
// an array of sentences which are themselves arrays of strings.
var parsedText = [
	[ 'Victorious,', 'you','brave', 'flag.' ],
	[ 'Delirious,', 'I', 'come', 'back.' ],
	[ '\n' ],
	[ 'Why,', 'oh', 'wattlebird?' ]
];

var playback = Playback( state );
// This is a bit annoying, but it makes it possible to give new
// text to `playback` whenever you want.
playback.process( parsedText );

What Now?


Now listen carefully before you tip over the first domino:

var showWord = function ( eventName, playbackInstance, fragment ) {
	console.log( fragment );
};

state.emitter.on( 'newWordFragment', showWord );

playback.play();

Those are the basics! More fancy stuff is described below.


Development


Issue filing welcome. Pull requests are also welcome, but the tests still have to be set up to run automatically. Currently you'll have to run each test script in the console separately and let each finish before moving onto the next. You can run each test individually:

node tests/singles
node tests/combos
node tests/skips
node tests/skip-combos

Or you can run all the tests at once:

npm test

Running all the tests at once takes about 3.5min. and willhave some weird "Unhandled promise rejection" errors (maybe my code or may be the fake-clock module I'm using to make the tests faster and more consistent). The weird promise rejection things are fine. Actual failures should be in red.


API


Definitions/Variables


incrementors

Either an array of three integers or one single integer. There are two ways that incrementors move in the text. They can either navigate to an absolute position ("go to the third word") or to a relative position ("go three words forwards from here"). An array of integers is for relative movement (see number 1 below) while a single integer is for absolute movement (see number 2 below). The examples use .once() because that function uses incrementors most directly, but this is the behavior whenever they are used:

  1. Relative movement: .once( [ int, int, int ] ) will move you backwards or forwards relative to your current position in the text. The first integer says how many sentences to move forward or back, the second integer is for words, and the third integer is for fragments. Positive integers will move forward and negative integers will move backwards. So to move back three words you'd do .once( [ 0, -3, 0 ] ). It only listens to the first non-zero integer. That is, if you send [ -1, 2, 5 ], it'll just move one sentence backwards and ignore the rest. You can also send [ 0, 0, 0 ]` to get the current fragment. Note: If you're in the middle of a sentence or word and you move backwards, the first move is to the beginning of that sentence or word.

  2. Absolute movement: .once( int ) will take you to the position/index in the text that the integer indicates. It acts as if the parsed text is a simple array of words and your integer is being used as an index position. If you try to navigate past the end, you'll just get the first fragment of the last word. If you use a negative number you'll get the first fragment of the first word. In both of those cases, you'll set off the 'done' event and its friends.

progress

A fraction/float that's > 0 and <= 1 representing how far along in the text the current position is.

direction

Negative numbers are treated as going backwards. Positive numbers and 0 are treated as going forwards.

loop

The loop is basically the point of this whole thing. It's like the update loop in a game or animation. It fires events that, among other things, will transmit the fragment. It's events are described below.

queue

Don't mess with the queue. The queue is the way the package lines up functions to be called one after another so they don't interfere with each other.


Playback


The Playback constructor can take from 1 to 3 arguments:

  1. (Required) A state object described below
  2. (Optional) A constructor for an object that controls navigation through parsed text (see the default here: https://github.com/knod/prose-stepper)
  3. (Optional) A constructor for an object that can return a number of milliseconds - the amount of time between the transmission of each new fragment (see the default here: https://github.com/knod/string-time)

state Object Properties


See "Getting Started" above for an example of a state object and instantiation.

The state object passed in to Playback and its mutable properties MUST NOT BE DESTROYED. You can alter its properties non-destructively, but you can't destroy its properties right now. Currently there's no way to set a new state.

REQUIRED state PROPERTIES

.emitter

An event emitter with a .trigger function.

.stepper

Can be an empty object. State properties for the stepper object, all of which are optional. See the default stepper and what can use here: https://github.com/knod/prose-stepper.

.delayer

Can be an empty object. State properties for the delayer object, all of which are optional. See the default delayer and what can use here: https://github.com/knod/string-time.

.playback

Can be an empty object. State properties for this object, all of which are optional and listed below.

OPTIONAL state.playback PROPERTIES

All properties of state.playback that this package can use

.calcDelay( str )

Will be given a string - the current fragment - and should return a millisecond values. That value is the delay till the next fragment is transmitted. It can be calculated based on the characteristics of the fragment. A function for an alternative delay calculation if you're into that. It can control delays between new fragments. By default it will use the "delayer" constructed at the start, either the default one or one you hand in on instantiation. In future this will be given two arguments in instead of one - the Playback instance and the word fragment.

.accelerate( str )

Will be given a string - the current fragment - and should return a millisecond value. Basically a temporary replacement for .calcDelay(). A function that .rewind() and .fastForward() call. It should return the amount of ms delay until the next non-skipping fragment is transmitted. It can be used to do things like return smaller and smaller values, allowing rewinding and fast-forwarding to speed up over time. 'accelerate' is a terrible name for it and will probably change sometime. In future this will be given two arguments in instead of one - the Playback instance and the word fragment.

.transformFragment( str )

YOU CAN USE THIS TO SKIP FRAGMENTS. Will be given a string - the current fragment - and must return a string, either the same one or a different one. Also a terrible name. This won't actually transform the fragment in the array. It'll transform, basically, a copy of the fragment. It is a function that takes a string (the currently calculated fragment) and returns a string. If you return '@@skip@@', that fragment will be skipped. Right now it's just used for skipping, but in future this'll be your last chance to output something different than the current fragment.

.checkRepeat( str )

Will be given a string - the current fragment - and should return a boolean, true or false. It's a function that returns whether the loop should continue to repeat or if it should stop.


Playback Properties For Your Use


.done

Boolean. An indication of progress in the text. It will be true if the text navigation has either reached the end if traveling forward or reached the start if traveling backward. Otherwise, it will be false. It's the ony property that can/should be directly accessed from the outside of the package.


Playback Functions/Methods For Your Use


SETUP

.process( [[str]] )

Takes an array of arrays of strings - your text parsed into an array of sentences which are themselves arrays of strings. This method allows the module to navigate your text. It can be called with new text whenever desired.

TEXT NAVIGATION/FLOW CONTROL

Some may say there are too many functions here. I'd have to agree.

.play()

Starting with the current word fragment, transmit one word fragment at a time at intervals determined by the delay calcuations (see .calcDelay()), moving forward until the last fragment is reached. Fires the 'playBegin' and 'playFinish' events.

.pause()

What it sounds like. Fires the 'pauseBegin' and 'pauseFinish' events.

.toggle()

If playback is paused, stopped, or closed, it'll start it playing. If playback is playing, it will pause. It sets off the events of whatever it callse - 'playBegin' and 'playFinish', etc., for play, and so on.

.fastForward()

Go forwards fast. Sets off the 'fastForwardBegin' and 'fastForwardFinish' events. If you define state.playback.accelerate(), you can use it to return a ms delay value, so you can adjust the speed, or even increase the speed over time, etc. You can see more about it in the 'OPTIONAL state PROPERTIES' section.

.rewind()

Go backwards fast. Sets off the 'rewindBegin' and 'rewindFinish' events. Otherwise, the same as .fastForwards().

.revert()

This one's a tricky concept. playback can revert to either 'play' or 'pause'. It keeps track of what the last action of that type was and returns to it with this function. For example, if you were paused, then you rewound, then you fast-forwarded, and then called .revert(), playback would pause. You can use it to resume playing (or pausing) after doing some other action, like jumping forwards a few words. I know it sounds weird, but I've asked around - if a user was playing and then rewound, they'd expect the app to start playing again. Or at least that's what most people said. This lets you do that.

.restart()

What it sounds like. It fires the 'restartBegin' and 'restartFinish' events. It doesn't fire 'playBegin' or 'playFinish'.

.stop()

Another name for .pause(). It will set off the 'stopBegin' and 'stopFinish' events.

.close()

Another name for .pause(). It will set off the 'closeBegin' and 'closeFinish' events.

.once( [int, int, int] || int )

Takes either an array of three integers or one integer. Sets off the events 'onceBegin', 'onceFinish', and loop events. If playing and pausing aren't enough for you (they weren't for me), you can have complete control of navigation using incrementors. Look at the definition for 'incrementors' for more details.

.once() can do any of the text navigation operations described below. They're there because the arguments for incrementing can annoying and hard to remember. If you're going to use .once(), I suggest picking either relative or absolute movement, but not both. Code gets confusing after a while otherwise.

.current()

Will get the instance to transmit the current non-skipped fragment. Sets off the 'onceBegin', 'onceFinish', and loop events.

.nextWord(), .nextSentence(), .prevWord(), .prevSentence()

What they sound like. They set off the 'onceBegin', 'onceFinish', and loop events.

.jumpWords( int )

Takes a positive or negative int and uses it to move forward or back by word relative to the current position. If you're in the middle of a word, .jumpWords(-1) will go to the beginning of the word. It fires the 'onceBegin', 'onceFinish', and loop events.

.jumpSentences( int )

Takes a positive or negative int and uses it to move forward or back by sentence relative to the current position. If you're in the middle of a sentence, .jumpSentences(-1) will go to the beginning of the sentence. It fires the 'onceBegin', 'onceFinish', and loop events.

.jumpTo( int )

Takes a positive or negative int, though a negative int doesn't do much. Moves to an absolute position the the text. See 'Absolute' navigation in the description for .once(). It fires the 'onceBegin', 'onceFinish', and loop events.

.reset()

Reset values to their starting value, including clearing the queue (queue is in the definitions section). It fires the 'resetBegin' and 'resetFinish' events. Plain old .reset() does get added to the queue itself, so if you're really desparate, use .forceReset() which skips the queue.

.forcReset()

Like .reset() except it fires right away instead of putting itself on the queue and waiting for its turn (queue is in the definitions section). It fires the 'resetBegin' and 'resetFinish' events.

GETTERS & SETTERS

playback.getLength()

Returns the number of words in the text. Can be used to set up a scrubber bar that allows jumping to different parts of the text.

playback.getProgress()

Returns a positive float/fraction that has a maximum value of 1 and that is always greater than 0 (0 < progress <= 1). Can be used to set up a scrubber bar as described above, or can be used to set up a progress bar that shows how far the user has gotten in the text.

playback.getIndex()

Returns a positive integer representing the index of the current word relative to the other words. It's as if the parsed text had been turned into one array of all the words and you were looking for the index number of the current word.

FUTURE

Depending on need, may add .next() and .prev() to move by fragment, and .skip() to move one fragment forward without transmitting it through 'newWordFragment'.


Events


Events allow their listeners to do things like change UI when appropriate (changing button icons, pulsing a symbol, etc.).

All events send the same first two arguments - the event name as the first argument and the Playback instance as the second argument. Some events send further data:

'newWordFragment'

Sends: eventName, playbackInstance, fragmentString, incrementors. Fired when a loop is triggered and the fragment isn't skipped.

'loopSkipped'

Sends: eventName, playbackInstance, skippedFragment. Fired when a loop is triggered and the fragment is skipped. To be changed to 'skippedFragment'.

'progress'

Sends: eventName, playbackInstance, progressFraction. Fired every loop

loop events

Functions that set off a fragment loop - like .play(), .rewind(), and .jumpTo() - can, after transmitting their 'Begin' event, set off a few other events. In the following description, 'foo' is a stand-in for whatever function you called (for .play() it would be 'play'). The events are, in order:

  • 'fooBegin'
  • 'loopBegin'
  • 'loopSkip' or 'newWordFragment'
  • 'progress'
  • 'loopFinish'

Then if the end has been reached when moving forward or the start has been reached when moving backwards:

  • 'stopBegin'
  • 'stopFinish'
  • 'done'

Then always ends with

  • 'fooFinish'

Functions that trigger loops/loop events

  • .play()
  • .toggle() (if internal action state is paused)
  • .fastForward()
  • .rewind()
  • .revert() (if reverts to playing)
  • .restart()
  • .once()
  • .current()
  • .nextWord(), .nextSentence(), .prevWord(), .prevSentence()
  • .jumpWords()
  • .jumpSentences()
  • .jumpTo()

Events that only get triggered internally

  • 'loopBegin/Finish' - Triggered by anything that triggeres a loop
  • 'newWordFragment' - Triggered by anything that triggeres a loop where the fragment is NOT skipped
  • 'loopSkip' - Triggered by anything that triggeres a loop where the fragment IS skipped
  • 'progress' - Triggered by anything that triggeres a loop, skipped or not
  • 'done' - Triggered by anything that triggeres a loop that hits either the start while traveling forwards (0 counts as forwards) or hits the start when traveling backwards

Events that can get triggered externally, but are also sometimes triggered internally

That is, they have directly associated functions, but other functions that are called externally can also indirectly cause these to be triggered internally

  • 'resetBegin/Finish' - Triggered by .reset() and .forceReset()
  • 'playBegin/Finish' - Triggered by .play() and, sometimes, .toggle()
  • 'restartBegin/Finish' - Triggered by .restart() and, when .done is true, .play() and .toggle()
  • 'pauseBegin/Finish' - Triggered by .pause(), .revert(), and someitmes .toggle()
  • 'onceBegin/Finish' - Triggered by .once(), .current(), and anything starting with '.jump', '.next', or '.prev'
  • 'revertBegin/Finish' - Triggered by .revert() and sometimes .toggle()
  • 'stopBegin/Finish' - Triggered by .stop() and anything that causes the playback to finish, either going forward or back

Events that can only be triggered from external calls of their namesakes:

  • 'closeBegin/Finish'
  • 'rewindBegin/Finish'
  • 'fastForwardBegin/Finish'

Dev Events

If you're wondering about '_queued' and '_dequeued', they're events put there for development to simplify testing. Don't mess with the queue. A dequeueing operation will be triggered either when a new function is added to it or when a function called from the queue has finished its synchronous operations.

Versions

Current Tags

  • Version
    Downloads (Last 7 Days)
    • Tag
  • 1.0.1
    2
    • latest

Version History

  • Version
    Downloads (Last 7 Days)
    • Published
  • 1.0.1
    2
  • 1.0.0
    0

Package Sidebar

Install

npm i @knod/playback

Weekly Downloads

2

Version

1.0.1

License

MIT

Last publish

Collaborators

  • knod