diff --git a/package.json b/package.json index d8a3703..5c8230d 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "tone": "^14.8.49", "unique-names-generator": "^4.7.1", "vite-plugin-markdown": "^2.1.0", - "zifferjs": "^0.0.19", + "zifferjs": "^0.0.21", "zzfx": "^1.2.0" } } diff --git a/src/API.ts b/src/API.ts index 0e2d5ef..e366308 100644 --- a/src/API.ts +++ b/src/API.ts @@ -5,7 +5,7 @@ import { DrunkWalk } from "./Utils/Drunk"; import { scale } from "./Scales"; import { Editor } from "./main"; import { SoundEvent } from "./classes/SoundEvent"; -import { NoteEvent } from "./classes/MidiEvent"; +import { MidiEvent } from "./classes/MidiEvent"; import { LRUCache } from "lru-cache"; import { InputOptions, Player } from "./classes/ZPlayer"; import { @@ -316,7 +316,7 @@ export class UserAPI { value: number | object = 60, velocity?: number, channel?: number - ): NoteEvent => { + ): MidiEvent => { /** * Sends a MIDI note to the current MIDI output. * @@ -342,7 +342,7 @@ export class UserAPI { value["channel"] = channel; } - return new NoteEvent(value, this.app); + return new MidiEvent(value, this.app); }; public sysex = (data: Array): void => { diff --git a/src/TransportNode.js b/src/TransportNode.js index 0d3d1df..42c8a47 100644 --- a/src/TransportNode.js +++ b/src/TransportNode.js @@ -8,7 +8,7 @@ export class TransportNode extends AudioWorkletNode { this.app = application this.port.addEventListener("message", this.handleMessage); this.port.start(); - this.timeviewer = document.getElementById("timeviewer"); + this.timeviewer = document.getElementById("timeviewer"); } /** @type {(this: MessagePort, ev: MessageEvent) => any} */ @@ -18,7 +18,7 @@ export class TransportNode extends AudioWorkletNode { this.app.clock.tick++ const futureTimeStamp = this.app.clock.convertTicksToTimeposition(this.app.clock.tick); this.app.clock.time_position = futureTimeStamp; - this.timeviewer.innerHTML = `${zeroPad(futureTimeStamp.bar, 2)}:${futureTimeStamp.beat+1}:${zeroPad(futureTimeStamp.pulse, 2)}`; + this.timeviewer.innerHTML = `${zeroPad(futureTimeStamp.bar, 2)}:${futureTimeStamp.beat+1}:${zeroPad(futureTimeStamp.pulse, 2)}`; if (this.app.exampleIsPlaying) { tryEvaluate(this.app, this.app.example_buffer); diff --git a/src/classes/MidiEvent.ts b/src/classes/MidiEvent.ts index 76eef56..2321df0 100644 --- a/src/classes/MidiEvent.ts +++ b/src/classes/MidiEvent.ts @@ -3,7 +3,7 @@ import { type Editor } from "../main"; import { MidiConnection } from "../IO/MidiConnection"; import { midiToFreq, noteFromPc } from "zifferjs"; -export class NoteEvent extends AudibleEvent { +export class MidiEvent extends AudibleEvent { midiConnection: MidiConnection; constructor(input: number | object, public app: Editor) { @@ -13,6 +13,11 @@ export class NoteEvent extends AudibleEvent { this.midiConnection = app.api.MidiConnection; } + chord = (value: number[]): this => { + this.values["chord"] = value; + return this; + }; + note = (value: number): this => { this.values["note"] = value; return this; @@ -74,29 +79,40 @@ export class NoteEvent extends AudibleEvent { }; out = (): void => { - const note = this.values.note ? this.values.note : 60; - const channel = this.values.channel ? this.values.channel : 0; - const velocity = this.values.velocity ? this.values.velocity : 100; + function play(note: number, event: MidiEvent): void { + const channel = event.values.channel ? event.values.channel : 0; + const velocity = event.values.velocity ? event.values.velocity : 100; - const sustain = this.values.sustain - ? this.values.sustain * - this.app.clock.pulse_duration * - this.app.api.ppqn() - : this.app.clock.pulse_duration * this.app.api.ppqn(); + const sustain = event.values.sustain + ? event.values.sustain * + event.app.clock.pulse_duration * + event.app.api.ppqn() + : event.app.clock.pulse_duration * event.app.api.ppqn(); - const bend = this.values.bend ? this.values.bend : undefined; + const bend = event.values.bend ? event.values.bend : undefined; - const port = this.values.port - ? this.midiConnection.getMidiOutputIndex(this.values.port) - : this.midiConnection.getCurrentMidiPortIndex(); + const port = event.values.port + ? event.midiConnection.getMidiOutputIndex(event.values.port) + : event.midiConnection.getCurrentMidiPortIndex(); - this.midiConnection.sendMidiNote( - note, - channel, - velocity, - sustain, - port, - bend - ); + event.midiConnection.sendMidiNote( + note, + channel, + velocity, + sustain, + port, + bend + ); + } + + if(this.values.chord) { + this.values.chord.forEach((note: number) => { + play(note, this); + }); + } else { + const note = this.values.note ? this.values.note : 60; + play(note, this); + } + }; } diff --git a/src/classes/SoundEvent.ts b/src/classes/SoundEvent.ts index 90de244..cd36250 100644 --- a/src/classes/SoundEvent.ts +++ b/src/classes/SoundEvent.ts @@ -90,6 +90,7 @@ export class SoundEvent extends AudibleEvent { return this; }; public sound = (value: string) => this.updateValue("s", value); + public chord = (value: number[]) => this.updateValue("chord", value); public snd = this.sound; public nudge = (value: number) => this.updateValue("nudge", value); public cut = (value: number) => this.updateValue("cut", value); @@ -162,7 +163,15 @@ export class SoundEvent extends AudibleEvent { this.values.freq = midiToFreq(note); }; - out = (): object => { - return superdough(this.values, 1 / 4, this.values.dur || 0.5); + out = (): void => { + if(this.values.chord) { + this.values.chord.forEach((freq: number) => { + const copy = {...this.values}; + copy.freq = freq; + superdough(copy, 1 / 4, this.values.dur || 0.5); + }); + } else { + superdough(this.values, 1 / 4, this.values.dur || 0.5); + } }; } diff --git a/src/classes/ZPlayer.ts b/src/classes/ZPlayer.ts index fc573aa..75313f9 100644 --- a/src/classes/ZPlayer.ts +++ b/src/classes/ZPlayer.ts @@ -3,7 +3,7 @@ import { Editor } from "../main"; import { Event } from "./AbstractEvents"; import { SkipEvent } from "./SkipEvent"; import { SoundEvent } from "./SoundEvent"; -import { NoteEvent } from "./MidiEvent"; +import { MidiEvent } from "./MidiEvent"; import { RestEvent } from "./RestEvent"; export type InputOptions = { [key: string]: string | number }; @@ -144,6 +144,9 @@ export class Player extends Event { "parsedScale" ); return new SoundEvent(obj, this.app).sound(name); + } else if(event instanceof Chord) { + const pitches = event.freqs(); + return new SoundEvent(event, this.app).chord(pitches).sound(name); } else if (event instanceof ZRest) { return RestEvent.createRestProxy(event.duration, this.app); } @@ -155,20 +158,23 @@ export class Player extends Event { midi(value: number | undefined = undefined) { if (this.areWeThereYet()) { const event = this.next() as Pitch | Chord | ZRest; + const obj = event.getExisting( + "note", + "pitch", + "bend", + "key", + "scale", + "octave", + "parsedScale" + ); if (event instanceof Pitch) { - const obj = event.getExisting( - "note", - "pitch", - "bend", - "key", - "scale", - "octave", - "parsedScale" - ); - const note = new NoteEvent(obj, this.app); + const note = new MidiEvent(obj, this.app); return value ? note.note(value) : note; } else if (event instanceof ZRest) { return RestEvent.createRestProxy(event.duration, this.app); + } else if (event instanceof Chord) { + const pitches = event.notes(); + return new MidiEvent(obj, this.app).chord(pitches); } } else { return SkipEvent.createSkipProxy(); diff --git a/src/documentation/ziffers.ts b/src/documentation/ziffers.ts index 83d3974..e01c828 100644 --- a/src/documentation/ziffers.ts +++ b/src/documentation/ziffers.ts @@ -101,6 +101,41 @@ z1('w [0 [5 [3 7]]] h [0 4]') false )} +## Chords + +Chords can be build by grouping pitches or using roman numeral notation, or by using named chords. + +${makeExample( + "Chords from pitches", + ` +z1('q 024 468').sound('sine').scale("minor").out() +` +)} + +${makeExample( + "Chords from roman numerals", + ` + z1('i i v vii vi iv iv v').sound("pad").out(); +` +)} + +${makeExample( + "Named chords with repeats", + ` + z1('e C9:4 Emin:4 F7:4 Emaj:4') + .sound("stab").sustain(2.0).out() +` +)} + +${makeExample( + "Transposing chords", + ` + z1('q Fmaj Amin Dmin Cmaj Cdim') + .key(["F3","E3","D3","E3"].div(3)) + .sound('sawtooth').out() +` +)} + ## Algorithmic operations Ziffers provides shorthands for **many** numeric and algorithimic operations such as evaluating random numbers and creating sequences using list operations: diff --git a/src/themes/toposTheme.ts b/src/themes/toposTheme.ts index 3fbf239..40c48de 100644 --- a/src/themes/toposTheme.ts +++ b/src/themes/toposTheme.ts @@ -63,7 +63,7 @@ export const toposDarkTheme = EditorView.theme( }, ".cm-activeLine": { // backgroundColor: highlightBackground - backgroundColor: "rgb(76,86,106, 0.4)", + backgroundColor: "rgb(76,76,106, 0.1)", }, ".cm-selectionMatch": { backgroundColor: base04, diff --git a/tsconfig.json b/tsconfig.json index a28617f..1ad475b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,6 @@ { "compilerOptions": { "target": "ES2020", - "useDefineForClassFields": true, "module": "ESNext", "lib": ["ES2020", "DOM", "DOM.Iterable"], "skipLibCheck": true, diff --git a/yarn.lock b/yarn.lock index a0c2402..ec0ad2f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1451,10 +1451,10 @@ yaml@^2.1.1: resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.1.tgz#02fe0975d23cd441242aa7204e09fc28ac2ac33b" integrity sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ== -zifferjs@^0.0.19: - version "0.0.19" - resolved "https://registry.yarnpkg.com/zifferjs/-/zifferjs-0.0.19.tgz#a3e1fc04b12cef1f59a78f3d23229b072d9f0a60" - integrity sha512-mdrfac/ryDF1MEC1iMCV9KTj36AXNVsiDoYiDEA5ZCYFuHqfzS5s2aCh2iK6teyRKTIagE2IvZCrsTacB5GBBg== +zifferjs@^0.0.21: + version "0.0.21" + resolved "https://registry.yarnpkg.com/zifferjs/-/zifferjs-0.0.21.tgz#5df0c6541ae189b2515b4eb4856eb9d2c0610778" + integrity sha512-wJat1nbeCJ1j7+5YpeB5MnZdbyFfAwVRB8Ei+cgViEtjidwe4oCLtNq1p8Gq1PjjpSzr32YR57egCdME2Lb5qg== zzfx@^1.2.0: version "1.2.0"