igorski / efflux-tracker

Browser based music making application driving synthesizers, samplers and effects using both a tracker and piano roll/loop based interface. Supports MIDI controllers and provides both local and cloud based project storage.
https://www.igorski.nl/application/efflux
MIT License
209 stars 23 forks source link
audio-applications audio-engine daw music-maker music-making music-tracker tracker-music vue vuejs web-audio web-midi webaudio webmidi

efflux

MadeWithVueJs.com shield

What is it ?

Efflux is an application that allows users to create music inside their browser. Efflux is a tracker and follows conventions familiar to those who have used anything from Ultimate Soundtracker to Renoise. All sounds are oscillator (or wave table) based and can be routed through an effects signal path, making Efflux a modular synthesis environment, where the synths are driven by the tracker.

Feature list

Sounds cool, but I don't want to build from source, I just want to tinker with this!!

Of course, this was made to allow for instant composition, so let's cut the chatter! You can use the application right now from your web browser by visiting this link.

The Issue Tracker is your point of contact

Bug reports, feature requests, questions and discussions are welcome on the GitHub Issue Tracker, please do not send e-mails through the development website. However, please search before posting to avoid duplicates, and limit to one issue per post.

Please vote on feature requests by using the Thumbs Up/Down reaction on the first post.

Project outline

All source code can be found in the ./src-folder. Efflux is written using Vue, with the audio engine fully written in TypeScript.

Additional folders:

Application actors

The data store is defined in ./src/store/ and its submodules in the ./src/store/modules/-folder. Each part of the application has its own module, these being:

Efflux song model

The model of an Efflux song consists of the following actors (created via their respective factories):

A song contains a list of EffluxPatterns, ordered within a EffluxPatternOrder-list. Each Pattern contains a list of EffluxChannels (one for each available instrument) and a descriptor for the amount of steps the pattern contains (e.g. 16, 32, 64, etc.). The EffluxPatternOrder contains a series of Patterns which define their playback order within the song (patterns can appear multiple times within this list, allowing you to reuse the same pattern without having to manage duplicates).

Each channel inside a pattern contains EffluxAudioEvents. These describe an action that should happen at a given step within a patterns channel. These can be note on/note off instructions or module parameter automations, or both. Each event references an Instrument that it will operate on (as not all events within a single pattern channel necessarily reference the same instrument to allow for more complex compositions).

As hinted above, a song also has Instruments. There are an equal amount of instruments available as there are tracks (the pattern channels). By default each channel references its own instruments. As the instruments in Efflux are synthesizers, each Instrument has a list of InstrumentOscillators which can be individually tuned and configured for playback.

Instruments also reference InstrumentModules. Each of these modules is basically an effects processor. Each instrument can have its output routed through multiple processors before its output is mixed into the master channel (by the AudioService).

All model types are generated through their respected FACTORIES. The factory should be able to create a new instance of the model type, as well as be able to assemble an instance from a serialized version. SERIALIZERS are separate files (to minimize file size of the Tiny player that should only be able to assemble (deserialize) serialized songs).

Efflux song model and Vuex

In Vuex, the song is stored inside its own Vuex store module "song-module.js". The editors bind directly to this reactive model, but for audio playback, an additional Object structure (e.g. EffluxAudioEvent.seq) is used. This separates the data aspect (maintained by the Vuex store mutations) from the audio rendering.

The history module provides custom methods for saving and restoring individual actions for specific sections of a song. While Vuex makes it easy to simply save an entire song upon each mutation, this will consume a lot (!) of memory fast. Which brings us to:

State history

Mutations can be registered in state history (Vuex history-module.js) in order to provide undo and redo of operations. In order to prevent storing a lot of changes of the same property (for instance when dragging a slider), the storage of a new state is deferred through a queue. This is why history states are enqueued by propertyName:

When enqueuing a new state while there is an existing one enqueued for the same property name, the first state is updated so its redo will match that of the newest state, the undo remaining unchanged. The second state will not be added to the queue.

It is good to understand that the undo/redo for an action should be considered separate from the Vue component that is triggering the transaction, the reason being that the component can be unmounted at the moment the history state is changed (and the component is no longer active).

That's why undo/redo handlers should either work on variables in a local scope, or on the Vuex store when mutating store properties. When relying on store state and getters, be sure to cache their values in the local scope to avoid conflicts (for instance in below example we cache selectedInstrument as it is used by the undo/redo methods to update a specific instrument. selectedInstrument can change during the application lifetime before the undo/redo handler fires which would otherwise lead to the wrong instrument being updated.

update( propertyName: string, newValue: any ): void {
    // cache the existing values of the property value we are about to mutate...
    const existingValue = this.getterForExistingValue;
    // ...and the instrument index that is used to identify the instrument containing the property
    const instrumentIndex = this.selectedInstrument;
    const store: Store<EffluxState> = this.$store;
    // define the method that will mutate the existing value to given newValue
    const commit = (): void => store.commit( "updateInstrument", { instrumentIndex, prop: propertyName, value: newValue });
    // and perform the mutation directly
    commit();
    // now define and enqueue undo/redo handlers to reverse and redo the commit mutation for given propertyName
    enqueueState( propertyName, {
        undo(): void {
            store.commit( "updateInstrument", { instrumentIndex, prop: propertyName, value: existingValue });
        },
        redo(): void {
            commit();
        },
    });
}

Audio rendering

The synthesizer itself renders its output by use of the Web Audio API's OscillatorNodes and PeriodicWave. All of this logic is determined by noteOn and noteOff events (similar to the MIDI spec) that is handled by the AudioService. The routing of audio paths, connecting of nodes and effect modules, recording, etc. is handled by the same service, see the ./services/audio/-folder.

When in doubt of how the audio output relates to the visual interface, remember that the Vuex song model defines what it should sound like (as in: defines the properties of the instruments) while the AudioService takes care of the actual sound rendering, outside of Vue.

Wiki

For more in-depth topics, you can consult the Wiki on GitHub.

Build instructions

You will need Node.js in order to run the build scripts and resolve the dependencies.

To build Efflux, first resolve all dependencies using NPM:

npm install

Generate the combined fixtures JSON files by running the following command (note: this only needs to be executed once or when new files are added / modified in the fixtures folder)

npm run fixtures

After which a development mode can be started (which conveniently opens your browser and points it to the correct location at http://localhost:5173) using the following NPM command:

npm run dev

A minified and transpiled production build can be created using the following command:

npm run build

After which the build output is available in the ./dist/-folder.

Unit testing

Unit tests are run via Jest. You can run the tests by using:

npm run test

Unit tests go in the ./tests-folder. The file name for a unit test should equal the file it is testing, but contain the .spec-suffix, e.g. functions.js will have a test file functions.spec.js.

Roadmap

Efflux is considered complete, but you can expect incremental major/minor version bumps as new features are added every now and then.

A consideration for the codebase is migrating from Vue 2 to Vue 3 (to facilitate this, first the Vuex modules must be migrated to Pinia). For the time being the focus is on developing new features rather than on this migration as the end user doesn't care about these things, yo.