migrated to vue wtf
This commit is contained in:
465
src/Beat.ts
465
src/Beat.ts
@@ -1,6 +1,6 @@
|
||||
import Track, { TrackEvents, TrackInitOptions } from "@/Track";
|
||||
import { deserialise as deserialiseTrack, type TrackInitOptions, createTrack, isValidTimeSigRange, type Track } from "@/Track";
|
||||
import { greatestCommonDivisor, isPosInt } from "@/utils";
|
||||
import { Capsule, ICapsule, IPublisher, ISubscriber, Publisher } from "@djledda/ladder";
|
||||
import { watch, computed, effectScope, ref, shallowRef, triggerRef } from "vue";
|
||||
|
||||
type BeatGroupInitOptions = {
|
||||
barCount: number;
|
||||
@@ -23,337 +23,182 @@ export type BeatSerial = {
|
||||
name: string,
|
||||
};
|
||||
|
||||
export const enum BeatEvents {
|
||||
TrackOrderChanged="be-0",
|
||||
TrackListChanged="be-1",
|
||||
BarCountChanged="be-2",
|
||||
TimeSigUpChanged="be-3",
|
||||
AutoBeatSettingsChanged="be-4",
|
||||
LockingChanged="be-5",
|
||||
GlobalLoopLengthChanged="be-5",
|
||||
GlobalDisplayTypeChanged="be-6",
|
||||
DeepChange="be-7",
|
||||
function isBeatSerial(serial: Record<string, unknown>): serial is BeatSerial {
|
||||
return Array.isArray(serial.tracks) &&
|
||||
typeof serial.barCount === "number" &&
|
||||
typeof serial.timeSigUp === "number" &&
|
||||
typeof serial.globalLoopLength === "number" &&
|
||||
typeof serial.globalIsLooping === "boolean" &&
|
||||
typeof serial.useAutoBeatLength === "boolean" &&
|
||||
typeof serial.barSettingsLocked === "boolean";
|
||||
}
|
||||
|
||||
const EventTypeSubscriptions = [
|
||||
TrackEvents.LoopLengthChanged,
|
||||
TrackEvents.DisplayTypeChanged,
|
||||
TrackEvents.WantsRemoval,
|
||||
TrackEvents.DeepChange,
|
||||
TrackEvents.Baked,
|
||||
];
|
||||
type EventTypeSubscriptions = typeof EventTypeSubscriptions[number];
|
||||
|
||||
export default class Beat implements IPublisher<BeatEvents>, ISubscriber<EventTypeSubscriptions> {
|
||||
private static globalCounter = 0;
|
||||
private tracks: Track[] = [];
|
||||
private publisher: Publisher<BeatEvents, Beat> = new Publisher<BeatEvents, Beat>(this);
|
||||
private barCount: number;
|
||||
private timeSigUp: number;
|
||||
private globalLoopLength: number;
|
||||
private globalIsLooping: boolean;
|
||||
private useAutoBeatLength: boolean;
|
||||
private barSettingsLocked = false;
|
||||
private name: ICapsule<string>;
|
||||
|
||||
constructor(options?: BeatGroupInitOptions) {
|
||||
Beat.globalCounter++;
|
||||
if (options?.name) {
|
||||
this.name = Capsule.new<string>(options.name);
|
||||
} else {
|
||||
this.name = Capsule.new<string>(`Pattern ${Beat.globalCounter}`);
|
||||
}
|
||||
if (options?.tracks) {
|
||||
for (const trackOptions of options.tracks) {
|
||||
this.addTrack(trackOptions);
|
||||
}
|
||||
}
|
||||
this.barCount = options?.barCount ?? 4;
|
||||
this.timeSigUp = options?.timeSigUp ?? 4;
|
||||
this.globalLoopLength = options?.loopLength ?? this.timeSigUp;
|
||||
this.globalIsLooping = options?.isLooping ?? false;
|
||||
this.useAutoBeatLength = options?.useAutoBeatLength ?? false;
|
||||
export function deserialise(serial: {}) {
|
||||
if (!isBeatSerial(serial)) {
|
||||
throw new Error("Not a valid beat serial");
|
||||
}
|
||||
const newBeat = createBeat({
|
||||
loopLength: serial.globalLoopLength,
|
||||
barCount: serial.barCount,
|
||||
isLooping: serial.globalIsLooping,
|
||||
name: serial.name,
|
||||
timeSigUp: serial.timeSigUp,
|
||||
useAutoBeatLength: serial.useAutoBeatLength,
|
||||
});
|
||||
serial.tracks.forEach(trackSerial => {
|
||||
const track = deserialiseTrack(trackSerial);
|
||||
if (track) newBeat.tracks.value.push(track);
|
||||
});
|
||||
return newBeat;
|
||||
}
|
||||
|
||||
static deserialise(serial: any): Beat {
|
||||
if (!Beat.isBeatSerial(serial)) {
|
||||
throw new Error("Not a valid beat serial");
|
||||
}
|
||||
const newBeat = new Beat({
|
||||
loopLength: serial.globalLoopLength,
|
||||
barCount: serial.barCount,
|
||||
isLooping: serial.globalIsLooping,
|
||||
name: serial.name,
|
||||
timeSigUp: serial.timeSigUp,
|
||||
useAutoBeatLength: serial.useAutoBeatLength,
|
||||
});
|
||||
serial.tracks.forEach(trackSerial => newBeat.addTrack(Track.deserialise(trackSerial)));
|
||||
return newBeat;
|
||||
}
|
||||
|
||||
notify(publisher: unknown, event: EventTypeSubscriptions): void {
|
||||
switch (event) {
|
||||
case TrackEvents.LoopLengthChanged:
|
||||
case TrackEvents.DisplayTypeChanged:
|
||||
this.autoBeatLength();
|
||||
break;
|
||||
case TrackEvents.WantsRemoval:
|
||||
this.removeTrack((publisher as Track).getKey());
|
||||
break;
|
||||
case TrackEvents.Baked:
|
||||
this.setIsUsingAutoBeatLength(false);
|
||||
break;
|
||||
}
|
||||
this.publisher.notifySubs(BeatEvents.DeepChange);
|
||||
}
|
||||
|
||||
addSubscriber(subscriber: ISubscriber<BeatEvents>, eventType: BeatEvents | Readonly<BeatEvents[]>): { unbind: () => void } {
|
||||
return this.publisher.addSubscriber(subscriber, eventType);
|
||||
}
|
||||
|
||||
private setBarCountInternal(barCount: number): void {
|
||||
if (!isPosInt(barCount)) {
|
||||
barCount = this.barCount;
|
||||
}
|
||||
this.barCount = barCount;
|
||||
for (const track of this.tracks) {
|
||||
track.setBarCount(barCount);
|
||||
}
|
||||
this.publisher.notifySubs(BeatEvents.BarCountChanged);
|
||||
}
|
||||
|
||||
setBarCount(barCount: number): void {
|
||||
if (!this.barSettingsLocked) {
|
||||
this.setBarCountInternal(barCount);
|
||||
} else {
|
||||
this.setBarCountInternal(this.barCount);
|
||||
}
|
||||
}
|
||||
|
||||
getBarCount(): number {
|
||||
return this.barCount;
|
||||
}
|
||||
|
||||
setLoopLength(loopLength: number): void {
|
||||
if (!isPosInt(loopLength)) {
|
||||
return;
|
||||
}
|
||||
this.globalLoopLength = loopLength;
|
||||
for (const track of this.tracks) {
|
||||
track.setLoopLength(loopLength);
|
||||
}
|
||||
this.publisher.notifySubs(BeatEvents.GlobalLoopLengthChanged);
|
||||
}
|
||||
|
||||
getLoopLength(): number {
|
||||
return this.globalLoopLength;
|
||||
}
|
||||
|
||||
setLooping(isLooping: boolean): void {
|
||||
this.globalIsLooping = isLooping;
|
||||
for (const track of this.tracks) {
|
||||
track.setLooping(isLooping);
|
||||
}
|
||||
this.publisher.notifySubs(BeatEvents.GlobalDisplayTypeChanged);
|
||||
}
|
||||
|
||||
isLooping(): boolean {
|
||||
return this.globalIsLooping;
|
||||
}
|
||||
|
||||
private findSmallestLoopLength(): number {
|
||||
const loopLengths = [this.timeSigUp];
|
||||
for (const track of this.tracks) {
|
||||
if (track.isLooping()) {
|
||||
const loopLength = track.getLoopLength();
|
||||
if (loopLengths.indexOf(loopLength) === -1) {
|
||||
loopLengths.push(loopLength);
|
||||
export function createBeat(opts: BeatGroupInitOptions) {
|
||||
const scope = effectScope();
|
||||
return scope.run(() => {
|
||||
const tracks = shallowRef<Track[]>([]);
|
||||
const barCountInternal = ref<number>(opts.barCount ?? 4);
|
||||
const barCount = computed({
|
||||
get() {
|
||||
if (useAutoBeatLength.value) {
|
||||
const loopLengths = [timeSigUp.value];
|
||||
for (const track of tracks.value) {
|
||||
if (track.looping.value) {
|
||||
const loopLength = track.loopLength.value;
|
||||
if (loopLengths.indexOf(loopLength) === -1) {
|
||||
loopLengths.push(loopLength);
|
||||
}
|
||||
}
|
||||
}
|
||||
const smallestLoopLength = loopLengths.reduce((prev, curr) => (prev * curr) / greatestCommonDivisor(prev, curr), 1);
|
||||
return smallestLoopLength / timeSigUp.value;
|
||||
}
|
||||
return barCountInternal.value;
|
||||
},
|
||||
set(val) {
|
||||
if (barSettingsLocked.value || !isPosInt(val)) {
|
||||
return;
|
||||
}
|
||||
barCountInternal.value = val;
|
||||
},
|
||||
});
|
||||
const timeSigUp = ref<number>(opts.timeSigUp ?? 4);
|
||||
const globalLoopLengthInternal = ref<number>(opts.loopLength ?? timeSigUp.value);
|
||||
const globalLoopLength = computed({
|
||||
get() {
|
||||
return globalLoopLengthInternal.value;
|
||||
},
|
||||
set(val) {
|
||||
if (!isPosInt(val)) {
|
||||
return;
|
||||
}
|
||||
globalLoopLengthInternal.value = val;
|
||||
},
|
||||
});
|
||||
const globalIsLooping = ref<boolean>(opts.isLooping ?? false);
|
||||
const useAutoBeatLength = ref(opts.useAutoBeatLength ?? false);
|
||||
const barSettingsLocked = computed(() => useAutoBeatLength.value);
|
||||
const name = ref(opts.name ?? 'Beat');
|
||||
|
||||
function setTimeSigUp(timeSigVal: number): void {
|
||||
if (!isValidTimeSigRange(timeSigVal)) {
|
||||
timeSigVal = timeSigUp.value;
|
||||
}
|
||||
timeSigUp.value = timeSigVal;
|
||||
for (const track of tracks.value) {
|
||||
track.timeSigUp.value = timeSigVal;
|
||||
}
|
||||
}
|
||||
if (loopLengths.length === 1) {
|
||||
loopLengths.push(1);
|
||||
|
||||
function getTrackByIndex(trackIndex: number) {
|
||||
const track = tracks.value[trackIndex];
|
||||
if (!track) {
|
||||
throw new Error(`Could not find the track with index: ${trackIndex}`);
|
||||
}
|
||||
return track;
|
||||
}
|
||||
return loopLengths.reduce((prev, curr) => (prev * curr) / greatestCommonDivisor(prev, curr));
|
||||
}
|
||||
|
||||
setTimeSigUp(timeSigUp: number): void {
|
||||
if (!Track.isValidTimeSigRange(timeSigUp)) {
|
||||
timeSigUp = this.timeSigUp;
|
||||
function swapTracksByIndices(trackIndex1: number, trackIndex2: number): void {
|
||||
const track1 = getTrackByIndex(trackIndex1);
|
||||
const track2 = getTrackByIndex(trackIndex2);
|
||||
tracks.value[trackIndex1] = track2;
|
||||
tracks.value[trackIndex2] = track1;
|
||||
}
|
||||
this.timeSigUp = timeSigUp;
|
||||
for (const track of this.tracks) {
|
||||
track.setTimeSignature({ up: timeSigUp });
|
||||
}
|
||||
this.autoBeatLength();
|
||||
this.publisher.notifySubs(BeatEvents.TimeSigUpChanged);
|
||||
}
|
||||
|
||||
getTimeSigUp(): number {
|
||||
return this.timeSigUp;
|
||||
}
|
||||
|
||||
getTrackByKey(trackKey: string): Track {
|
||||
const foundTrack = this.tracks.find(track => track.getKey() === trackKey);
|
||||
if (typeof foundTrack === "undefined") {
|
||||
throw new Error(`Could not find the track with key: ${trackKey}`);
|
||||
}
|
||||
return foundTrack;
|
||||
}
|
||||
|
||||
getTrackByIndex(trackIndex: number): Track {
|
||||
if (!this.tracks[trackIndex]) {
|
||||
throw new Error(`Could not find the track with index: ${trackIndex}`);
|
||||
}
|
||||
return this.tracks[trackIndex];
|
||||
}
|
||||
|
||||
getTrackCount(): number {
|
||||
return this.tracks.length;
|
||||
}
|
||||
|
||||
getTrackKeys(): string[] {
|
||||
return this.tracks.map(track => track.getKey());
|
||||
}
|
||||
|
||||
swapTracksByIndices(trackIndex1: number, trackIndex2: number): void {
|
||||
const track1 = this.getTrackByIndex(trackIndex1);
|
||||
const track2 = this.getTrackByIndex(trackIndex2);
|
||||
this.tracks[trackIndex1] = track2;
|
||||
this.tracks[trackIndex2] = track1;
|
||||
this.publisher.notifySubs(BeatEvents.TrackOrderChanged);
|
||||
}
|
||||
|
||||
moveTrackBack(trackKey: string): void {
|
||||
const index = this.tracks.indexOf(this.getTrackByKey(trackKey));
|
||||
if (typeof index !== "undefined" && index > 0) {
|
||||
this.swapTracksByIndices(index, index - 1);
|
||||
}
|
||||
this.publisher.notifySubs(BeatEvents.TrackOrderChanged);
|
||||
this.publisher.notifySubs(BeatEvents.TrackListChanged);
|
||||
}
|
||||
|
||||
moveTrackForward(trackKey: string): void {
|
||||
const index = this.tracks.indexOf(this.getTrackByKey(trackKey));
|
||||
if (typeof index !== "undefined" && index < this.getTrackCount()) {
|
||||
this.swapTracksByIndices(index, index + 1);
|
||||
}
|
||||
this.publisher.notifySubs(BeatEvents.TrackOrderChanged);
|
||||
this.publisher.notifySubs(BeatEvents.TrackListChanged);
|
||||
}
|
||||
|
||||
canMoveTrackBack(trackKey: string): boolean {
|
||||
return this.tracks.indexOf(this.getTrackByKey(trackKey)) > 0;
|
||||
}
|
||||
|
||||
canMoveTrackForward(trackKey: string): boolean {
|
||||
return this.tracks.indexOf(this.getTrackByKey(trackKey)) < this.tracks.length - 1;
|
||||
}
|
||||
|
||||
addTrack(track: Track): void;
|
||||
addTrack(options?: TrackInitOptions): Track;
|
||||
addTrack(optionsOrTrack?: Track | TrackInitOptions): Track | void {
|
||||
let newTrack: Track;
|
||||
if (optionsOrTrack instanceof Track) {
|
||||
newTrack = optionsOrTrack;
|
||||
} else {
|
||||
optionsOrTrack = {
|
||||
function addTrack(options?: TrackInitOptions): Track | null {
|
||||
let newTrack: Track | null;
|
||||
options = {
|
||||
bars: barCount.value,
|
||||
isLooping: globalIsLooping.value,
|
||||
loopLength: globalLoopLength.value,
|
||||
...options,
|
||||
timeSig: {
|
||||
up: this.timeSigUp,
|
||||
up: timeSigUp.value,
|
||||
down: 4,
|
||||
...options?.timeSig ?? {},
|
||||
},
|
||||
bars: this.barCount,
|
||||
isLooping: this.globalIsLooping,
|
||||
loopLength: this.globalLoopLength,
|
||||
...optionsOrTrack
|
||||
};
|
||||
newTrack = new Track(optionsOrTrack);
|
||||
newTrack = createTrack(options) ?? null;
|
||||
if (newTrack) {
|
||||
tracks.value.push(newTrack);
|
||||
triggerRef(tracks);
|
||||
}
|
||||
return newTrack;
|
||||
}
|
||||
this.tracks.push(newTrack);
|
||||
newTrack.addSubscriber(this, EventTypeSubscriptions);
|
||||
this.publisher.notifySubs(BeatEvents.TrackListChanged);
|
||||
return newTrack;
|
||||
}
|
||||
|
||||
removeTrack(trackKey: string): void {
|
||||
const track = this.getTrackByKey(trackKey);
|
||||
this.tracks.splice(this.tracks.indexOf(track), 1);
|
||||
this.autoBeatLength();
|
||||
this.publisher.notifySubs(BeatEvents.TrackListChanged);
|
||||
}
|
||||
|
||||
setTrackName(trackKey: string, newName: string): void {
|
||||
this.getTrackByKey(trackKey).setName(newName);
|
||||
this.publisher.notifySubs(BeatEvents.TrackOrderChanged);
|
||||
}
|
||||
|
||||
autoBeatLengthOn(): boolean {
|
||||
return this.useAutoBeatLength;
|
||||
}
|
||||
|
||||
private autoBeatLength(): void {
|
||||
if (this.useAutoBeatLength) {
|
||||
this.setBarCountInternal(this.findSmallestLoopLength() / this.timeSigUp);
|
||||
function removeTrack(index: number): void {
|
||||
const track = getTrackByIndex(index);
|
||||
tracks.value.splice(index, 1);
|
||||
track.destroy();
|
||||
triggerRef(tracks);
|
||||
}
|
||||
}
|
||||
|
||||
setIsUsingAutoBeatLength(isOn: boolean): void {
|
||||
this.useAutoBeatLength = isOn;
|
||||
this.autoBeatLength();
|
||||
if (isOn) {
|
||||
this.lockBars();
|
||||
} else {
|
||||
this.unlockBars();
|
||||
function bakeLoops(): void {
|
||||
const barCountLooped = barCount.value;
|
||||
useAutoBeatLength.value = false;
|
||||
barCount.value = barCountLooped;
|
||||
tracks.value.forEach(track => track.bakeLoops());
|
||||
}
|
||||
this.publisher.notifySubs(BeatEvents.AutoBeatSettingsChanged);
|
||||
}
|
||||
|
||||
barsLocked(): boolean {
|
||||
return this.barSettingsLocked;
|
||||
}
|
||||
function serialise(): Readonly<BeatSerial> {
|
||||
return {
|
||||
tracks: tracks.value.map(track => track.serialise()),
|
||||
barCount: barCount.value,
|
||||
timeSigUp: timeSigUp.value,
|
||||
globalLoopLength: globalLoopLength.value,
|
||||
globalIsLooping: globalIsLooping.value,
|
||||
useAutoBeatLength: useAutoBeatLength.value,
|
||||
barSettingsLocked: barSettingsLocked.value,
|
||||
name: name.value,
|
||||
} as const;
|
||||
}
|
||||
|
||||
lockBars(): void {
|
||||
this.barSettingsLocked = true;
|
||||
this.publisher.notifySubs(BeatEvents.LockingChanged);
|
||||
}
|
||||
watch([barCount, timeSigUp], ([newBarCount, newTimeSigUp]) => {
|
||||
for (const track of tracks.value) {
|
||||
track.barCount.value = newBarCount;
|
||||
track.timeSigUp.value = newTimeSigUp;
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
unlockBars(): void {
|
||||
this.barSettingsLocked = false;
|
||||
this.publisher.notifySubs(BeatEvents.LockingChanged);
|
||||
}
|
||||
|
||||
bakeLoops(): void {
|
||||
this.tracks.forEach(track => track.bakeLoops());
|
||||
}
|
||||
|
||||
setName(newName: string): void {
|
||||
this.name.val = newName;
|
||||
}
|
||||
|
||||
getName(): ICapsule<string> {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
serialise(): Readonly<BeatSerial> {
|
||||
return {
|
||||
tracks: this.tracks.map(track => track.serialise()),
|
||||
barCount: this.barCount,
|
||||
timeSigUp: this.timeSigUp,
|
||||
globalLoopLength: this.globalLoopLength,
|
||||
globalIsLooping: this.globalIsLooping,
|
||||
useAutoBeatLength: this.useAutoBeatLength,
|
||||
barSettingsLocked: this.barSettingsLocked,
|
||||
name: this.name.val,
|
||||
} as const;
|
||||
}
|
||||
tracks,
|
||||
barCount,
|
||||
timeSigUp,
|
||||
globalLoopLength,
|
||||
globalIsLooping,
|
||||
useAutoBeatLength,
|
||||
barSettingsLocked,
|
||||
name,
|
||||
|
||||
static isBeatSerial(serial: any): serial is BeatSerial {
|
||||
return Array.isArray(serial.tracks) &&
|
||||
typeof serial.barCount === "number" &&
|
||||
typeof serial.timeSigUp === "number" &&
|
||||
typeof serial.globalLoopLength === "number" &&
|
||||
typeof serial.globalIsLooping === "boolean" &&
|
||||
typeof serial.useAutoBeatLength === "boolean" &&
|
||||
typeof serial.barSettingsLocked === "boolean";
|
||||
}
|
||||
setTimeSigUp,
|
||||
getTrackByIndex,
|
||||
swapTracksByIndices,
|
||||
addTrack,
|
||||
removeTrack,
|
||||
bakeLoops,
|
||||
serialise,
|
||||
destroy: scope.stop,
|
||||
};
|
||||
})!;
|
||||
}
|
||||
|
||||
export type Beat = ReturnType<typeof createBeat>;
|
||||
|
||||
Reference in New Issue
Block a user