@blibliki/engine
Advanced tools
Comparing version 0.1.9 to 0.1.10
@@ -7,3 +7,3 @@ export { default } from "./Engine"; | ||
export type { MidiDeviceInterface } from "./MidiDevice"; | ||
export type { ModuleInterface, AudioModule } from "./Module"; | ||
export type { ModuleInterface, AudioModule, ISequence } from "./Module"; | ||
export type { SerializeInterface as IOProps } from "./Module/IO"; |
@@ -0,5 +1,6 @@ | ||
import { ISequence } from "./Module/Sequencer"; | ||
import Note, { INote } from "./Note"; | ||
export declare type EType = "noteOn" | "noteOff"; | ||
export default class MidiEvent { | ||
note?: Note; | ||
notes: Note[]; | ||
readonly triggeredAt: number; | ||
@@ -9,2 +10,3 @@ _type: EType; | ||
private event; | ||
static fromSequence(sequence: ISequence, triggeredAt: number): MidiEvent; | ||
static fromNote(noteName: string | Note | INote, type: EType, triggeredAt?: number): MidiEvent; | ||
@@ -14,3 +16,3 @@ constructor(event: MIDIMessageEvent, triggeredAt?: number); | ||
get isNote(): boolean; | ||
defineNote(): void; | ||
defineNotes(): void; | ||
} |
@@ -8,3 +8,3 @@ import { now } from "tone"; | ||
export default class MidiEvent { | ||
note; | ||
notes; | ||
triggeredAt; | ||
@@ -14,9 +14,15 @@ _type; | ||
event; | ||
static fromSequence(sequence, triggeredAt) { | ||
const event = new MidiEvent(new MIDIMessageEvent("", { data: new Uint8Array([0, 0, 0]) }), triggeredAt); | ||
event._type = "noteOn"; | ||
event.notes = sequence.notes.map((n) => new Note(n)); | ||
return event; | ||
} | ||
static fromNote(noteName, type, triggeredAt) { | ||
const event = new MidiEvent(new MIDIMessageEvent("", { data: new Uint8Array([0, 0, 0]) }), triggeredAt); | ||
if (noteName instanceof Note) { | ||
event.note = noteName; | ||
event.notes = [noteName]; | ||
} | ||
else { | ||
event.note = new Note(noteName); | ||
event.notes = [new Note(noteName)]; | ||
} | ||
@@ -30,3 +36,3 @@ event._type = type; | ||
this.data = event.data; | ||
this.defineNote(); | ||
this.defineNotes(); | ||
} | ||
@@ -45,8 +51,10 @@ get type() { | ||
} | ||
defineNote() { | ||
defineNotes() { | ||
if (!this.isNote) | ||
return; | ||
this.note = new Note(this.event); | ||
if (this.notes) | ||
return; | ||
this.notes = [new Note(this.event)]; | ||
} | ||
} | ||
//# sourceMappingURL=MidiEvent.js.map |
@@ -5,2 +5,3 @@ import { InputNode } from "tone"; | ||
import { AudioModule } from "../Module"; | ||
import Note from "../Note"; | ||
export interface Connectable { | ||
@@ -49,6 +50,7 @@ connect: (inputNode: InputNode) => void; | ||
dispose(): void; | ||
triggerAttack(midiEvent: MidiEvent, duration?: number): void; | ||
triggerRelease(midiEvent: MidiEvent): void; | ||
triggerAttackRelease(midiEvent: MidiEvent): void; | ||
midiTriggered: (midiEvent: MidiEvent, duration?: number) => void; | ||
triggerAttack(note: Note, triggeredAt: number): void; | ||
triggerRelease(note: Note, triggeredAt: number): void; | ||
triggerAttackRelease(note: Note, triggeredAt: number): void; | ||
midiTriggered: (midiEvent: MidiEvent, noteIndex?: number) => void; | ||
private triggerer; | ||
serialize(): { | ||
@@ -55,0 +57,0 @@ id: string; |
@@ -20,3 +20,3 @@ import { v4 as uuidv4 } from "uuid"; | ||
updatedAt; | ||
_props; | ||
_props = {}; | ||
constructor(internalModule, props) { | ||
@@ -30,4 +30,2 @@ this.internalModule = internalModule; | ||
return; | ||
if (!this._props) | ||
this._props = value; | ||
this.updatedAt = new Date(); | ||
@@ -72,18 +70,18 @@ Object.assign(this, value); | ||
} | ||
triggerAttack(midiEvent, duration) { | ||
triggerAttack(note, triggeredAt) { | ||
throw Error("triggerAttack not implemented"); | ||
} | ||
triggerRelease(midiEvent) { | ||
triggerRelease(note, triggeredAt) { | ||
throw Error("triggerRelease not implemented"); | ||
} | ||
triggerAttackRelease(midiEvent) { | ||
triggerAttackRelease(note, triggeredAt) { | ||
throw Error("triggerAttackRelease not implemented"); | ||
} | ||
midiTriggered = (midiEvent, duration) => { | ||
midiTriggered = (midiEvent, noteIndex) => { | ||
switch (midiEvent.type) { | ||
case "noteOn": | ||
this.triggerAttack(midiEvent, duration); | ||
this.triggerer(this.triggerAttack, midiEvent, noteIndex); | ||
break; | ||
case "noteOff": | ||
this.triggerRelease(midiEvent); | ||
this.triggerer(this.triggerRelease, midiEvent, noteIndex); | ||
break; | ||
@@ -94,2 +92,10 @@ default: | ||
}; | ||
triggerer(trigger, midiEvent, noteIndex) { | ||
const { notes, triggeredAt } = midiEvent; | ||
if (noteIndex !== undefined && this.voiceNo !== undefined) { | ||
trigger(notes[noteIndex], triggeredAt); | ||
return; | ||
} | ||
notes.forEach((note) => trigger(note, triggeredAt)); | ||
} | ||
serialize() { | ||
@@ -96,0 +102,0 @@ const klass = this.constructor; |
import { Envelope as Env } from "tone"; | ||
import Module, { Connectable, Triggerable, Voicable } from "../Base"; | ||
import PolyModule from "../PolyModule"; | ||
import MidiEvent from "../../MidiEvent"; | ||
import Note from "../../Note"; | ||
export declare const enum EnvelopeStages { | ||
@@ -31,4 +31,5 @@ Attack = "attack", | ||
setStage(stage: EnvelopeStages, value: number): void; | ||
triggerAttack(midiEvent: MidiEvent): void; | ||
triggerRelease(midiEvent: MidiEvent): void; | ||
triggerAttack: (note: Note, triggeredAt: number) => void; | ||
triggerAttackRelease: (note: Note, triggeredAt: number) => void; | ||
triggerRelease: (note: Note, triggeredAt: number) => void; | ||
private maxTime; | ||
@@ -35,0 +36,0 @@ private minTime; |
@@ -59,19 +59,19 @@ import { Envelope as Env } from "tone"; | ||
} | ||
triggerAttack(midiEvent) { | ||
const { note, triggeredAt } = midiEvent; | ||
this.activeNote = note?.fullName; | ||
triggerAttack = (note, triggeredAt) => { | ||
if (note.duration) | ||
return this.triggerAttackRelease(note, triggeredAt); | ||
this.activeNote = note.fullName; | ||
this.triggeredAt = triggeredAt; | ||
if (note?.duration) { | ||
this.internalModule.triggerAttackRelease(note.duration, triggeredAt); | ||
} | ||
else { | ||
this.internalModule.triggerAttack(triggeredAt); | ||
} | ||
} | ||
triggerRelease(midiEvent) { | ||
const { note, triggeredAt } = midiEvent; | ||
if (this.activeNote && this.activeNote !== note?.fullName) | ||
this.internalModule.triggerAttack(triggeredAt); | ||
}; | ||
triggerAttackRelease = (note, triggeredAt) => { | ||
this.activeNote = note.fullName; | ||
this.triggeredAt = triggeredAt; | ||
this.internalModule.triggerAttackRelease(note.duration, triggeredAt); | ||
}; | ||
triggerRelease = (note, triggeredAt) => { | ||
if (this.activeNote && this.activeNote !== note.fullName) | ||
return; | ||
this.internalModule.triggerRelease(triggeredAt); | ||
} | ||
}; | ||
maxTime(stage) { | ||
@@ -78,0 +78,0 @@ return stage === EnvelopeStages.Sustain ? SUSTAIN_MAX_VALUE : MAX_TIME; |
@@ -8,4 +8,6 @@ import Module, { Connectable } from "./Base"; | ||
export { default as Oscillator } from "./Oscillator"; | ||
export { default as Sequencer } from "./Sequencer"; | ||
export type { ISequence } from "./Sequencer"; | ||
export { Envelope, AmpEnvelope, FreqEnvelope, EnvelopeStages, } from "./Envelope"; | ||
export declare type AudioModule = Module<Connectable, any> | PolyModule<Module<Connectable, any>, any>; | ||
export declare function createModule(name: string, type: string, props: any): AudioModule; |
@@ -19,2 +19,3 @@ import { camelCase, upperFirst } from "lodash"; | ||
export { default as Oscillator } from "./Oscillator"; | ||
export { default as Sequencer } from "./Sequencer"; | ||
export { Envelope, AmpEnvelope, FreqEnvelope, EnvelopeStages, } from "./Envelope"; | ||
@@ -21,0 +22,0 @@ export function createModule(name, type, props) { |
@@ -1,2 +0,1 @@ | ||
import MidiEvent from "../MidiEvent"; | ||
import { Oscillator as Osc } from "tone"; | ||
@@ -33,4 +32,4 @@ import Note from "../Note"; | ||
start(): void; | ||
triggerAttack(midiEvent: MidiEvent): void; | ||
triggerRelease(midiEvent: MidiEvent): void; | ||
triggerAttack: (note: Note, triggeredAt: number) => void; | ||
triggerRelease: (note: Note) => void; | ||
private updateFrequency; | ||
@@ -37,0 +36,0 @@ private getNote; |
@@ -80,10 +80,8 @@ import { Oscillator as Osc } from "tone"; | ||
} | ||
triggerAttack(midiEvent) { | ||
if (!midiEvent.note) | ||
return; | ||
this.setNoteAt(midiEvent.note, midiEvent.triggeredAt); | ||
} | ||
triggerRelease(midiEvent) { | ||
triggerAttack = (note, triggeredAt) => { | ||
this.setNoteAt(note, triggeredAt); | ||
}; | ||
triggerRelease = (note) => { | ||
// Do nothing | ||
} | ||
}; | ||
updateFrequency(time) { | ||
@@ -90,0 +88,0 @@ if (!this.note) |
@@ -29,3 +29,3 @@ import MidiEvent from "../MidiEvent"; | ||
dispose(): void; | ||
midiTriggered: (midiEvent: MidiEvent, voiceNo?: number) => void; | ||
midiTriggered: (midiEvent: MidiEvent, voiceNo: number, noteIndex?: number) => void; | ||
serialize(): { | ||
@@ -32,0 +32,0 @@ id: string; |
@@ -63,5 +63,5 @@ import { v4 as uuidv4 } from "uuid"; | ||
} | ||
midiTriggered = (midiEvent, voiceNo = 0) => { | ||
midiTriggered = (midiEvent, voiceNo, noteIndex) => { | ||
const audioModule = this.findVoice(voiceNo); | ||
audioModule?.midiTriggered(midiEvent); | ||
audioModule?.midiTriggered(midiEvent, noteIndex); | ||
}; | ||
@@ -68,0 +68,0 @@ serialize() { |
import Module, { DummnyInternalModule } from "./Base"; | ||
import { INote } from "../Note"; | ||
import { Output } from "./IO"; | ||
interface ISequencer { | ||
export interface ISequence { | ||
active: boolean; | ||
time: string; | ||
notes: INote[]; | ||
} | ||
interface ISequencer { | ||
sequences: ISequence[][]; | ||
length: number; | ||
bars: number; | ||
} | ||
export default class Sequencer extends Module<DummnyInternalModule, ISequencer> { | ||
static moduleName: string; | ||
midiOutput: Output; | ||
private _part; | ||
private part; | ||
private barParts; | ||
constructor(name: string, props: Partial<ISequencer>); | ||
get notes(): INote[]; | ||
set notes(value: INote[]); | ||
get length(): number; | ||
set length(value: number); | ||
get bars(): number; | ||
set bars(value: number); | ||
get sequences(): ISequence[][]; | ||
set sequences(value: ISequence[][]); | ||
start(): void; | ||
stop(): void; | ||
private registerOutputs; | ||
private get part(); | ||
private updateNotesToPart; | ||
private initializePart; | ||
private adjustNumberOfBars; | ||
private adjustNumberOfSequences; | ||
private updateBarParts; | ||
private get loopEnd(); | ||
private onEvent; | ||
private onPartEvent; | ||
private onSequenceEvent; | ||
} | ||
export {}; |
@@ -1,27 +0,48 @@ | ||
import { Part } from "tone"; | ||
import { Part, Time } from "tone"; | ||
import Module, { DummnyInternalModule } from "./Base"; | ||
import MidiEvent from "../MidiEvent"; | ||
const InitialProps = { | ||
notes: [], | ||
}; | ||
const InitialProps = () => ({ | ||
sequences: [], | ||
length: 16, | ||
bars: 1, | ||
}); | ||
export default class Sequencer extends Module { | ||
static moduleName = "Sequencer"; | ||
midiOutput; | ||
_part; | ||
part; | ||
barParts = []; | ||
constructor(name, props) { | ||
super(new DummnyInternalModule(), { | ||
name, | ||
props: { ...InitialProps, ...props }, | ||
props: { ...InitialProps(), ...props }, | ||
}); | ||
this.initializePart(); | ||
this.start(); | ||
this.registerOutputs(); | ||
this.start(); | ||
} | ||
get notes() { | ||
return this._props["notes"]; | ||
get length() { | ||
return this._props["length"]; | ||
} | ||
set notes(value) { | ||
const notes = value; | ||
this._props = { ...this.props, notes }; | ||
this.updateNotesToPart(); | ||
set length(value) { | ||
this._props["length"] = value; | ||
this.adjustNumberOfSequences(); | ||
this.updateBarParts(); | ||
} | ||
get bars() { | ||
return this.props["bars"]; | ||
} | ||
set bars(value) { | ||
this._props["bars"] = value; | ||
this.adjustNumberOfBars(); | ||
this.adjustNumberOfSequences(); | ||
this.updateBarParts(); | ||
} | ||
get sequences() { | ||
return this._props["sequences"]; | ||
} | ||
set sequences(value) { | ||
const sequences = value; | ||
this._props = { ...this.props, sequences }; | ||
this.updateBarParts(); | ||
} | ||
start() { | ||
@@ -36,22 +57,68 @@ this.part.start(); | ||
} | ||
get part() { | ||
if (this._part) | ||
return this._part; | ||
this._part = new Part(this.onEvent, this.notes); | ||
this._part.loop = true; | ||
this._part.loopEnd = this.loopEnd; | ||
return this._part; | ||
initializePart() { | ||
this.part = new Part(this.onPartEvent, []); | ||
this.part.loop = true; | ||
this.part.loopEnd = this.loopEnd; | ||
this.sequences.forEach((_, i) => { | ||
this.part.add(`${i}:0:0`, i); | ||
}); | ||
} | ||
updateNotesToPart() { | ||
this.part.clear(); | ||
this.notes.forEach((note) => { | ||
this.part.add(note.time, note); | ||
adjustNumberOfBars() { | ||
const currentBar = this.sequences.length; | ||
const num = currentBar - this.bars; | ||
if (num === 0) | ||
return; | ||
if (num > 0) { | ||
this.part?.remove(`${currentBar}:0:0`); | ||
this.sequences.pop(); | ||
} | ||
else { | ||
this.part?.add(`${currentBar}:0:0`, currentBar); | ||
this.sequences.push([]); | ||
} | ||
this.adjustNumberOfBars(); | ||
} | ||
adjustNumberOfSequences(bar = 0) { | ||
if (!this.bars) | ||
return; | ||
const sequences = this.sequences[bar]; | ||
const num = sequences.length - this.length; | ||
if (num === 0) { | ||
if (bar === this.bars - 1) | ||
return; | ||
this.adjustNumberOfSequences(bar + 1); | ||
return; | ||
} | ||
if (num > 0) { | ||
sequences.pop(); | ||
} | ||
else { | ||
const index = sequences.length; | ||
sequences.push({ active: false, time: `${bar}:0:${index}`, notes: [] }); | ||
} | ||
this.adjustNumberOfSequences(bar); | ||
} | ||
updateBarParts() { | ||
this.barParts = this.sequences.map((barSeqs, bar) => { | ||
const part = new Part(this.onSequenceEvent, []); | ||
barSeqs.forEach((seq) => part.add(seq.time, seq)); | ||
return part; | ||
}); | ||
} | ||
get loopEnd() { | ||
const end = Math.max(...this.notes.map((note) => parseInt(note.time.split(":")[0], 10))) + 1; | ||
return `${end}:0:0`; | ||
return `${this.bars}:0:0`; | ||
} | ||
onEvent = (time, note) => { | ||
const event = MidiEvent.fromNote(note, "noteOn", time); | ||
onPartEvent = (time, bar) => { | ||
if (bar === null) | ||
return; | ||
const part = this.barParts[bar]; | ||
if (!part) | ||
return; | ||
part.start(time); | ||
part.stop(time + Time("1m").toSeconds()); | ||
}; | ||
onSequenceEvent = (time, sequence) => { | ||
if (!sequence.active) | ||
return; | ||
const event = MidiEvent.fromSequence(sequence, time); | ||
this.midiOutput.connections.forEach((input) => { | ||
@@ -58,0 +125,0 @@ input.pluggable(event); |
import MidiEvent from "../MidiEvent"; | ||
import Note from "../Note"; | ||
import Module, { DummnyInternalModule } from "./Base"; | ||
@@ -14,4 +15,4 @@ import { Output } from "./IO"; | ||
sendMidi(midiEvent: MidiEvent): void; | ||
triggerAttack(midiEvent: MidiEvent): void; | ||
triggerRelease(midiEvent: MidiEvent): void; | ||
triggerAttack: (note: Note) => void; | ||
triggerRelease: (note: Note) => void; | ||
serialize(): { | ||
@@ -18,0 +19,0 @@ activeNotes: string[]; |
@@ -25,3 +25,2 @@ import Engine from "../Engine"; | ||
sendMidi(midiEvent) { | ||
this.midiTriggered(midiEvent); | ||
this.midiOutput.connections.forEach((input) => { | ||
@@ -31,14 +30,10 @@ input.pluggable(midiEvent); | ||
} | ||
triggerAttack(midiEvent) { | ||
if (!midiEvent.note) | ||
return; | ||
this.activeNotes = [...this.activeNotes, midiEvent.note.fullName]; | ||
triggerAttack = (note) => { | ||
this.activeNotes = [...this.activeNotes, note.fullName]; | ||
Engine._triggerPropsUpdate(this.id, this.props); | ||
} | ||
triggerRelease(midiEvent) { | ||
if (!midiEvent.note) | ||
return; | ||
this.activeNotes = this.activeNotes.filter((name) => name !== midiEvent.note.fullName); | ||
}; | ||
triggerRelease = (note) => { | ||
this.activeNotes = this.activeNotes.filter((name) => name !== note.fullName); | ||
Engine._triggerPropsUpdate(this.id, this.props); | ||
} | ||
}; | ||
serialize() { | ||
@@ -45,0 +40,0 @@ return { |
@@ -24,3 +24,3 @@ import Module, { DummnyInternalModule, Voicable } from "./Base"; | ||
}; | ||
private findFreeVoice; | ||
private findFreeVoices; | ||
private registerInputs; | ||
@@ -37,4 +37,4 @@ private registerOutputs; | ||
constructor(name: string, props: VoiceInterface); | ||
midiTriggered: (midiEvent: MidiEvent) => void; | ||
midiTriggered: (midiEvent: MidiEvent, noteIndex?: number) => void; | ||
} | ||
export {}; |
@@ -31,9 +31,11 @@ import Module, { DummnyInternalModule } from "./Base"; | ||
midiTriggered = (midiEvent) => { | ||
let voice; | ||
let voices; | ||
switch (midiEvent.type) { | ||
case "noteOn": | ||
voice = this.findFreeVoice(); | ||
voices = this.findFreeVoices(midiEvent.notes.length); | ||
break; | ||
case "noteOff": | ||
voice = this.audioModules.find((v) => v.activeNote === midiEvent.note?.fullName); | ||
voices = midiEvent.notes.map((note) => { | ||
return this.audioModules.find((v) => v.activeNote === note.fullName); | ||
}); | ||
break; | ||
@@ -43,7 +45,11 @@ default: | ||
} | ||
if (voice === undefined) | ||
if (voices.length === 0) | ||
return; | ||
voice.midiTriggered(midiEvent); | ||
this.midiOutput.connections.forEach((input) => { | ||
input.pluggable(midiEvent, voice?.voiceNo); | ||
voices.forEach((voice, i) => { | ||
if (!voice) | ||
return; | ||
voice.midiTriggered(midiEvent, i); | ||
this.midiOutput.connections.forEach((input) => { | ||
input.pluggable(midiEvent, voice.voiceNo, i); | ||
}); | ||
}); | ||
@@ -59,12 +65,14 @@ }; | ||
} | ||
findFreeVoice() { | ||
let voice = this.audioModules.find((v) => !v.activeNote); | ||
if (!voice) { | ||
voice = this.audioModules.sort((a, b) => { | ||
findFreeVoices(num = 1) { | ||
let voices = this.audioModules.filter((v) => !v.activeNote).slice(0, num); | ||
if (voices.length === 0) { | ||
voices = this.audioModules | ||
.sort((a, b) => { | ||
if (!a || !b) | ||
return 0; | ||
return a.triggeredAt - b.triggeredAt; | ||
})[0]; | ||
}) | ||
.slice(0, num); | ||
} | ||
return voice; | ||
return voices; | ||
} | ||
@@ -100,6 +108,11 @@ registerInputs() { | ||
} | ||
midiTriggered = (midiEvent) => { | ||
const { triggeredAt, note, type } = midiEvent; | ||
midiTriggered = (midiEvent, noteIndex) => { | ||
if (this.voiceNo === undefined) | ||
throw Error("Voice without voiceNo"); | ||
if (noteIndex === undefined) | ||
return; | ||
const { triggeredAt, notes, type } = midiEvent; | ||
const note = notes[noteIndex]; | ||
if (!note) | ||
throw Error("No valid note on this event"); | ||
return; | ||
const noteName = note.fullName; | ||
@@ -106,0 +119,0 @@ switch (type) { |
@@ -1,7 +0,5 @@ | ||
import { TimeClass } from "tone"; | ||
export interface INote { | ||
note: string; | ||
time: string; | ||
duration: string; | ||
velocity?: number; | ||
duration?: string; | ||
} | ||
@@ -12,6 +10,5 @@ export default class Note { | ||
octave: number; | ||
time: TimeClass; | ||
velocity?: number; | ||
duration?: string; | ||
constructor(eventOrString: INote | MIDIMessageEvent | string, duration?: string); | ||
velocity: number; | ||
duration: string; | ||
constructor(eventOrString: Partial<INote> | MIDIMessageEvent | string); | ||
static notes(octave?: number): Note[]; | ||
@@ -18,0 +15,0 @@ get isSemi(): boolean; |
@@ -1,2 +0,1 @@ | ||
import { Time } from "tone"; | ||
import frequencyTable from "./frequencyTable"; | ||
@@ -9,7 +8,5 @@ const Notes = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]; | ||
octave; | ||
time = Time("0:0:0"); | ||
velocity = 1; | ||
duration; | ||
constructor(eventOrString, duration) { | ||
this.duration = duration; | ||
constructor(eventOrString) { | ||
if (typeof eventOrString === "string") { | ||
@@ -54,6 +51,5 @@ this.fromString(eventOrString); | ||
return { | ||
time: this.time.toBarsBeatsSixteenths(), | ||
note: this.fullName, | ||
velocity: this.velocity, | ||
duration: this.duration, | ||
velocity: 1, | ||
}; | ||
@@ -71,9 +67,8 @@ } | ||
fromProps(props) { | ||
const { note, time, duration, velocity } = props; | ||
this.fromString(note); | ||
this.time = Time(time); | ||
this.duration = duration; | ||
this.velocity = velocity; | ||
Object.assign(this, props); | ||
if (!props.note) | ||
throw Error("note props is mandatory"); | ||
this.fromString(props.note); | ||
} | ||
} | ||
//# sourceMappingURL=index.js.map |
{ | ||
"name": "@blibliki/engine", | ||
"version": "0.1.9", | ||
"version": "0.1.10", | ||
"main": "build/index.js", | ||
@@ -5,0 +5,0 @@ "types": "build/index.d.ts", |
@@ -8,4 +8,4 @@ export { default } from "./Engine"; | ||
export type { MidiDeviceInterface } from "./MidiDevice"; | ||
export type { ModuleInterface, AudioModule } from "./Module"; | ||
export type { ModuleInterface, AudioModule, ISequence } from "./Module"; | ||
export type { SerializeInterface as IOProps } from "./Module/IO"; |
import { now } from "tone"; | ||
import { ISequence } from "./Module/Sequencer"; | ||
import Note, { INote } from "./Note"; | ||
@@ -12,3 +13,3 @@ | ||
export default class MidiEvent { | ||
note?: Note; | ||
notes: Note[]; | ||
readonly triggeredAt: number; | ||
@@ -19,2 +20,13 @@ _type: EType; | ||
static fromSequence(sequence: ISequence, triggeredAt: number) { | ||
const event = new MidiEvent( | ||
new MIDIMessageEvent("", { data: new Uint8Array([0, 0, 0]) }), | ||
triggeredAt | ||
); | ||
event._type = "noteOn"; | ||
event.notes = sequence.notes.map((n) => new Note(n)); | ||
return event; | ||
} | ||
static fromNote( | ||
@@ -31,5 +43,5 @@ noteName: string | Note | INote, | ||
if (noteName instanceof Note) { | ||
event.note = noteName; | ||
event.notes = [noteName]; | ||
} else { | ||
event.note = new Note(noteName); | ||
event.notes = [new Note(noteName)]; | ||
} | ||
@@ -45,3 +57,3 @@ event._type = type; | ||
this.data = event.data; | ||
this.defineNote(); | ||
this.defineNotes(); | ||
} | ||
@@ -65,7 +77,8 @@ | ||
defineNote() { | ||
defineNotes() { | ||
if (!this.isNote) return; | ||
if (this.notes) return; | ||
this.note = new Note(this.event); | ||
this.notes = [new Note(this.event)]; | ||
} | ||
} |
@@ -7,2 +7,3 @@ import { v4 as uuidv4 } from "uuid"; | ||
import { AudioModule, PolyModule } from "../Module"; | ||
import Note from "../Note"; | ||
@@ -53,3 +54,3 @@ export interface Connectable { | ||
updatedAt: Date; | ||
_props: PropsInterface; | ||
_props: PropsInterface = {} as PropsInterface; | ||
@@ -65,3 +66,2 @@ constructor(internalModule: InternalModule, props: Partial<ModuleInterface>) { | ||
if (!value) return; | ||
if (!this._props) this._props = value; | ||
@@ -128,21 +128,21 @@ this.updatedAt = new Date(); | ||
triggerAttack(midiEvent: MidiEvent, duration?: number) { | ||
triggerAttack(note: Note, triggeredAt: number) { | ||
throw Error("triggerAttack not implemented"); | ||
} | ||
triggerRelease(midiEvent: MidiEvent) { | ||
triggerRelease(note: Note, triggeredAt: number) { | ||
throw Error("triggerRelease not implemented"); | ||
} | ||
triggerAttackRelease(midiEvent: MidiEvent) { | ||
triggerAttackRelease(note: Note, triggeredAt: number) { | ||
throw Error("triggerAttackRelease not implemented"); | ||
} | ||
midiTriggered = (midiEvent: MidiEvent, duration?: number) => { | ||
midiTriggered = (midiEvent: MidiEvent, noteIndex?: number) => { | ||
switch (midiEvent.type) { | ||
case "noteOn": | ||
this.triggerAttack(midiEvent, duration); | ||
this.triggerer(this.triggerAttack, midiEvent, noteIndex); | ||
break; | ||
case "noteOff": | ||
this.triggerRelease(midiEvent); | ||
this.triggerer(this.triggerRelease, midiEvent, noteIndex); | ||
break; | ||
@@ -154,2 +154,17 @@ default: | ||
private triggerer( | ||
trigger: Function, | ||
midiEvent: MidiEvent, | ||
noteIndex?: number | ||
) { | ||
const { notes, triggeredAt } = midiEvent; | ||
if (noteIndex !== undefined && this.voiceNo !== undefined) { | ||
trigger(notes[noteIndex], triggeredAt); | ||
return; | ||
} | ||
notes.forEach((note) => trigger(note, triggeredAt)); | ||
} | ||
serialize() { | ||
@@ -156,0 +171,0 @@ const klass = this.constructor as typeof Module; |
@@ -6,2 +6,3 @@ import { Envelope as Env } from "tone"; | ||
import MidiEvent from "../../MidiEvent"; | ||
import Note from "../../Note"; | ||
@@ -92,22 +93,21 @@ export const enum EnvelopeStages { | ||
triggerAttack(midiEvent: MidiEvent) { | ||
const { note, triggeredAt } = midiEvent; | ||
triggerAttack = (note: Note, triggeredAt: number) => { | ||
if (note.duration) return this.triggerAttackRelease(note, triggeredAt); | ||
this.activeNote = note?.fullName; | ||
this.activeNote = note.fullName; | ||
this.triggeredAt = triggeredAt; | ||
this.internalModule.triggerAttack(triggeredAt); | ||
}; | ||
if (note?.duration) { | ||
this.internalModule.triggerAttackRelease(note.duration, triggeredAt); | ||
} else { | ||
this.internalModule.triggerAttack(triggeredAt); | ||
} | ||
} | ||
triggerAttackRelease = (note: Note, triggeredAt: number) => { | ||
this.activeNote = note.fullName; | ||
this.triggeredAt = triggeredAt; | ||
this.internalModule.triggerAttackRelease(note.duration, triggeredAt); | ||
}; | ||
triggerRelease(midiEvent: MidiEvent) { | ||
const { note, triggeredAt } = midiEvent; | ||
triggerRelease = (note: Note, triggeredAt: number) => { | ||
if (this.activeNote && this.activeNote !== note.fullName) return; | ||
if (this.activeNote && this.activeNote !== note?.fullName) return; | ||
this.internalModule.triggerRelease(triggeredAt); | ||
} | ||
}; | ||
@@ -114,0 +114,0 @@ private maxTime(stage: EnvelopeStages): number { |
@@ -24,2 +24,5 @@ import { camelCase, upperFirst } from "lodash"; | ||
export { default as Oscillator } from "./Oscillator"; | ||
export { default as Sequencer } from "./Sequencer"; | ||
export type { ISequence } from "./Sequencer"; | ||
export { | ||
@@ -26,0 +29,0 @@ Envelope, |
import MidiEvent from "../MidiEvent"; | ||
import { Oscillator as Osc, ToneOscillatorType } from "tone"; | ||
import { now, Oscillator as Osc, ToneOscillatorType } from "tone"; | ||
@@ -115,10 +115,9 @@ import Note from "../Note"; | ||
triggerAttack(midiEvent: MidiEvent) { | ||
if (!midiEvent.note) return; | ||
this.setNoteAt(midiEvent.note, midiEvent.triggeredAt); | ||
} | ||
triggerAttack = (note: Note, triggeredAt: number) => { | ||
this.setNoteAt(note, triggeredAt); | ||
}; | ||
triggerRelease(midiEvent: MidiEvent) { | ||
triggerRelease = (note: Note) => { | ||
// Do nothing | ||
} | ||
}; | ||
@@ -125,0 +124,0 @@ private updateFrequency(time?: number) { |
@@ -93,5 +93,9 @@ import MidiEvent from "../MidiEvent"; | ||
midiTriggered = (midiEvent: MidiEvent, voiceNo: number = 0) => { | ||
midiTriggered = ( | ||
midiEvent: MidiEvent, | ||
voiceNo: number, | ||
noteIndex?: number | ||
) => { | ||
const audioModule = this.findVoice(voiceNo); | ||
audioModule?.midiTriggered(midiEvent); | ||
audioModule?.midiTriggered(midiEvent, noteIndex); | ||
}; | ||
@@ -98,0 +102,0 @@ |
@@ -1,14 +0,23 @@ | ||
import { Part } from "tone"; | ||
import { Part, Time } from "tone"; | ||
import Module, { DummnyInternalModule } from "./Base"; | ||
import Note, { INote } from "../Note"; | ||
import { INote } from "../Note"; | ||
import { Output } from "./IO"; | ||
import MidiEvent from "../MidiEvent"; | ||
interface ISequencer { | ||
export interface ISequence { | ||
active: boolean; | ||
time: string; | ||
notes: INote[]; | ||
} | ||
interface ISequencer { | ||
sequences: ISequence[][]; | ||
length: number; | ||
bars: number; | ||
} | ||
const InitialProps: ISequencer = { | ||
notes: [], | ||
}; | ||
const InitialProps = () => ({ | ||
sequences: [], | ||
length: 16, | ||
bars: 1, | ||
}); | ||
@@ -21,3 +30,4 @@ export default class Sequencer extends Module< | ||
midiOutput: Output; | ||
private _part: Part; | ||
private part: Part<number>; | ||
private barParts: Part<ISequence>[] = []; | ||
@@ -27,19 +37,41 @@ constructor(name: string, props: Partial<ISequencer>) { | ||
name, | ||
props: { ...InitialProps, ...props }, | ||
props: { ...InitialProps(), ...props }, | ||
}); | ||
this.initializePart(); | ||
this.start(); | ||
this.registerOutputs(); | ||
this.start(); | ||
} | ||
get notes() { | ||
return this._props["notes"]; | ||
get length() { | ||
return this._props["length"]; | ||
} | ||
set notes(value: INote[]) { | ||
const notes = value; | ||
this._props = { ...this.props, notes }; | ||
this.updateNotesToPart(); | ||
set length(value: number) { | ||
this._props["length"] = value; | ||
this.adjustNumberOfSequences(); | ||
this.updateBarParts(); | ||
} | ||
get bars() { | ||
return this.props["bars"]; | ||
} | ||
set bars(value: number) { | ||
this._props["bars"] = value; | ||
this.adjustNumberOfBars(); | ||
this.adjustNumberOfSequences(); | ||
this.updateBarParts(); | ||
} | ||
get sequences() { | ||
return this._props["sequences"]; | ||
} | ||
set sequences(value: ISequence[][]) { | ||
const sequences = value; | ||
this._props = { ...this.props, sequences }; | ||
this.updateBarParts(); | ||
} | ||
start() { | ||
@@ -57,17 +89,58 @@ this.part.start(); | ||
private get part() { | ||
if (this._part) return this._part; | ||
private initializePart() { | ||
this.part = new Part(this.onPartEvent, [] as number[]); | ||
this.part.loop = true; | ||
this.part.loopEnd = this.loopEnd; | ||
this._part = new Part(this.onEvent, this.notes); | ||
this._part.loop = true; | ||
this._part.loopEnd = this.loopEnd; | ||
this.sequences.forEach((_, i) => { | ||
this.part.add(`${i}:0:0`, i); | ||
}); | ||
} | ||
return this._part; | ||
private adjustNumberOfBars() { | ||
const currentBar = this.sequences.length; | ||
const num = currentBar - this.bars; | ||
if (num === 0) return; | ||
if (num > 0) { | ||
this.part?.remove(`${currentBar}:0:0`); | ||
this.sequences.pop(); | ||
} else { | ||
this.part?.add(`${currentBar}:0:0`, currentBar); | ||
this.sequences.push([]); | ||
} | ||
this.adjustNumberOfBars(); | ||
} | ||
private updateNotesToPart() { | ||
this.part.clear(); | ||
private adjustNumberOfSequences(bar = 0) { | ||
if (!this.bars) return; | ||
this.notes.forEach((note) => { | ||
this.part.add(note.time, note); | ||
const sequences = this.sequences[bar]; | ||
const num = sequences.length - this.length; | ||
if (num === 0) { | ||
if (bar === this.bars - 1) return; | ||
this.adjustNumberOfSequences(bar + 1); | ||
return; | ||
} | ||
if (num > 0) { | ||
sequences.pop(); | ||
} else { | ||
const index = sequences.length; | ||
sequences.push({ active: false, time: `${bar}:0:${index}`, notes: [] }); | ||
} | ||
this.adjustNumberOfSequences(bar); | ||
} | ||
private updateBarParts() { | ||
this.barParts = this.sequences.map((barSeqs, bar) => { | ||
const part = new Part(this.onSequenceEvent, [] as Array<ISequence>); | ||
barSeqs.forEach((seq) => part.add(seq.time, seq)); | ||
return part; | ||
}); | ||
@@ -77,13 +150,20 @@ } | ||
private get loopEnd() { | ||
const end = | ||
Math.max( | ||
...this.notes.map((note) => parseInt(note.time.split(":")[0], 10)) | ||
) + 1; | ||
return `${end}:0:0`; | ||
return `${this.bars}:0:0`; | ||
} | ||
private onEvent = (time: number, note: INote) => { | ||
const event = MidiEvent.fromNote(note, "noteOn", time); | ||
private onPartEvent = (time: number, bar: number | null) => { | ||
if (bar === null) return; | ||
const part = this.barParts[bar]; | ||
if (!part) return; | ||
part.start(time); | ||
part.stop(time + Time("1m").toSeconds()); | ||
}; | ||
private onSequenceEvent = (time: number, sequence: ISequence) => { | ||
if (!sequence.active) return; | ||
const event = MidiEvent.fromSequence(sequence, time); | ||
this.midiOutput.connections.forEach((input) => { | ||
@@ -90,0 +170,0 @@ input.pluggable(event); |
import Engine from "../Engine"; | ||
import MidiEvent from "../MidiEvent"; | ||
import Note from "../Note"; | ||
import Module, { DummnyInternalModule } from "./Base"; | ||
@@ -41,3 +42,2 @@ import { Output } from "./IO"; | ||
sendMidi(midiEvent: MidiEvent) { | ||
this.midiTriggered(midiEvent); | ||
this.midiOutput.connections.forEach((input) => { | ||
@@ -48,17 +48,13 @@ input.pluggable(midiEvent); | ||
triggerAttack(midiEvent: MidiEvent) { | ||
if (!midiEvent.note) return; | ||
this.activeNotes = [...this.activeNotes, midiEvent.note.fullName]; | ||
triggerAttack = (note: Note) => { | ||
this.activeNotes = [...this.activeNotes, note.fullName]; | ||
Engine._triggerPropsUpdate(this.id, this.props); | ||
} | ||
}; | ||
triggerRelease(midiEvent: MidiEvent) { | ||
if (!midiEvent.note) return; | ||
triggerRelease = (note: Note) => { | ||
this.activeNotes = this.activeNotes.filter( | ||
(name) => name !== midiEvent.note!.fullName | ||
(name) => name !== note.fullName | ||
); | ||
Engine._triggerPropsUpdate(this.id, this.props); | ||
} | ||
}; | ||
@@ -65,0 +61,0 @@ serialize() { |
@@ -46,13 +46,13 @@ import Module, { DummnyInternalModule, Voicable } from "./Base"; | ||
midiTriggered = (midiEvent: MidiEvent) => { | ||
let voice: Voice | undefined; | ||
let voices: Array<Voice | undefined>; | ||
switch (midiEvent.type) { | ||
case "noteOn": | ||
voice = this.findFreeVoice(); | ||
voices = this.findFreeVoices(midiEvent.notes.length); | ||
break; | ||
case "noteOff": | ||
voice = this.audioModules.find( | ||
(v) => v.activeNote === midiEvent.note?.fullName | ||
); | ||
voices = midiEvent.notes.map((note) => { | ||
return this.audioModules.find((v) => v.activeNote === note.fullName); | ||
}); | ||
break; | ||
@@ -63,7 +63,11 @@ default: | ||
if (voice === undefined) return; | ||
if (voices.length === 0) return; | ||
voice.midiTriggered(midiEvent); | ||
this.midiOutput.connections.forEach((input) => { | ||
input.pluggable(midiEvent, voice?.voiceNo); | ||
voices.forEach((voice, i) => { | ||
if (!voice) return; | ||
voice.midiTriggered(midiEvent, i); | ||
this.midiOutput.connections.forEach((input) => { | ||
input.pluggable(midiEvent, voice.voiceNo, i); | ||
}); | ||
}); | ||
@@ -82,14 +86,16 @@ }; | ||
private findFreeVoice(): Voice { | ||
let voice = this.audioModules.find((v) => !v.activeNote); | ||
private findFreeVoices(num: number = 1): Voice[] { | ||
let voices = this.audioModules.filter((v) => !v.activeNote).slice(0, num); | ||
if (!voice) { | ||
voice = this.audioModules.sort((a, b) => { | ||
if (!a || !b) return 0; | ||
if (voices.length === 0) { | ||
voices = this.audioModules | ||
.sort((a, b) => { | ||
if (!a || !b) return 0; | ||
return a.triggeredAt - b.triggeredAt; | ||
})[0]; | ||
return a.triggeredAt - b.triggeredAt; | ||
}) | ||
.slice(0, num); | ||
} | ||
return voice; | ||
return voices; | ||
} | ||
@@ -133,6 +139,10 @@ | ||
midiTriggered = (midiEvent: MidiEvent) => { | ||
const { triggeredAt, note, type } = midiEvent; | ||
midiTriggered = (midiEvent: MidiEvent, noteIndex?: number) => { | ||
if (this.voiceNo === undefined) throw Error("Voice without voiceNo"); | ||
if (noteIndex === undefined) return; | ||
if (!note) throw Error("No valid note on this event"); | ||
const { triggeredAt, notes, type } = midiEvent; | ||
const note = notes[noteIndex]; | ||
if (!note) return; | ||
const noteName = note.fullName; | ||
@@ -139,0 +149,0 @@ |
@@ -1,3 +0,1 @@ | ||
import { TimeClass, Time } from "tone"; | ||
import MidiEvent from "../MidiEvent"; | ||
import frequencyTable from "./frequencyTable"; | ||
@@ -11,5 +9,4 @@ | ||
note: string; | ||
time: string; | ||
duration: string; | ||
velocity?: number; | ||
duration?: string; | ||
} | ||
@@ -21,12 +18,6 @@ | ||
octave: number; | ||
time: TimeClass = Time("0:0:0"); | ||
velocity?: number = 1; | ||
duration?: string; | ||
velocity: number = 1; | ||
duration: string; | ||
constructor( | ||
eventOrString: INote | MIDIMessageEvent | string, | ||
duration?: string | ||
) { | ||
this.duration = duration; | ||
constructor(eventOrString: Partial<INote> | MIDIMessageEvent | string) { | ||
if (typeof eventOrString === "string") { | ||
@@ -77,6 +68,5 @@ this.fromString(eventOrString); | ||
return { | ||
time: this.time.toBarsBeatsSixteenths(), | ||
note: this.fullName, | ||
velocity: this.velocity, | ||
duration: this.duration, | ||
velocity: 1, | ||
}; | ||
@@ -97,10 +87,8 @@ } | ||
private fromProps(props: INote) { | ||
const { note, time, duration, velocity } = props; | ||
private fromProps(props: Partial<INote>) { | ||
Object.assign(this, props); | ||
if (!props.note) throw Error("note props is mandatory"); | ||
this.fromString(note); | ||
this.time = Time(time); | ||
this.duration = duration; | ||
this.velocity = velocity; | ||
this.fromString(props.note); | ||
} | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
200930
4740