@blibliki/engine 0.1.8 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/build/Engine.d.ts +4 -1
  2. package/build/Engine.js +16 -0
  3. package/build/Engine.js.map +1 -1
  4. package/build/MidiEvent.d.ts +10 -7
  5. package/build/MidiEvent.js +22 -9
  6. package/build/MidiEvent.js.map +1 -1
  7. package/build/Module/Base.d.ts +7 -3
  8. package/build/Module/Base.js +17 -8
  9. package/build/Module/Base.js.map +1 -1
  10. package/build/Module/Envelope/Base.d.ts +4 -3
  11. package/build/Module/Envelope/Base.js +13 -8
  12. package/build/Module/Envelope/Base.js.map +1 -1
  13. package/build/Module/IO.js +1 -1
  14. package/build/Module/IO.js.map +1 -1
  15. package/build/Module/Oscillator.d.ts +2 -3
  16. package/build/Module/Oscillator.js +5 -7
  17. package/build/Module/Oscillator.js.map +1 -1
  18. package/build/Module/PolyModule.d.ts +1 -1
  19. package/build/Module/PolyModule.js +2 -2
  20. package/build/Module/PolyModule.js.map +1 -1
  21. package/build/Module/Sequencer.d.ts +37 -0
  22. package/build/Module/Sequencer.js +128 -0
  23. package/build/Module/Sequencer.js.map +1 -0
  24. package/build/Module/VirtualMidi.d.ts +3 -2
  25. package/build/Module/VirtualMidi.js +6 -11
  26. package/build/Module/VirtualMidi.js.map +1 -1
  27. package/build/Module/VoiceScheduler.d.ts +2 -2
  28. package/build/Module/VoiceScheduler.js +29 -16
  29. package/build/Module/VoiceScheduler.js.map +1 -1
  30. package/build/Module/index.d.ts +2 -0
  31. package/build/Module/index.js +4 -0
  32. package/build/Module/index.js.map +1 -1
  33. package/build/Note/index.d.ts +10 -1
  34. package/build/Note/index.js +19 -1
  35. package/build/Note/index.js.map +1 -1
  36. package/build/index.d.ts +2 -1
  37. package/package.json +1 -1
  38. package/src/Engine.ts +25 -2
  39. package/src/MidiEvent.ts +37 -12
  40. package/src/Module/Base.ts +27 -7
  41. package/src/Module/Envelope/Base.ts +13 -8
  42. package/src/Module/IO.ts +2 -2
  43. package/src/Module/Oscillator.ts +6 -7
  44. package/src/Module/PolyModule.ts +6 -2
  45. package/src/Module/Sequencer.ts +169 -0
  46. package/src/Module/VirtualMidi.ts +7 -11
  47. package/src/Module/VoiceScheduler.ts +30 -20
  48. package/src/Module/index.ts +6 -0
  49. package/src/Note/index.ts +27 -2
  50. package/src/index.ts +2 -1
@@ -4,6 +4,7 @@ import { InputNode } from "tone";
4
4
  import { Input, Output, IOInterface } from "./IO";
5
5
  import MidiEvent from "../MidiEvent";
6
6
  import { AudioModule, PolyModule } from "../Module";
7
+ import Note from "../Note";
7
8
 
8
9
  export interface Connectable {
9
10
  connect: (inputNode: InputNode) => void;
@@ -14,6 +15,7 @@ export interface Connectable {
14
15
  export interface Triggerable {
15
16
  triggerAttack: Function;
16
17
  triggerRelease: Function;
18
+ triggerAttackRelease: Function;
17
19
  }
18
20
 
19
21
  export interface Voicable {
@@ -48,7 +50,7 @@ class Module<InternalModule extends Connectable, PropsInterface>
48
50
  outputs: Output[] = [];
49
51
  readonly voiceNo?: number;
50
52
  updatedAt: Date;
51
- _props: PropsInterface;
53
+ _props: PropsInterface = {} as PropsInterface;
52
54
 
53
55
  constructor(internalModule: InternalModule, props: Partial<ModuleInterface>) {
54
56
  this.internalModule = internalModule;
@@ -59,7 +61,6 @@ class Module<InternalModule extends Connectable, PropsInterface>
59
61
 
60
62
  set props(value: PropsInterface) {
61
63
  if (!value) return;
62
- if (!this._props) this._props = value;
63
64
 
64
65
  this.updatedAt = new Date();
65
66
 
@@ -121,27 +122,46 @@ class Module<InternalModule extends Connectable, PropsInterface>
121
122
  this.internalModule.dispose();
122
123
  }
123
124
 
124
- triggerAttack(midiEvent: MidiEvent) {
125
+ triggerAttack(note: Note, triggeredAt: number) {
125
126
  throw Error("triggerAttack not implemented");
126
127
  }
127
128
 
128
- triggerRelease(midiEvent: MidiEvent) {
129
+ triggerRelease(note: Note, triggeredAt: number) {
129
130
  throw Error("triggerRelease not implemented");
130
131
  }
131
132
 
132
- midiTriggered = (midiEvent: MidiEvent) => {
133
+ triggerAttackRelease(note: Note, triggeredAt: number) {
134
+ throw Error("triggerAttackRelease not implemented");
135
+ }
136
+
137
+ midiTriggered = (midiEvent: MidiEvent, noteIndex?: number) => {
133
138
  switch (midiEvent.type) {
134
139
  case "noteOn":
135
- this.triggerAttack(midiEvent);
140
+ this.triggerer(this.triggerAttack, midiEvent, noteIndex);
136
141
  break;
137
142
  case "noteOff":
138
- this.triggerRelease(midiEvent);
143
+ this.triggerer(this.triggerRelease, midiEvent, noteIndex);
139
144
  break;
140
145
  default:
141
146
  throw Error("This type is not a note");
142
147
  }
143
148
  };
144
149
 
150
+ private triggerer(
151
+ trigger: Function,
152
+ midiEvent: MidiEvent,
153
+ noteIndex?: number
154
+ ) {
155
+ const { notes, triggeredAt } = midiEvent;
156
+
157
+ if (noteIndex !== undefined && this.voiceNo !== undefined) {
158
+ trigger(notes[noteIndex], triggeredAt);
159
+ return;
160
+ }
161
+
162
+ notes.forEach((note) => trigger(note, triggeredAt));
163
+ }
164
+
145
165
  serialize() {
146
166
  const klass = this.constructor as typeof Module;
147
167
 
@@ -3,6 +3,7 @@ import { Envelope as Env } from "tone";
3
3
  import Module, { Connectable, Triggerable, Voicable } from "../Base";
4
4
  import PolyModule from "../PolyModule";
5
5
  import MidiEvent from "../../MidiEvent";
6
+ import Note from "../../Note";
6
7
 
7
8
  export const enum EnvelopeStages {
8
9
  Attack = "attack",
@@ -88,21 +89,25 @@ export default abstract class EnvelopeModule<EnvelopeLike extends Env>
88
89
  this.internalModule[stage] = calculatedValue;
89
90
  }
90
91
 
91
- triggerAttack(midiEvent: MidiEvent) {
92
- const { note, triggeredAt } = midiEvent;
92
+ triggerAttack = (note: Note, triggeredAt: number) => {
93
+ if (note.duration) return this.triggerAttackRelease(note, triggeredAt);
93
94
 
94
- this.activeNote = note?.fullName;
95
+ this.activeNote = note.fullName;
95
96
  this.triggeredAt = triggeredAt;
96
97
  this.internalModule.triggerAttack(triggeredAt);
97
- }
98
+ };
98
99
 
99
- triggerRelease(midiEvent: MidiEvent) {
100
- const { note, triggeredAt } = midiEvent;
100
+ triggerAttackRelease = (note: Note, triggeredAt: number) => {
101
+ this.activeNote = note.fullName;
102
+ this.triggeredAt = triggeredAt;
103
+ this.internalModule.triggerAttackRelease(note.duration, triggeredAt);
104
+ };
101
105
 
102
- if (this.activeNote && this.activeNote !== note?.fullName) return;
106
+ triggerRelease = (note: Note, triggeredAt: number) => {
107
+ if (this.activeNote && this.activeNote !== note.fullName) return;
103
108
 
104
109
  this.internalModule.triggerRelease(triggeredAt);
105
- }
110
+ };
106
111
 
107
112
  private maxTime(stage: EnvelopeStages): number {
108
113
  return stage === EnvelopeStages.Sustain ? SUSTAIN_MAX_VALUE : MAX_TIME;
package/src/Module/IO.ts CHANGED
@@ -39,11 +39,11 @@ abstract class IO {
39
39
  }
40
40
 
41
41
  plug(io: IO) {
42
+ this.connections.push(io);
43
+
42
44
  if (this.onPlug) this.onPlug(io);
43
45
 
44
46
  if (this.ioType === IOType.Output) io.plug(this);
45
-
46
- this.connections.push(io);
47
47
  }
48
48
 
49
49
  unPlug(io: IO) {
@@ -1,5 +1,5 @@
1
1
  import MidiEvent from "../MidiEvent";
2
- import { Oscillator as Osc, ToneOscillatorType } from "tone";
2
+ import { now, Oscillator as Osc, ToneOscillatorType } from "tone";
3
3
 
4
4
  import Note from "../Note";
5
5
  import Module, { Voicable } from "./Base";
@@ -112,14 +112,13 @@ class MonoOscillator extends Module<Osc, OscillatorInterface> {
112
112
  this.internalModule.start();
113
113
  }
114
114
 
115
- triggerAttack(midiEvent: MidiEvent) {
116
- if (!midiEvent.note) return;
117
- this.setNoteAt(midiEvent.note, midiEvent.triggeredAt);
118
- }
115
+ triggerAttack = (note: Note, triggeredAt: number) => {
116
+ this.setNoteAt(note, triggeredAt);
117
+ };
119
118
 
120
- triggerRelease(midiEvent: MidiEvent) {
119
+ triggerRelease = (note: Note) => {
121
120
  // Do nothing
122
- }
121
+ };
123
122
 
124
123
  private updateFrequency(time?: number) {
125
124
  if (!this.note) return;
@@ -90,9 +90,13 @@ export default abstract class PolyModule<
90
90
  });
91
91
  }
92
92
 
93
- midiTriggered = (midiEvent: MidiEvent, voiceNo: number = 0) => {
93
+ midiTriggered = (
94
+ midiEvent: MidiEvent,
95
+ voiceNo: number,
96
+ noteIndex?: number
97
+ ) => {
94
98
  const audioModule = this.findVoice(voiceNo);
95
- audioModule?.midiTriggered(midiEvent);
99
+ audioModule?.midiTriggered(midiEvent, noteIndex);
96
100
  };
97
101
 
98
102
  serialize() {
@@ -0,0 +1,169 @@
1
+ import { Part, Time } from "tone";
2
+ import Module, { DummnyInternalModule } from "./Base";
3
+ import { INote } from "../Note";
4
+ import { Output } from "./IO";
5
+ import MidiEvent from "../MidiEvent";
6
+
7
+ export interface ISequence {
8
+ active: boolean;
9
+ time: string;
10
+ notes: INote[];
11
+ }
12
+ interface ISequencer {
13
+ sequences: ISequence[][];
14
+ length: number;
15
+ bars: number;
16
+ }
17
+
18
+ const InitialProps = () => ({
19
+ sequences: [],
20
+ length: 16,
21
+ bars: 1,
22
+ });
23
+
24
+ export default class Sequencer extends Module<
25
+ DummnyInternalModule,
26
+ ISequencer
27
+ > {
28
+ static moduleName = "Sequencer";
29
+ midiOutput: Output;
30
+ private part: Part<number>;
31
+ private barParts: Part<ISequence>[] = [];
32
+
33
+ constructor(name: string, props: Partial<ISequencer>) {
34
+ super(new DummnyInternalModule(), {
35
+ name,
36
+ props: { ...InitialProps(), ...props },
37
+ });
38
+
39
+ this.initializePart();
40
+ this.start();
41
+ this.registerOutputs();
42
+ }
43
+
44
+ get length() {
45
+ return this._props["length"];
46
+ }
47
+
48
+ set length(value: number) {
49
+ this._props["length"] = value;
50
+ this.adjustNumberOfSequences();
51
+ this.updateBarParts();
52
+ }
53
+
54
+ get bars() {
55
+ return this.props["bars"];
56
+ }
57
+
58
+ set bars(value: number) {
59
+ this._props["bars"] = value;
60
+ this.adjustNumberOfBars();
61
+ this.adjustNumberOfSequences();
62
+ this.updateBarParts();
63
+ }
64
+
65
+ get sequences() {
66
+ return this._props["sequences"];
67
+ }
68
+
69
+ set sequences(value: ISequence[][]) {
70
+ const sequences = value;
71
+ this._props = { ...this.props, sequences };
72
+ this.updateBarParts();
73
+ }
74
+
75
+ start() {
76
+ this.part.start();
77
+ }
78
+
79
+ stop() {
80
+ this.part.stop();
81
+ }
82
+
83
+ private registerOutputs() {
84
+ this.midiOutput = this.registerOutput({ name: "midi out" });
85
+ }
86
+
87
+ private initializePart() {
88
+ this.part = new Part(this.onPartEvent, [] as number[]);
89
+ this.part.loop = true;
90
+ this.part.loopEnd = this.loopEnd;
91
+
92
+ this.sequences.forEach((_, i) => {
93
+ this.part.add(`${i}:0:0`, i);
94
+ });
95
+ }
96
+
97
+ private adjustNumberOfBars() {
98
+ const currentBar = this.sequences.length;
99
+ const num = currentBar - this.bars;
100
+
101
+ if (num === 0) return;
102
+
103
+ if (num > 0) {
104
+ this.part?.remove(`${currentBar}:0:0`);
105
+ this.sequences.pop();
106
+ } else {
107
+ this.part?.add(`${currentBar}:0:0`, currentBar);
108
+ this.sequences.push([]);
109
+ }
110
+
111
+ this.adjustNumberOfBars();
112
+ }
113
+
114
+ private adjustNumberOfSequences(bar = 0) {
115
+ if (!this.bars) return;
116
+
117
+ const sequences = this.sequences[bar];
118
+ const num = sequences.length - this.length;
119
+
120
+ if (num === 0) {
121
+ if (bar === this.bars - 1) return;
122
+
123
+ this.adjustNumberOfSequences(bar + 1);
124
+ return;
125
+ }
126
+
127
+ if (num > 0) {
128
+ sequences.pop();
129
+ } else {
130
+ const index = sequences.length;
131
+ sequences.push({ active: false, time: `${bar}:0:${index}`, notes: [] });
132
+ }
133
+
134
+ this.adjustNumberOfSequences(bar);
135
+ }
136
+
137
+ private updateBarParts() {
138
+ this.barParts = this.sequences.map((barSeqs, bar) => {
139
+ const part = new Part(this.onSequenceEvent, [] as Array<ISequence>);
140
+ barSeqs.forEach((seq) => part.add(seq.time, seq));
141
+
142
+ return part;
143
+ });
144
+ }
145
+
146
+ private get loopEnd() {
147
+ return `${this.bars}:0:0`;
148
+ }
149
+
150
+ private onPartEvent = (time: number, bar: number | null) => {
151
+ if (bar === null) return;
152
+
153
+ const part = this.barParts[bar];
154
+ if (!part) return;
155
+
156
+ part.start(time);
157
+ part.stop(time + Time("1m").toSeconds());
158
+ };
159
+
160
+ private onSequenceEvent = (time: number, sequence: ISequence) => {
161
+ if (!sequence.active) return;
162
+
163
+ const event = MidiEvent.fromSequence(sequence, time);
164
+
165
+ this.midiOutput.connections.forEach((input) => {
166
+ input.pluggable(event);
167
+ });
168
+ };
169
+ }
@@ -1,5 +1,6 @@
1
1
  import Engine from "../Engine";
2
2
  import MidiEvent from "../MidiEvent";
3
+ import Note from "../Note";
3
4
  import Module, { DummnyInternalModule } from "./Base";
4
5
  import { Output } from "./IO";
5
6
 
@@ -38,27 +39,22 @@ export default class VirtualMidi extends Module<
38
39
  }
39
40
 
40
41
  sendMidi(midiEvent: MidiEvent) {
41
- this.midiTriggered(midiEvent);
42
42
  this.midiOutput.connections.forEach((input) => {
43
43
  input.pluggable(midiEvent);
44
44
  });
45
45
  }
46
46
 
47
- triggerAttack(midiEvent: MidiEvent) {
48
- if (!midiEvent.note) return;
49
-
50
- this.activeNotes = [...this.activeNotes, midiEvent.note.fullName];
47
+ triggerAttack = (note: Note) => {
48
+ this.activeNotes = [...this.activeNotes, note.fullName];
51
49
  Engine._triggerPropsUpdate(this.id, this.props);
52
- }
53
-
54
- triggerRelease(midiEvent: MidiEvent) {
55
- if (!midiEvent.note) return;
50
+ };
56
51
 
52
+ triggerRelease = (note: Note) => {
57
53
  this.activeNotes = this.activeNotes.filter(
58
- (name) => name !== midiEvent.note!.fullName
54
+ (name) => name !== note.fullName
59
55
  );
60
56
  Engine._triggerPropsUpdate(this.id, this.props);
61
- }
57
+ };
62
58
 
63
59
  serialize() {
64
60
  return {
@@ -43,27 +43,31 @@ export default class VoiceScheduler extends PolyModule<
43
43
  }
44
44
 
45
45
  midiTriggered = (midiEvent: MidiEvent) => {
46
- let voice: Voice | undefined;
46
+ let voices: Array<Voice | undefined>;
47
47
 
48
48
  switch (midiEvent.type) {
49
49
  case "noteOn":
50
- voice = this.findFreeVoice();
50
+ voices = this.findFreeVoices(midiEvent.notes.length);
51
51
 
52
52
  break;
53
53
  case "noteOff":
54
- voice = this.audioModules.find(
55
- (v) => v.activeNote === midiEvent.note?.fullName
56
- );
54
+ voices = midiEvent.notes.map((note) => {
55
+ return this.audioModules.find((v) => v.activeNote === note.fullName);
56
+ });
57
57
  break;
58
58
  default:
59
59
  throw Error("This type is not a note");
60
60
  }
61
61
 
62
- if (voice === undefined) return;
62
+ if (voices.length === 0) return;
63
63
 
64
- voice.midiTriggered(midiEvent);
65
- this.midiOutput.connections.forEach((input) => {
66
- input.pluggable(midiEvent, voice?.voiceNo);
64
+ voices.forEach((voice, i) => {
65
+ if (!voice) return;
66
+
67
+ voice.midiTriggered(midiEvent, i);
68
+ this.midiOutput.connections.forEach((input) => {
69
+ input.pluggable(midiEvent, voice.voiceNo, i);
70
+ });
67
71
  });
68
72
  };
69
73
 
@@ -77,18 +81,20 @@ export default class VoiceScheduler extends PolyModule<
77
81
  };
78
82
  }
79
83
 
80
- private findFreeVoice(): Voice {
81
- let voice = this.audioModules.find((v) => !v.activeNote);
84
+ private findFreeVoices(num: number = 1): Voice[] {
85
+ let voices = this.audioModules.filter((v) => !v.activeNote).slice(0, num);
82
86
 
83
- if (!voice) {
84
- voice = this.audioModules.sort((a, b) => {
85
- if (!a || !b) return 0;
87
+ if (voices.length === 0) {
88
+ voices = this.audioModules
89
+ .sort((a, b) => {
90
+ if (!a || !b) return 0;
86
91
 
87
- return a.triggeredAt - b.triggeredAt;
88
- })[0];
92
+ return a.triggeredAt - b.triggeredAt;
93
+ })
94
+ .slice(0, num);
89
95
  }
90
96
 
91
- return voice;
97
+ return voices;
92
98
  }
93
99
 
94
100
  private registerInputs() {
@@ -127,10 +133,14 @@ class Voice extends Module<DummnyInternalModule, VoiceInterface> {
127
133
  });
128
134
  }
129
135
 
130
- midiTriggered = (midiEvent: MidiEvent) => {
131
- const { triggeredAt, note, type } = midiEvent;
136
+ midiTriggered = (midiEvent: MidiEvent, noteIndex?: number) => {
137
+ if (this.voiceNo === undefined) throw Error("Voice without voiceNo");
138
+ if (noteIndex === undefined) return;
139
+
140
+ const { triggeredAt, notes, type } = midiEvent;
141
+ const note = notes[noteIndex];
132
142
 
133
- if (!note) throw Error("No valid note on this event");
143
+ if (!note) return;
134
144
  const noteName = note.fullName;
135
145
 
136
146
  switch (type) {
@@ -13,6 +13,7 @@ import Reverb from "./Reverb";
13
13
  import Delay from "./Delay";
14
14
  import Distortion from "./Distortion";
15
15
  import BitCrusher from "./BitCrusher";
16
+ import Sequencer from "./Sequencer";
16
17
 
17
18
  export { default } from "./Base";
18
19
  export { default as PolyModule } from "./PolyModule";
@@ -20,6 +21,9 @@ export type { ModuleInterface, Connectable, Triggerable } from "./Base";
20
21
 
21
22
  export { default as Filter } from "./Filter";
22
23
  export { default as Oscillator } from "./Oscillator";
24
+ export { default as Sequencer } from "./Sequencer";
25
+ export type { ISequence } from "./Sequencer";
26
+
23
27
  export {
24
28
  Envelope,
25
29
  AmpEnvelope,
@@ -73,6 +77,8 @@ function moduleClassFromType(type: string) {
73
77
  return Distortion;
74
78
  case BitCrusher.moduleName:
75
79
  return BitCrusher;
80
+ case Sequencer.moduleName:
81
+ return Sequencer;
76
82
  default:
77
83
  throw Error(`Unknown module type ${type}`);
78
84
  }
package/src/Note/index.ts CHANGED
@@ -4,16 +4,26 @@ const Notes = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
4
4
 
5
5
  const NotesLength = Notes.length;
6
6
 
7
+ export interface INote {
8
+ note: string;
9
+ duration: string;
10
+ velocity?: number;
11
+ }
12
+
7
13
  export default class Note {
8
14
  static _notes: Note[];
9
15
  name: string;
10
16
  octave: number;
17
+ velocity: number = 1;
18
+ duration: string;
11
19
 
12
- constructor(eventOrString: MIDIMessageEvent | string) {
20
+ constructor(eventOrString: Partial<INote> | MIDIMessageEvent | string) {
13
21
  if (typeof eventOrString === "string") {
14
22
  this.fromString(eventOrString);
15
- } else {
23
+ } else if (eventOrString instanceof MIDIMessageEvent) {
16
24
  this.fromEvent(eventOrString);
25
+ } else {
26
+ this.fromProps(eventOrString);
17
27
  }
18
28
  }
19
29
 
@@ -51,6 +61,14 @@ export default class Note {
51
61
  return this.fullName;
52
62
  }
53
63
 
64
+ serialize(): INote {
65
+ return {
66
+ note: this.fullName,
67
+ velocity: this.velocity,
68
+ duration: this.duration,
69
+ };
70
+ }
71
+
54
72
  private fromString(string: string) {
55
73
  const matches = string.match(/(\w#?)(\d)?/) || [];
56
74
 
@@ -62,4 +80,11 @@ export default class Note {
62
80
  this.name = Notes[event.data[1] % 12];
63
81
  this.octave = Math.floor(event.data[1] / 12) - 2;
64
82
  }
83
+
84
+ private fromProps(props: Partial<INote>) {
85
+ Object.assign(this, props);
86
+ if (!props.note) throw Error("note props is mandatory");
87
+
88
+ this.fromString(props.note);
89
+ }
65
90
  }
package/src/index.ts CHANGED
@@ -3,7 +3,8 @@ export { default as MidiDevice } from "./MidiDevice";
3
3
  export { default as MidiDeviceManager } from "./MidiDeviceManager";
4
4
  export { default as Note } from "./Note";
5
5
 
6
+ export type { INote } from "./Note";
6
7
  export type { MidiDeviceInterface } from "./MidiDevice";
7
- export type { ModuleInterface, AudioModule } from "./Module";
8
+ export type { ModuleInterface, AudioModule, ISequence } from "./Module";
8
9
 
9
10
  export type { SerializeInterface as IOProps } from "./Module/IO";