feat: added autobeat for full bars

This commit is contained in:
Daniel Ledda
2021-09-04 08:55:46 +02:00
parent 5d24be0612
commit 5697ee6524
7 changed files with 174 additions and 111 deletions

View File

@@ -1,4 +1,4 @@
import BeatUnit, {BeatUnitType} from "./BeatUnit"; import BeatUnit from "./BeatUnit";
import {IPublisher, Publisher} from "./Publisher"; import {IPublisher, Publisher} from "./Publisher";
import ISubscriber from "./Subscriber"; import ISubscriber from "./Subscriber";
import BeatLike from "./BeatLike"; import BeatLike from "./BeatLike";
@@ -15,13 +15,12 @@ export type BeatInitOptions = {
loopLength?: number, loopLength?: number,
}; };
export enum BeatEvents { export const enum BeatEvents {
NewTimeSig, NewTimeSig="BE0",
NewBarCount, NewBarCount="BE1",
NewName, NewName="BE2",
UnitChanged, DisplayTypeChanged="BE3",
DisplayTypeChanged, LoopLengthChanged="BE4",
LoopLengthChanged,
} }
export default class Beat implements IPublisher<BeatEvents>, BeatLike { export default class Beat implements IPublisher<BeatEvents>, BeatLike {
@@ -48,9 +47,9 @@ export default class Beat implements IPublisher<BeatEvents>, BeatLike {
setLoopLength(loopLength: number): void { setLoopLength(loopLength: number): void {
if (!isPosInt(loopLength) || loopLength < 2) { if (!isPosInt(loopLength) || loopLength < 2) {
return; loopLength = this.loopLength;
} }
this.loopLength = loopLength | 0; this.loopLength = loopLength;
this.publisher.notifySubs(BeatEvents.LoopLengthChanged); this.publisher.notifySubs(BeatEvents.LoopLengthChanged);
} }
@@ -59,14 +58,13 @@ export default class Beat implements IPublisher<BeatEvents>, BeatLike {
this.publisher.notifySubs(BeatEvents.DisplayTypeChanged); this.publisher.notifySubs(BeatEvents.DisplayTypeChanged);
} }
addSubscriber(subscriber: ISubscriber, eventType: BeatEvents | "all"): { unbind: () => void } { addSubscriber(subscriber: ISubscriber, eventType: BeatEvents | BeatEvents[] | "all"): { unbind: () => void } {
return this.publisher.addSubscriber(subscriber, eventType); return this.publisher.addSubscriber(subscriber, eventType);
} }
setTimeSignature(timeSig: {up?: number, down?: number}): void { setTimeSignature(timeSig: {up?: number, down?: number}): void {
if (timeSig.up && Beat.isValidTimeSigRange(timeSig.up)) { if (timeSig.up && Beat.isValidTimeSigRange(timeSig.up)) {
this.timeSigUp = timeSig.up | 0; this.timeSigUp = timeSig.up | 0;
this.loopLength = this.timeSigUp * this.barCount;
} }
if (timeSig.down && Beat.isValidTimeSigRange(timeSig.down)) { if (timeSig.down && Beat.isValidTimeSigRange(timeSig.down)) {
this.timeSigDown = timeSig.down | 0; this.timeSigDown = timeSig.down | 0;
@@ -77,17 +75,16 @@ export default class Beat implements IPublisher<BeatEvents>, BeatLike {
setBarCount(barCount: number): void { setBarCount(barCount: number): void {
if (!isPosInt(barCount) || barCount == this.barCount) { if (!isPosInt(barCount) || barCount == this.barCount) {
return; barCount = this.barCount;
} }
this.barCount = barCount; this.barCount = barCount;
this.loopLength = this.timeSigUp * this.barCount;
this.updateBeatUnitLength(); this.updateBeatUnitLength();
this.publisher.notifySubs(BeatEvents.NewBarCount); this.publisher.notifySubs(BeatEvents.NewBarCount);
} }
getUnitByIndex(index: number): BeatUnit | null { getUnitByIndex(index: number): BeatUnit | null {
if (this.looping) { if (this.looping) {
return this.unitRecord[index % this.loopLength]; index %= this.loopLength;
} }
return this.unitRecord[index] ?? null; return this.unitRecord[index] ?? null;
} }
@@ -112,63 +109,6 @@ export default class Beat implements IPublisher<BeatEvents>, BeatLike {
return this.timeSigDown; return this.timeSigDown;
} }
turnUnitOn(index: number): void {
if (!isPosInt(index)) {
return;
}
const unit = this.getUnit(index);
if (unit) {
unit.setOn(true);
this.publisher.notifySubs(BeatEvents.UnitChanged);
}
}
turnUnitOff(index: number): void {
if (!isPosInt(index)) {
return;
}
const unit = this.getUnit(index);
if (unit) {
unit.setOn(false);
this.publisher.notifySubs(BeatEvents.UnitChanged);
}
}
toggleUnit(index: number): void {
if (!isPosInt(index)) {
return;
}
const unit = this.getUnit(index);
if (unit) {
unit.toggle();
this.publisher.notifySubs(BeatEvents.UnitChanged);
}
}
setUnitType(index: number, type: BeatUnitType): void {
if (!isPosInt(index)) {
return;
}
this.getUnit(index).setType(type);
this.publisher.notifySubs(BeatEvents.UnitChanged);
}
unitIsOn(index: number): boolean {
return this.getUnit(index)?.isOn();
}
unitType(index: number): BeatUnitType {
return this.getUnit(index)?.getType();
}
private getUnit(index: number): BeatUnit {
if (!this.unitRecord[index]) {
throw new Error(`Invalid beat unit index! - ${index}`);
}
return this.unitRecord[index];
}
getBarCount(): number { getBarCount(): number {
return this.barCount; return this.barCount;
} }
@@ -178,7 +118,7 @@ export default class Beat implements IPublisher<BeatEvents>, BeatLike {
} }
static isValidTimeSigRange(sig: number): boolean { static isValidTimeSigRange(sig: number): boolean {
return sig >= 2 && sig <= 64; return sig >= 2 && sig <= 32;
} }
setName(newName: string): void { setName(newName: string): void {

View File

@@ -10,23 +10,28 @@ type BeatGroupInitOptions = {
timeSigUp: number; timeSigUp: number;
beats: BeatInitOptions[], beats: BeatInitOptions[],
loopLength?: number, loopLength?: number,
forceFullBars?: boolean,
useAutoBeatLength?: boolean,
} }
export const enum BeatGroupEvents { export const enum BeatGroupEvents {
BeatOrderChanged, BeatOrderChanged="BGE0",
BeatListChanged, BeatListChanged="BGE1",
GlobalBarCountChanged, BarCountChanged="BGE2",
GlobalTimeSigUpChanged, TimeSigUpChanged="BGE3",
AutoBeatSettingsChanged="BGE4",
} }
export default class BeatGroup implements IPublisher<BeatGroupEvents | BeatEvents>, BeatLike { export default class BeatGroup implements IPublisher<BeatGroupEvents | BeatEvents>, BeatLike, ISubscriber {
private beats: Beat[] = []; private beats: Beat[] = [];
private beatKeyMap: Record<string, number> = {}; private beatKeyMap: Record<string, number> = {};
private publisher: Publisher<BeatGroupEvents | BeatEvents> = new Publisher<BeatGroupEvents | BeatEvents>(); private publisher: Publisher<BeatGroupEvents | BeatEvents> = new Publisher<BeatGroupEvents | BeatEvents>();
private globalBarCount: number; private barCount: number;
private globalTimeSigUp: number; private timeSigUp: number;
private globalLoopLength: number; private globalLoopLength: number;
private globalIsLooping: boolean; private globalIsLooping: boolean;
private forceFullBars: boolean;
private useAutoBeatLength: boolean;
constructor(options?: BeatGroupInitOptions) { constructor(options?: BeatGroupInitOptions) {
if (options?.beats) { if (options?.beats) {
@@ -36,10 +41,18 @@ export default class BeatGroup implements IPublisher<BeatGroupEvents | BeatEvent
this.beatKeyMap[newBeat.getKey()] = this.beats.length - 1; this.beatKeyMap[newBeat.getKey()] = this.beats.length - 1;
} }
} }
this.globalBarCount = options?.barCount ?? 4; this.barCount = options?.barCount ?? 4;
this.globalTimeSigUp = options?.timeSigUp ?? 4; this.timeSigUp = options?.timeSigUp ?? 4;
this.globalLoopLength = options?.loopLength ?? this.globalBarCount * this.globalTimeSigUp; this.globalLoopLength = options?.loopLength ?? this.barCount * this.timeSigUp;
this.globalIsLooping = options?.isLooping ?? false; this.globalIsLooping = options?.isLooping ?? false;
this.useAutoBeatLength = options?.useAutoBeatLength ?? false;
this.forceFullBars = options?.forceFullBars ?? true;
}
notify<T extends string | number>(publisher: IPublisher<T>, event: "all" | T[] | T): void {
if (event === BeatEvents.LoopLengthChanged) {
this.autoBeatLength();
}
} }
addSubscriber(subscriber: ISubscriber, eventType: "all" | BeatGroupEvents | BeatEvents | (BeatGroupEvents | BeatEvents)[]): { unbind: () => void } { addSubscriber(subscriber: ISubscriber, eventType: "all" | BeatGroupEvents | BeatEvents | (BeatGroupEvents | BeatEvents)[]): { unbind: () => void } {
@@ -47,18 +60,18 @@ export default class BeatGroup implements IPublisher<BeatGroupEvents | BeatEvent
} }
setBarCount(barCount: number): void { setBarCount(barCount: number): void {
if (barCount <= 0 || (barCount | 0) !== barCount) { if (!isPosInt(barCount)) {
return; barCount = this.barCount;
} }
this.globalBarCount = barCount; this.barCount = barCount;
for (const beat of this.beats) { for (const beat of this.beats) {
beat.setBarCount(barCount); beat.setBarCount(barCount);
} }
this.publisher.notifySubs(BeatGroupEvents.GlobalBarCountChanged); this.publisher.notifySubs(BeatGroupEvents.BarCountChanged);
} }
getBarCount(): number { getBarCount(): number {
return this.globalBarCount; return this.barCount;
} }
setLoopLength(loopLength: number): void { setLoopLength(loopLength: number): void {
@@ -81,6 +94,9 @@ export default class BeatGroup implements IPublisher<BeatGroupEvents | BeatEvent
for (const beat of this.beats) { for (const beat of this.beats) {
beat.setLooping(isLooping); beat.setLooping(isLooping);
} }
if (isLooping) {
this.autoBeatLength();
}
this.publisher.notifySubs(BeatEvents.DisplayTypeChanged); this.publisher.notifySubs(BeatEvents.DisplayTypeChanged);
} }
@@ -88,15 +104,39 @@ export default class BeatGroup implements IPublisher<BeatGroupEvents | BeatEvent
return this.globalIsLooping; return this.globalIsLooping;
} }
setGlobalTimeSigUp(timeSigUp: number): void { private findSmallestLoopLength(): number {
if (!Beat.isValidTimeSigRange(timeSigUp)) { const loopLengths = [];
return; const denominators = [];
for (const beat of this.beats) {
loopLengths.push(beat.getLoopLength());
} }
this.globalTimeSigUp = timeSigUp; if (this.forceFullBars) {
loopLengths.push(this.timeSigUp);
}
for (let i = 0; i < loopLengths.length; i++) {
let isFactor = false;
for (let j = 0; j < loopLengths.length; j++) {
if (j !== i && loopLengths[j] % loopLengths[i] === 0 && loopLengths[j] !== loopLengths[i]) {
isFactor = true;
break;
}
}
if (!isFactor && denominators.indexOf(loopLengths[i]) === -1) {
denominators.push(loopLengths[i]);
}
}
return denominators.reduce((prev, curr) => prev * curr, 1);
}
setTimeSigUp(timeSigUp: number): void {
if (!Beat.isValidTimeSigRange(timeSigUp)) {
timeSigUp = this.timeSigUp;
}
this.timeSigUp = timeSigUp;
for (const beat of this.beats) { for (const beat of this.beats) {
beat.setTimeSignature({up: timeSigUp}); beat.setTimeSignature({up: timeSigUp});
} }
this.publisher.notifySubs(BeatGroupEvents.GlobalTimeSigUpChanged); this.publisher.notifySubs(BeatGroupEvents.TimeSigUpChanged);
} }
getBeatByKey(beatKey: string): Beat { getBeatByKey(beatKey: string): Beat {
@@ -165,6 +205,7 @@ export default class BeatGroup implements IPublisher<BeatGroupEvents | BeatEvent
const newBeat = new Beat(options); const newBeat = new Beat(options);
this.beats.push(newBeat); this.beats.push(newBeat);
this.beatKeyMap[newBeat.getKey()] = this.beats.length; this.beatKeyMap[newBeat.getKey()] = this.beats.length;
newBeat.addSubscriber(this, [BeatEvents.LoopLengthChanged]);
this.publisher.notifySubs(BeatGroupEvents.BeatListChanged); this.publisher.notifySubs(BeatGroupEvents.BeatListChanged);
return newBeat; return newBeat;
} }
@@ -179,4 +220,30 @@ export default class BeatGroup implements IPublisher<BeatGroupEvents | BeatEvent
this.getBeatByKey(beatKey).setName(newName); this.getBeatByKey(beatKey).setName(newName);
this.publisher.notifySubs(BeatGroupEvents.BeatOrderChanged); this.publisher.notifySubs(BeatGroupEvents.BeatOrderChanged);
} }
autoBeatLengthOn(): boolean {
return this.useAutoBeatLength;
}
forcesFullBars(): boolean {
return this.forceFullBars;
}
private autoBeatLength(): void {
if (this.useAutoBeatLength && this.globalIsLooping) {
this.setBarCount(this.findSmallestLoopLength() / this.timeSigUp);
}
}
setIsUsingAutoBeatLength(isOn: boolean): void {
this.useAutoBeatLength = isOn;
this.autoBeatLength();
this.publisher.notifySubs(BeatGroupEvents.AutoBeatSettingsChanged);
}
setForcesFullBars(force: boolean): void {
this.forceFullBars = force;
this.autoBeatLength();
this.publisher.notifySubs(BeatGroupEvents.AutoBeatSettingsChanged);
}
} }

View File

@@ -3,7 +3,6 @@ import Beat, {BeatEvents} from "../../../../Beat";
import {IPublisher} from "../../../../Publisher"; import {IPublisher} from "../../../../Publisher";
import ISubscriber from "../../../../Subscriber"; import ISubscriber from "../../../../Subscriber";
import "./BeatSettings.css"; import "./BeatSettings.css";
import BeatLike from "../../../../BeatLike";
import BeatLikeLoopSettingsView from "../../BeatLikeLoopSettings/BeatLikeLoopSettingsView"; import BeatLikeLoopSettingsView from "../../BeatLikeLoopSettings/BeatLikeLoopSettingsView";
export type BeatSettingsViewUINodeOptions = UINodeOptions & { export type BeatSettingsViewUINodeOptions = UINodeOptions & {

View File

@@ -18,8 +18,8 @@ export default class BeatView extends UINode implements ISubscriber {
private beatUnitViews: BeatUnitView[] = []; private beatUnitViews: BeatUnitView[] = [];
private beatUnitViewBlock: HTMLElement | null = null; private beatUnitViewBlock: HTMLElement | null = null;
private lastHoveredBeatUnitView: BeatUnitView | null = null; private lastHoveredBeatUnitView: BeatUnitView | null = null;
private static deselectingUnits = false; static deselectingUnits = false;
private static selectingUnits = false; static selectingUnits = false;
constructor(options: BeatUINodeOptions) { constructor(options: BeatUINodeOptions) {
super(options); super(options);
@@ -68,11 +68,6 @@ export default class BeatView extends UINode implements ISubscriber {
view = new BeatUnitView({beatUnit}); view = new BeatUnitView({beatUnit});
this.beatUnitViews.push(view); this.beatUnitViews.push(view);
} }
view.onMouseDown((event: MouseEvent) => this.onBeatUnitClick(event.button, i));
window.addEventListener("mouseup", (event: MouseEvent) => {
BeatView.selectingUnits = false;
BeatView.deselectingUnits = false;
});
view.onHover(() => { view.onHover(() => {
this.lastHoveredBeatUnitView = view; this.lastHoveredBeatUnitView = view;
if (BeatView.selectingUnits) { if (BeatView.selectingUnits) {
@@ -81,6 +76,7 @@ export default class BeatView extends UINode implements ISubscriber {
this.lastHoveredBeatUnitView.turnOff(); this.lastHoveredBeatUnitView.turnOff();
} }
}); });
view.onMouseDown((event: MouseEvent) => this.onBeatUnitClick(event.button, i));
} }
} }
} }
@@ -149,6 +145,9 @@ export default class BeatView extends UINode implements ISubscriber {
classes: ["beat-title"], classes: ["beat-title"],
}); });
this.setupBeatUnits(); this.setupBeatUnits();
if (!this.beatUnitViewBlock) {
throw new Error("Beat unit block setup failed!");
}
this.settingsView = new BeatSettingsView({beat: this.beat}); this.settingsView = new BeatSettingsView({beat: this.beat});
this.settingsToggleButton = UINode.make("div", { this.settingsToggleButton = UINode.make("div", {
classes: ["beat-settings-btn"], classes: ["beat-settings-btn"],
@@ -162,7 +161,7 @@ export default class BeatView extends UINode implements ISubscriber {
classes: ["beat-main"], classes: ["beat-main"],
subs: [ subs: [
this.title, this.title,
this.beatUnitViewBlock!, this.beatUnitViewBlock,
] ]
}), }),
this.settingsToggleButton, this.settingsToggleButton,
@@ -175,3 +174,8 @@ export default class BeatView extends UINode implements ISubscriber {
return this.node; return this.node;
} }
} }
window.addEventListener("mouseup", () => {
BeatView.selectingUnits = false;
BeatView.deselectingUnits = false;
});

View File

@@ -12,3 +12,10 @@
display: inline-block; display: inline-block;
text-align: center; text-align: center;
} }
.beat-group-settings-option-group {
display: none;
}
.beat-group-settings-option-group.visible {
display: inline-block;
}

View File

@@ -5,6 +5,7 @@ import {IPublisher} from "../../../Publisher";
import {BeatGroupEvents} from "../../../BeatGroup"; import {BeatGroupEvents} from "../../../BeatGroup";
import BeatLikeLoopSettingsView from "../BeatLikeLoopSettings/BeatLikeLoopSettingsView"; import BeatLikeLoopSettingsView from "../BeatLikeLoopSettings/BeatLikeLoopSettingsView";
import "./BeatGroupSettings.css"; import "./BeatGroupSettings.css";
import {BeatEvents} from "../../../Beat";
export type BeatGroupSettingsUINodeOptions = UINodeOptions & { export type BeatGroupSettingsUINodeOptions = UINodeOptions & {
beatGroup: BeatGroup, beatGroup: BeatGroup,
@@ -15,21 +16,31 @@ export default class BeatGroupSettingsView extends UINode implements ISubscriber
private barCountInput!: HTMLInputElement; private barCountInput!: HTMLInputElement;
private timeSigUpInput!: HTMLInputElement; private timeSigUpInput!: HTMLInputElement;
private loopSettingsView!: BeatLikeLoopSettingsView; private loopSettingsView!: BeatLikeLoopSettingsView;
private autoBeatLengthCheckbox!: HTMLInputElement;
private forceFullBarsCheckbox!: HTMLInputElement;
private autoBeatOptions!: HTMLElement;
constructor(options: BeatGroupSettingsUINodeOptions) { constructor(options: BeatGroupSettingsUINodeOptions) {
super(options); super(options);
this.beatGroup = options.beatGroup; this.beatGroup = options.beatGroup;
this.beatGroup.addSubscriber(this, [ this.beatGroup.addSubscriber(this, [
BeatGroupEvents.GlobalBarCountChanged, BeatGroupEvents.BarCountChanged,
BeatGroupEvents.GlobalTimeSigUpChanged BeatGroupEvents.TimeSigUpChanged,
BeatEvents.DisplayTypeChanged,
]); ]);
} }
notify<T extends string | number>(publisher: IPublisher<T>, event: "all" | T[] | T): void { notify<T extends string | number>(publisher: IPublisher<T>, event: "all" | T[] | T): void {
if (event === BeatGroupEvents.GlobalBarCountChanged) { if (event === BeatGroupEvents.BarCountChanged) {
this.barCountInput.value = this.beatGroup.getBeatByIndex(0).getBarCount().toString(); this.barCountInput.valueAsNumber = this.beatGroup.getBeatByIndex(0).getBarCount();
} else if (event === BeatGroupEvents.GlobalTimeSigUpChanged) { } else if (event === BeatGroupEvents.TimeSigUpChanged) {
this.barCountInput.value = this.beatGroup.getBeatByIndex(0).getBarCount().toString(); this.timeSigUpInput.valueAsNumber = this.beatGroup.getBeatByIndex(0).getTimeSigUp();
} else if (event === BeatEvents.DisplayTypeChanged) {
if (this.beatGroup.isLooping()) {
this.autoBeatOptions.classList.add("visible");
} else {
this.autoBeatOptions.classList.remove("visible");
}
} }
} }
@@ -46,13 +57,44 @@ export default class BeatGroupSettingsView extends UINode implements ISubscriber
type: "number", type: "number",
value: this.beatGroup.getBeatByIndex(0).getTimeSigUp().toString(), value: this.beatGroup.getBeatByIndex(0).getTimeSigUp().toString(),
oninput: () => { oninput: () => {
this.beatGroup.setGlobalTimeSigUp(Number(this.timeSigUpInput.value)); this.beatGroup.setTimeSigUp(Number(this.timeSigUpInput.value));
}, },
}); });
this.autoBeatLengthCheckbox = UINode.make("input", {
type: "checkbox",
checked: this.beatGroup.autoBeatLengthOn(),
oninput: () => {
this.beatGroup.setIsUsingAutoBeatLength(this.autoBeatLengthCheckbox.checked);
},
});
this.forceFullBarsCheckbox = UINode.make("input", {
type: "checkbox",
checked: this.beatGroup.forcesFullBars(),
oninput: () => {
this.beatGroup.setForcesFullBars(this.forceFullBarsCheckbox.checked);
},
});
this.autoBeatOptions = UINode.make("div", {
classes: ["beat-group-settings-option-group"],
subs: [
UINode.make("div", {
subs: [
UINode.make("label", { innerText: "Auto beat length:"}),
this.autoBeatLengthCheckbox,
],
}),
UINode.make("div", {
subs: [
UINode.make("label", { innerText: "Force full bars:"}),
this.forceFullBarsCheckbox,
],
}),
]
});
this.node = UINode.make("div", { this.node = UINode.make("div", {
classes: ["beat-group-settings"], classes: ["beat-group-settings"],
subs: [ subs: [
UINode.make("h4", { innerText: "Settings for beat" }), UINode.make("div", { innerText: "Settings for beat" }),
UINode.make("div", { UINode.make("div", {
classes: ["beat-group-settings-options"], classes: ["beat-group-settings-options"],
subs: [ subs: [
@@ -71,6 +113,7 @@ export default class BeatGroupSettingsView extends UINode implements ISubscriber
], ],
}), }),
this.loopSettingsView.render(), this.loopSettingsView.render(),
this.autoBeatOptions,
], ],
}), }),
], ],

View File

@@ -21,7 +21,10 @@ export default class BeatLikeLoopSettingsView extends UINode implements ISubscri
} }
private setupBindings() { private setupBindings() {
this.beatLike.addSubscriber(this, "all"); this.beatLike.addSubscriber(this, [
BeatEvents.LoopLengthChanged,
BeatEvents.DisplayTypeChanged
]);
} }
notify<T extends string | number>(publisher: IPublisher<T>, event: "all" | T[] | T): void { notify<T extends string | number>(publisher: IPublisher<T>, event: "all" | T[] | T): void {