dawg / dawg

A DAW built using Electron and the Web Audio API

Home Page:https://dawg.github.io

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Sequencing

jsmith opened this issue · comments

commented

This will describe the problems, requirements and solution to implement sequences, including embedded sequences.

Motivation

A sequence is a core concept of a DAW. The most basic example of a sequence is a sequence of musical notes. Each note has at the bare minimum a start time and end time. A more complex example is the playlist which is also considered a sequence of events. At the time of writing, a playlist may contain a pattern, audio clip or automation clip. The pattern, which sits within the playlist, then becomes an embedded sequence. Furthermore, a pattern can be embedded several times within the same playlist.

Supporting embedded playlists is an essential part of DAWs. The current solution is buggy and involves the use of TransportRepeatEvent, a Tone concept.

Considerations

  1. For extremely complex projects, a playlist could contain hundreds of scheduled patterns. The solution should be efficient.
  2. Sequences can be moved at anytime, even during playback.
  3. We start playback after the start time of a pattern (e.g. in the middle).
  4. Patterns can change length at any time, even during playback.
  5. How will the solution integrate with the sequencer.
  6. All events must be serializable and unserializable.
  7. In addition to a time, events also have a row associated with them. For example, a note has a key and a pattern has a vertical location in the playlist.
  8. They must be cloneable.
  9. If no loop bounds are defined, playback should end at the end of the phrase.

Solution

The solution will tend, for the moment, towards a simpler solution than an efficient solution; however, the implementation will hopefully make it easy for others to optimize in the future. Additionally, before I begin, I want to make the distinction between a sequence and a scheduled sequence. A sequence contains a list of events whereas a scheduled sequence has, at the bare minimum, a sequence, start time, and duration (or end time).

A transport class will an audio class that represents a sequence and hold a list of events. Each event ****will have a start time, duration, an optional onStart callback, an optional onEnd callback, and an optional onTick callback. The transport ****class will provide a method to schedule an event ****that will return an object containing several operations. These include a method to remove that event, set the start time, and set the duration. All of these operations must be handled appropriately and will be discussed below.

type Beats = number;
type Ticks = number;

interface Event {
  startTime: number;
  duration: number;
  onStart?: (currentTick: Ticks) => number;
  onEnd?: (currentTick: Ticks) => number;
  onTick?: (currentTick: Ticks) => number;
}

interface EventController {
  setStartTime(startTime: Beats): void;
  setDuration(startTime: Beats): void;
  remove(): void;
}

interface Transport {
  schedule(event: Event): EventController;
}

Internally, the timeline class will keep the list of elements sorted by start time. At each tick, it will check which events should start. For these events that are start, their onStart method will be called and will be pushed onto a list of active events if at least one of the onEnd or onTick functions are defined. Additionally, all of the active events will then be checked for onTick callbacks. If the end time is equal to the current time (the end time being lesser than the start time is possible if the duration or start time are changed), then the onEnd will be called and the event will be removed. Furthermore, if the start time is greater than the end time, which is possible if start time is changed, then the same actions as above should be taken.

When the start of event is changed, the situation where event the start time is before the current time and the end time has not occurred yet needs to handled. In this situation, it should be pushed onto the active list mentioned above (if it is not already in the list) so that it is checked during the next tick event. When the duration is changed, the same situation needs to be checked. For example, if the current time is 3, the start time of an event is 1 and it's duration time is changed from 1 to 3 (changing the end time to 4), then it would need to be retroactively added to the active event list.

This event object will be retained within a project class representing a scheduled element. This outer class will also contain start time, the duration, and the row (satisfied consideration #7). It will have a method to remove itself from the transport and, crucially, will emit an event after doing so. It will also have methods for serialization and for cloning itself.

interface Scheduled {
  startTime: Beats;
  duration: Beats;
  row: number;
  remove(): void;
  on(event: 'removed', cb: () => void): { dipose: () => void };
}

The sequence class will be a project class that will wrap the transport class. It will have a list of scheduled elements. When a scheduled element is added (ie. by using push or splice), the sequence class will attach itself to the remove event and will remove the element from the list and from the transport when the event is fired. Additionally, elements can be removed from the list using the splice method. This class will be used heavily in the sequencer components (the components which render the piano roll and playlist). For example, the list will be used in a v-for to render all of the elements and the push method will be used to add new elements.

interface Sequence {
  elements: Scheduled[];
}