update
This commit is contained in:
317
src/Beat.ts
317
src/Beat.ts
@@ -1,6 +1,8 @@
|
||||
import { deserialise as deserialiseTrack, type TrackInitOptions, createTrack, isValidTimeSigRange, type Track } from "@/Track";
|
||||
import { deserialise as deserialiseTrack, type TrackInitOptions, isValidTimeSigRange, Track } from "@/Track";
|
||||
import { greatestCommonDivisor, isPosInt } from "@/utils";
|
||||
import { watch, computed, effectScope, ref, shallowRef, triggerRef } from "vue";
|
||||
import { watch, computed, ref, shallowRef, triggerRef, type WritableComputedRef, type Ref, type ShallowRef } from "vue";
|
||||
import EffectScoped from "./EffectScoped";
|
||||
import { z } from "zod";
|
||||
|
||||
export interface BeatManager {
|
||||
notifyChange(): void;
|
||||
@@ -17,58 +19,41 @@ type BeatGroupInitOptions = {
|
||||
name?: string,
|
||||
};
|
||||
|
||||
export type BeatSerial = {
|
||||
tracks: Record<string, any>[],
|
||||
barCount: number,
|
||||
timeSigUp: number,
|
||||
globalLoopLength: number,
|
||||
globalIsLooping: boolean,
|
||||
useAutoBeatLength: boolean,
|
||||
barSettingsLocked: boolean,
|
||||
name: string,
|
||||
};
|
||||
const BeatSerialSchema = z.object({
|
||||
tracks: z.array(z.any()),
|
||||
barCount: z.number(),
|
||||
timeSigUp: z.number(),
|
||||
globalLoopLength: z.number(),
|
||||
globalIsLooping: z.boolean(),
|
||||
useAutoBeatLength: z.boolean(),
|
||||
barSettingsLocked: z.boolean(),
|
||||
name: z.string(),
|
||||
});
|
||||
export type BeatSerial = z.infer<typeof BeatSerialSchema>;
|
||||
|
||||
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";
|
||||
}
|
||||
export class Beat extends EffectScoped {
|
||||
private barCountInternal: Ref<number>;
|
||||
private globalLoopLengthInternal: Ref<number>;
|
||||
manager: BeatManager;
|
||||
tracks: ShallowRef<Track[]>;
|
||||
barCount: WritableComputedRef<number>;
|
||||
timeSigUp: Ref<number>;
|
||||
globalLoopLength: WritableComputedRef<number>;
|
||||
globalIsLooping: Ref<boolean>;
|
||||
useAutoBeatLength: Ref<boolean>;
|
||||
barSettingsLocked: Ref<boolean>;
|
||||
name: Ref<string>;
|
||||
|
||||
export function deserialise(serial: {}, manager: BeatManager) {
|
||||
if (!isBeatSerial(serial)) {
|
||||
throw new Error("Not a valid beat serial");
|
||||
}
|
||||
const newBeat = createBeat({
|
||||
manager,
|
||||
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, manager);
|
||||
if (track) newBeat.tracks.value.push(track);
|
||||
});
|
||||
return newBeat;
|
||||
}
|
||||
|
||||
export function createBeat(opts: BeatGroupInitOptions) {
|
||||
const scope = effectScope();
|
||||
return scope.run(() => {
|
||||
const manager = opts.manager;
|
||||
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) {
|
||||
constructor(opts: BeatGroupInitOptions) {
|
||||
super();
|
||||
this.manager = opts.manager;
|
||||
this.tracks = shallowRef<Track[]>([]);
|
||||
this.barCountInternal = ref<number>(opts.barCount ?? 4);
|
||||
this.barCount = computed({
|
||||
get: () => {
|
||||
if (this.useAutoBeatLength.value) {
|
||||
const loopLengths = [this.timeSigUp.value];
|
||||
for (const track of this.tracks.value) {
|
||||
if (track.looping.value) {
|
||||
const loopLength = track.loopLength.value;
|
||||
if (loopLengths.indexOf(loopLength) === -1) {
|
||||
@@ -77,140 +62,136 @@ export function createBeat(opts: BeatGroupInitOptions) {
|
||||
}
|
||||
}
|
||||
const smallestLoopLength = loopLengths.reduce((prev, curr) => (prev * curr) / greatestCommonDivisor(prev, curr), 1);
|
||||
return smallestLoopLength / timeSigUp.value;
|
||||
return smallestLoopLength / this.timeSigUp.value;
|
||||
}
|
||||
return barCountInternal.value;
|
||||
return this.barCountInternal.value;
|
||||
},
|
||||
set(val) {
|
||||
if (barSettingsLocked.value || !isPosInt(val)) {
|
||||
set: (val) => {
|
||||
if (this.barSettingsLocked.value || !isPosInt(val)) {
|
||||
return;
|
||||
}
|
||||
barCountInternal.value = val;
|
||||
this.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;
|
||||
this.timeSigUp = ref<number>(opts.timeSigUp ?? 4);
|
||||
this.globalLoopLengthInternal = ref<number>(opts.loopLength ?? this.timeSigUp.value);
|
||||
this.globalLoopLength = computed({
|
||||
get: () => {
|
||||
return this.globalLoopLengthInternal.value;
|
||||
},
|
||||
set(val) {
|
||||
set: (val) => {
|
||||
if (!isPosInt(val)) {
|
||||
return;
|
||||
}
|
||||
globalLoopLengthInternal.value = val;
|
||||
this.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');
|
||||
this.globalIsLooping = ref<boolean>(opts.isLooping ?? false);
|
||||
this.useAutoBeatLength = ref(opts.useAutoBeatLength ?? false);
|
||||
this.barSettingsLocked = computed(() => this.useAutoBeatLength.value);
|
||||
this.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;
|
||||
}
|
||||
}
|
||||
|
||||
function getTrackByIndex(trackIndex: number) {
|
||||
const track = tracks.value[trackIndex];
|
||||
if (!track) {
|
||||
throw new Error(`Could not find the track with index: ${trackIndex}`);
|
||||
}
|
||||
return track;
|
||||
}
|
||||
|
||||
function swapTracksByIndices(trackIndex1: number, trackIndex2: number): void {
|
||||
const track1 = getTrackByIndex(trackIndex1);
|
||||
const track2 = getTrackByIndex(trackIndex2);
|
||||
tracks.value[trackIndex1] = track2;
|
||||
tracks.value[trackIndex2] = track1;
|
||||
}
|
||||
|
||||
function addTrack(opts?: Omit<TrackInitOptions, 'manager'>): Track | null {
|
||||
let newTrack: Track | null;
|
||||
const options: TrackInitOptions = {
|
||||
manager,
|
||||
bars: barCount.value,
|
||||
isLooping: globalIsLooping.value,
|
||||
loopLength: globalLoopLength.value,
|
||||
...opts,
|
||||
timeSig: {
|
||||
up: timeSigUp.value,
|
||||
down: 4,
|
||||
...opts?.timeSig ?? {},
|
||||
},
|
||||
};
|
||||
newTrack = createTrack(options) ?? null;
|
||||
if (newTrack) {
|
||||
tracks.value.push(newTrack);
|
||||
triggerRef(tracks);
|
||||
}
|
||||
return newTrack;
|
||||
}
|
||||
|
||||
function removeTrack(index: number): void {
|
||||
const track = getTrackByIndex(index);
|
||||
tracks.value.splice(index, 1);
|
||||
track.destroy();
|
||||
triggerRef(tracks);
|
||||
}
|
||||
|
||||
function bakeLoops(): void {
|
||||
const barCountLooped = barCount.value;
|
||||
useAutoBeatLength.value = false;
|
||||
barCount.value = barCountLooped;
|
||||
tracks.value.forEach(track => track.bakeLoops());
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
watch(tracks, () => {
|
||||
manager.notifyChange();
|
||||
});
|
||||
|
||||
watch([barCount, timeSigUp], ([newBarCount, newTimeSigUp]) => {
|
||||
for (const track of tracks.value) {
|
||||
watch([this.barCount, this.timeSigUp], ([newBarCount, newTimeSigUp]) => {
|
||||
for (const track of this.tracks.value) {
|
||||
track.barCount.value = newBarCount;
|
||||
track.timeSigUp.value = newTimeSigUp;
|
||||
}
|
||||
}, { immediate: true });
|
||||
}
|
||||
|
||||
setTimeSigUp(timeSigVal: number): void {
|
||||
if (!isValidTimeSigRange(timeSigVal)) {
|
||||
timeSigVal = this.timeSigUp.value;
|
||||
}
|
||||
this.timeSigUp.value = timeSigVal;
|
||||
for (const track of this.tracks.value) {
|
||||
track.timeSigUp.value = timeSigVal;
|
||||
}
|
||||
}
|
||||
|
||||
getTrackByIndex(trackIndex: number) {
|
||||
const track = this.tracks.value[trackIndex];
|
||||
if (!track) {
|
||||
throw new Error(`Could not find the track with index: ${trackIndex}`);
|
||||
}
|
||||
return track;
|
||||
}
|
||||
|
||||
swapTracksByIndices(trackIndex1: number, trackIndex2: number): void {
|
||||
const track1 = this.getTrackByIndex(trackIndex1);
|
||||
const track2 = this.getTrackByIndex(trackIndex2);
|
||||
this.tracks.value[trackIndex1] = track2;
|
||||
this.tracks.value[trackIndex2] = track1;
|
||||
}
|
||||
|
||||
addTrack(options?: Omit<TrackInitOptions, 'manager'>): Track | null {
|
||||
const optionsResolved = {
|
||||
manager: this.manager,
|
||||
bars: this.barCount.value,
|
||||
isLooping: this.globalIsLooping.value,
|
||||
loopLength: this.globalLoopLength.value,
|
||||
...options,
|
||||
timeSig: {
|
||||
up: this.timeSigUp.value,
|
||||
down: 4,
|
||||
...options?.timeSig ?? {},
|
||||
},
|
||||
} satisfies TrackInitOptions;
|
||||
const newTrack = Track.asScoped(optionsResolved) ?? null;
|
||||
if (newTrack) {
|
||||
this.tracks.value.push(newTrack);
|
||||
triggerRef(this.tracks);
|
||||
}
|
||||
return newTrack;
|
||||
}
|
||||
|
||||
removeTrack(index: number): void {
|
||||
const track = this.getTrackByIndex(index);
|
||||
this.tracks.value.splice(index, 1);
|
||||
track.destroy();
|
||||
triggerRef(this.tracks);
|
||||
}
|
||||
|
||||
bakeLoops(): void {
|
||||
const barCountLooped = this.barCount.value;
|
||||
this.useAutoBeatLength.value = false;
|
||||
this.barCount.value = barCountLooped;
|
||||
this.tracks.value.forEach(track => track.bakeLoops());
|
||||
}
|
||||
|
||||
serialise(): Readonly<BeatSerial> {
|
||||
return {
|
||||
tracks,
|
||||
barCount,
|
||||
timeSigUp,
|
||||
globalLoopLength,
|
||||
globalIsLooping,
|
||||
useAutoBeatLength,
|
||||
barSettingsLocked,
|
||||
name,
|
||||
|
||||
setTimeSigUp,
|
||||
getTrackByIndex,
|
||||
swapTracksByIndices,
|
||||
addTrack,
|
||||
removeTrack,
|
||||
bakeLoops,
|
||||
serialise,
|
||||
destroy: scope.stop,
|
||||
};
|
||||
})!;
|
||||
tracks: this.tracks.value.map(track => track.serialise()),
|
||||
barCount: this.barCount.value,
|
||||
timeSigUp: this.timeSigUp.value,
|
||||
globalLoopLength: this.globalLoopLength.value,
|
||||
globalIsLooping: this.globalIsLooping.value,
|
||||
useAutoBeatLength: this.useAutoBeatLength.value,
|
||||
barSettingsLocked: this.barSettingsLocked.value,
|
||||
name: this.name.value,
|
||||
} as const;
|
||||
}
|
||||
}
|
||||
|
||||
export type Beat = ReturnType<typeof createBeat>;
|
||||
export function deserialise(serial: unknown, manager: BeatManager): Beat | null {
|
||||
const parse = BeatSerialSchema.safeParse(serial);
|
||||
if (!parse.success) {
|
||||
console.error('Encountered invalid beat serial:', parse.error, serial);
|
||||
return null;
|
||||
}
|
||||
const beat = parse.data;
|
||||
const newBeat = Beat.asScoped({
|
||||
manager,
|
||||
loopLength: beat.globalLoopLength,
|
||||
barCount: beat.barCount,
|
||||
isLooping: beat.globalIsLooping,
|
||||
name: beat.name,
|
||||
timeSigUp: beat.timeSigUp,
|
||||
useAutoBeatLength: beat.useAutoBeatLength,
|
||||
});
|
||||
beat.tracks.forEach(trackSerial => {
|
||||
const track = deserialiseTrack(trackSerial, manager);
|
||||
if (track) newBeat.tracks.value.push(track);
|
||||
});
|
||||
return newBeat;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user