diff --git a/package.json b/package.json
index ea7857a..f9f5c0a 100644
--- a/package.json
+++ b/package.json
@@ -37,13 +37,13 @@
"postcss": "^8.4.27",
"showdown": "^2.1.0",
"showdown-highlight": "^3.1.0",
- "superdough": "^0.9.11",
+ "superdough": "^0.9.12",
"tailwind-highlightjs": "^2.0.1",
"tailwindcss": "^3.3.3",
"tone": "^14.8.49",
"unique-names-generator": "^4.7.1",
"vite-plugin-markdown": "^2.1.0",
- "zifferjs": "^0.0.51",
+ "zifferjs": "^0.0.54",
"zyklus": "^0.1.4",
"zzfx": "^1.2.0"
}
diff --git a/src/API.ts b/src/API.ts
index 78904c0..c33509b 100644
--- a/src/API.ts
+++ b/src/API.ts
@@ -146,6 +146,7 @@ export class UserAPI {
: (this.app.selectedExample as string);
}
this.stop();
+ this.resetAllFromCache();
this.play();
};
@@ -168,6 +169,7 @@ export class UserAPI {
this.stop();
this.play();
this.app.exampleIsPlaying = true;
+ this.resetAllFromCache();
evaluateOnce(this.app, code as string);
};
diff --git a/src/Documentation.ts b/src/Documentation.ts
index e7f3c97..8cf9a61 100644
--- a/src/Documentation.ts
+++ b/src/Documentation.ts
@@ -139,13 +139,17 @@ export const showDocumentation = (app: Editor) => {
document.getElementById("documentation")?.classList.remove("hidden");
// Load and convert Markdown content from the documentation file
let style = createDocumentationStyle(app);
- let bindings = Object.keys(style).map((key) => ({
- type: "output",
- regex: new RegExp(`<${key}([^>]*)>`, "g"),
- //@ts-ignore
- replace: (match, p1) => `<${key} class="${style[key]}" ${p1}>`,
- }));
- updateDocumentationContent(app, bindings);
+
+ function update_and_assign(callback: Function) {
+ let bindings = Object.keys(style).map((key) => ({
+ type: "output",
+ regex: new RegExp(`<${key}([^>]*)>`, "g"),
+ //@ts-ignore
+ replace: (match, p1) => `<${key} class="${style[key]}" ${p1}>`,
+ }));
+ callback(bindings)
+ }
+ update_and_assign((e: Object) => updateDocumentationContent(app, e));
}
};
@@ -166,15 +170,28 @@ export const updateDocumentationContent = (app: Editor, bindings: any) => {
* @param app - The editor application.
* @param bindings - Additional bindings for the showdown converter.
*/
+ let loading_message: string = "
Loading! Clic to refresh!
";
const converter = new showdown.Converter({
emoji: true,
moreStyling: true,
backslashEscapesHTMLTags: true,
extensions: [showdownHighlight({ auto_detection: true }), ...bindings],
});
- const converted_markdown = converter.makeHtml(
- app.docs[app.currentDocumentationPane],
- );
- document.getElementById("documentation-content")!.innerHTML =
- converted_markdown;
-};
+ console.log(app.currentDocumentationPane);
+
+ function _update_and_assign(callback: Function) {
+ const converted_markdown = converter.makeHtml(
+ app.docs[app.currentDocumentationPane],
+ );
+ callback(converted_markdown)
+ }
+ _update_and_assign((e: string)=> {
+ let display_content = e === undefined ? loading_message : e;
+ document.getElementById("documentation-content")!.innerHTML = display_content;
+ })
+ if (document.getElementById("documentation-content")!.innerHTML.replace(/"/g, "'") == loading_message.replace(/"/g, "'")) {
+ setTimeout(() => {
+ updateDocumentationContent(app, bindings);
+ }, 100);
+ }
+}
\ No newline at end of file
diff --git a/src/InterfaceLogic.ts b/src/InterfaceLogic.ts
index 7ba5733..e2e39fa 100644
--- a/src/InterfaceLogic.ts
+++ b/src/InterfaceLogic.ts
@@ -525,18 +525,25 @@ export const installInterfaceLogic = (app: Editor) => {
"loading_samples",
].forEach((e) => {
let name = `docs_` + e;
- document.getElementById(name)!.addEventListener("click", async () => {
- if (name !== "docs_sample_list") {
- app.currentDocumentationPane = e;
- updateDocumentationContent(app, bindings);
- } else {
- console.log("Loading samples!");
- await loadSamples().then(() => {
- app.docs = documentation_factory(app);
+
+ // Check if the element exists
+ let element = document.getElementById(name);
+ if (element) {
+ element.addEventListener("click", async () => {
+ if (name !== "docs_sample_list") {
app.currentDocumentationPane = e;
updateDocumentationContent(app, bindings);
- });
- }
- });
+ } else {
+ console.log("Loading samples!");
+ await loadSamples().then(() => {
+ app.docs = documentation_factory(app);
+ app.currentDocumentationPane = e;
+ updateDocumentationContent(app, bindings);
+ });
+ }
+ });
+ } else {
+ console.log("Could not find element " + name);
+ }
});
};
diff --git a/src/classes/AbstractEvents.ts b/src/classes/AbstractEvents.ts
index 6799cbe..057cbf2 100644
--- a/src/classes/AbstractEvents.ts
+++ b/src/classes/AbstractEvents.ts
@@ -8,6 +8,7 @@ import {
} from "zifferjs";
import { SkipEvent } from "./SkipEvent";
import { SoundParams } from "./SoundEvent";
+import { centsToSemitones, edoToSemitones, ratiosToSemitones } from "zifferjs/src/scale";
export type EventOperation = (instance: T, ...args: any[]) => void;
@@ -294,7 +295,9 @@ export abstract class AudibleEvent extends AbstractEvent {
value = Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs];
}
this.values["pitch"] = value;
- if (this.values.key && this.values.parsedScale) this.update();
+ this.values["originalPitch"] = value;
+ this.defaultPitchKeyScale();
+ this.update();
return this;
};
@@ -309,7 +312,7 @@ export abstract class AudibleEvent extends AbstractEvent {
if (kwargs.length > 0) {
value = Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs];
}
- this.values["octave"] = value;
+ this.values["paramOctave"] = value;
if (
this.values.key &&
(this.values.pitch || this.values.pitch === 0) &&
@@ -337,6 +340,12 @@ export abstract class AudibleEvent extends AbstractEvent {
return this;
};
+ defaultPitchKeyScale() {
+ if (!this.values.key) this.values.key = 60;
+ if (!(this.values.pitch || this.values.pitch === 0)) this.values.pitch = 0;
+ if (!this.values.parsedScale) this.values.parsedScale = safeScale("major");
+ }
+
scale = (
value: string | number | (number | string)[],
...kwargs: (string | number)[]
@@ -354,12 +363,43 @@ export abstract class AudibleEvent extends AbstractEvent {
} else if (Array.isArray(value)) {
this.values.parsedScale = value.map((v) => safeScale(v));
}
- if (this.values.key && (this.values.pitch || this.values.pitch === 0)) {
- this.update();
- }
+ this.defaultPitchKeyScale();
+ this.update();
return this;
};
+ semitones(values: number|number[], ...rest: number[]) {
+ const scaleValues = typeof values === "number" ? [values, ...rest] : values;
+ this.values.parsedScale = safeScale(scaleValues);
+ this.defaultPitchKeyScale();
+ this.update();
+ return this;
+ }
+ steps = this.semitones;
+
+ cents(values: number|number[], ...rest: number[]) {
+ const scaleValues = typeof values === "number" ? [values, ...rest] : values;
+ this.values.parsedScale = safeScale(centsToSemitones(scaleValues));
+ this.defaultPitchKeyScale();
+ this.update();
+ return this;
+ }
+
+ ratios(values: number|number[], ...rest: number[]) {
+ const scaleValues = typeof values === "number" ? [values, ...rest] : values;
+ this.values.parsedScale = safeScale(ratiosToSemitones(scaleValues));
+ this.defaultPitchKeyScale();
+ this.update();
+ return this;
+ }
+
+ edo(value: number, intervals: string|number[] = new Array(value).fill(1)) {
+ this.values.parsedScale = edoToSemitones(value, intervals);
+ this.defaultPitchKeyScale();
+ this.update();
+ return this;
+ }
+
protected updateValue(key: string, value: T | T[] | null): this {
if (value == null) return this;
this.values[key] = value;
diff --git a/src/classes/MidiEvent.ts b/src/classes/MidiEvent.ts
index df8fa37..b59c8c2 100644
--- a/src/classes/MidiEvent.ts
+++ b/src/classes/MidiEvent.ts
@@ -1,7 +1,7 @@
import { AudibleEvent } from "./AbstractEvents";
import { type Editor } from "../main";
import { MidiConnection } from "../IO/MidiConnection";
-import { noteFromPc } from "zifferjs";
+import { resolvePitchClass } from "zifferjs";
import {
filterObject,
arrayOfObjectsToObjectWithArrays,
@@ -84,31 +84,32 @@ export class MidiEvent extends AudibleEvent {
};
update = (): void => {
- // Get key, pitch, parsedScale and octave from this.values object
const filteredValues = filterObject(this.values, [
"key",
"pitch",
+ "originalPitch",
"parsedScale",
- "octave",
+ "addedOctave"
]);
const events = objectWithArraysToArrayOfObjects(filteredValues, [
"parsedScale",
]);
- events.forEach((event) => {
- const [note, bend] = noteFromPc(
- (event.key as number) || "C4",
- (event.pitch as number) || 0,
- (event.parsedScale as number[]) || event.scale || "MAJOR",
- (event.octave as number) || 0,
+ events.forEach((soundEvent) => {
+ const resolvedPitchClass = resolvePitchClass(
+ (soundEvent.key || "C4"),
+ (soundEvent.originalPitch || soundEvent.pitch || 0),
+ (soundEvent.parsedScale || soundEvent.scale || "MAJOR"),
+ (soundEvent.addedOctave || 0)
);
- event.note = note;
- if (bend) event.bend = bend;
+ soundEvent.note = resolvedPitchClass.note;
+ soundEvent.pitch = resolvedPitchClass.pitch;
+ soundEvent.octave = resolvedPitchClass.octave;
});
const newArrays = arrayOfObjectsToObjectWithArrays(events) as MidiParams;
-
+
this.values.note = newArrays.note;
if (newArrays.bend) this.values.bend = newArrays.bend;
};
diff --git a/src/classes/SoundEvent.ts b/src/classes/SoundEvent.ts
index 68dda78..664aea9 100644
--- a/src/classes/SoundEvent.ts
+++ b/src/classes/SoundEvent.ts
@@ -6,7 +6,7 @@ import {
arrayOfObjectsToObjectWithArrays,
objectWithArraysToArrayOfObjects,
} from "../Utils/Generic";
-import { midiToFreq, noteFromPc } from "zifferjs";
+import { midiToFreq, resolvePitchClass } from "zifferjs";
import {
superdough,
@@ -22,10 +22,13 @@ export type SoundParams = {
note?: number | number[];
freq?: number | number[];
pitch?: number | number[];
+ originalPitch?: number | number[];
key?: string;
scale?: string;
parsedScale?: number[];
octave?: number | number[];
+ addedOctave?: number | number[];
+ pitchOctave?: number | number[];
};
export class SoundEvent extends AudibleEvent {
@@ -387,27 +390,36 @@ export class SoundEvent extends AudibleEvent {
const filteredValues = filterObject(this.values, [
"key",
"pitch",
+ "originalPitch",
"parsedScale",
+ "addedOctave",
"octave",
+ "paramOctave"
]);
const events = objectWithArraysToArrayOfObjects(filteredValues, [
"parsedScale",
]);
- events.forEach((event) => {
- const [note, _] = noteFromPc(
- (event.key as number) || "C4",
- (event.pitch as number) || 0,
- (event.parsedScale as number[]) || event.scale || "MAJOR",
- (event.octave as number) || 0,
+ events.forEach((soundEvent) => {
+ const resolvedPitchClass = resolvePitchClass(
+ (soundEvent.key || "C4"),
+ (soundEvent.originalPitch || soundEvent.pitch || 0),
+ (soundEvent.parsedScale || soundEvent.scale || "MAJOR"),
+ (soundEvent.paramOctave || 0)+(soundEvent.addedOctave || 0)
);
- event.note = note;
- event.freq = midiToFreq(note);
+ soundEvent.note = resolvedPitchClass.note;
+ soundEvent.freq = midiToFreq(resolvedPitchClass.note);
+ soundEvent.pitch = resolvedPitchClass.pitch;
+ soundEvent.octave = resolvedPitchClass.octave;
});
const newArrays = arrayOfObjectsToObjectWithArrays(events) as SoundParams;
this.values.note = newArrays.note;
this.values.freq = newArrays.freq;
+ this.values.pitch = newArrays.pitch;
+ this.values.octave = newArrays.octave;
+ this.values.pitchOctave = newArrays.pitchOctave;
+
};
out = (orbit?: number | number[]): void => {
diff --git a/src/classes/ZPlayer.ts b/src/classes/ZPlayer.ts
index 2281ff7..efdb6d2 100644
--- a/src/classes/ZPlayer.ts
+++ b/src/classes/ZPlayer.ts
@@ -186,9 +186,12 @@ export class Player extends AbstractEvent {
"freq",
"note",
"pitch",
+ "originalPitch",
"key",
"scale",
"octave",
+ "pitchOctave",
+ "addedOctave",
"parsedScale",
) as SoundParams;
@@ -205,9 +208,12 @@ export class Player extends AbstractEvent {
"freq",
"note",
"pitch",
+ "originalPitch",
"key",
"scale",
"octave",
+ "pitchOctave",
+ "addedOctave",
"parsedScale",
);
}) as SoundParams[];
@@ -236,10 +242,13 @@ export class Player extends AbstractEvent {
const obj = event.getExisting(
"note",
"pitch",
+ "originalPitch",
"bend",
"key",
"scale",
"octave",
+ "pitchOctave",
+ "addedOctave",
"parsedScale",
) as MidiParams;
if (event instanceof Pitch) {
@@ -258,11 +267,34 @@ export class Player extends AbstractEvent {
}
}
- scale(name: string) {
+ scale(name: string|number[]) {
if (this.atTheBeginning()) this.ziffers.scale(name);
return this;
}
+ semitones(values: number|number[], ...rest: number[]) {
+ values = typeof values === "number" ? [values, ...rest] : values;
+ if (this.atTheBeginning()) this.ziffers.semitones(values);
+ return this;
+ }
+
+ cents(values: number|number[], ...rest: number[]) {
+ values = typeof values === "number" ? [values, ...rest] : values;
+ if (this.atTheBeginning()) this.ziffers.cents(values);
+ return this;
+ }
+
+ ratios(values: number|number[], ...rest: number[]) {
+ values = typeof values === "number" ? [values, ...rest] : values;
+ if (this.atTheBeginning()) this.ziffers.ratios(values);
+ return this;
+ }
+
+ edo(value: number, scale: string|number[] = new Array(value).fill(1)) {
+ if (this.atTheBeginning()) this.ziffers.edo(value, scale);
+ return this;
+ }
+
key(name: string) {
if (this.atTheBeginning()) this.ziffers.key(name);
return this;
diff --git a/src/documentation/patterns/patterns.ts b/src/documentation/patterns/patterns.ts
index 9aeb618..9488879 100644
--- a/src/documentation/patterns/patterns.ts
+++ b/src/documentation/patterns/patterns.ts
@@ -122,7 +122,6 @@ beat(1)::sound(['kick', 'fsnare'].dur(3, 1))
## Manipulating notes and scales
-
- pitch(): convert a list of integers to pitch classes
${makeExample(
@@ -136,7 +135,62 @@ beat(0.25) :: snd('sine')
true,
)}
- - scale(scale: string, base note: number): Map each element of the list to the closest note of the slected scale. [0, 2, 3, 5 ].scale("major", 50) returns [50, 52, 54, 55]. You can use western scale names like (Major, Minor, Minor pentatonic ...) or [zeitler](https://ianring.com/musictheory/scales/traditions/zeitler) scale names. Alternatively you can also use the integers as used by Ian Ring in his [study of scales](https://ianring.com/musictheory/scales/).
+- semitones(number[], ...args?): Create scale from semitone intervals.
+
+${makeExample(
+ "Play pitches from scale created from semitone intervals",
+ `
+ beat(1) :: sound('gtr').pitch([0, 4, 3, 2].beat()).key(64)
+ .semitones(1, 1, 3, 1, 1, 2, 3).out()
+`,
+ true,
+)}
+
+- cents(number[], ...args?): Create scale from cent intervals.
+
+${makeExample(
+ "Play pitches from scale created from cent intervals",
+ `
+ rhythm([0.5,0.25].beat(1),14,16) :: sound('pluck')
+ .stretch(r(1,5)).pitch(r(0,6)).key(57)
+.cents(120,270,540,670,785,950,1215).out()
+`,
+ true,
+)}
+
+- ratios(number[], ...args?): Create scale from ratios.
+
+${makeExample(
+ "Play pitches from scale created from ratios",
+ `
+ rhythm([0.5,0.25].beat(0.25),5,7) :: sound('east:3')
+ .pitch([0,1,2,3,4,5,6,7,8,9,10,11].beat(0.25)).key(67)
+.ratios(2/11,4/11,6/11,8/11,10/11,11/11).out()
+`,
+ true,
+)}
+
+- edo(number, scale?: string|number[]): Create scale from equal divisions of the octave. Creates chromatic scale by default.
+
+${makeExample(
+ "Play pitches from scale created from equal divisions of the octave",
+ `
+ z0("e bd bd ").sound().out()
+flipbar(1) :: rhythm(.25,14,16) :: sound("ST10:30").stretch(3).gain(0.5)
+.pitch([0,10,r(20,40),r(100,200),r(-200,200),r(200,300),200,r(3,666)].beat([1.0,0.5,0.25].bar(6)))
+ .octave(r(-6,6))
+ .edo(666,"rocritonic")
+ .out()
+rhythm(2.0,26,32) :: sound("ST20").n([22,5,24,34,31,5,11,19].pick()).stretch(rI(1,6))
+.pitch(rI(127,300))
+ .edo(666)
+ .out()
+`,
+ true,
+)}
+
+
+- scale(scale: string, base note: number): Map each element of the list to the closest note of the slected scale. [0, 2, 3, 5 ].scale("major", 50) returns [50, 52, 54, 55]. You can use western scale names like (Major, Minor, Minor pentatonic ...) or [zeitler](https://ianring.com/musictheory/scales/traditions/zeitler) scale names. Alternatively you can also use the integers as used by Ian Ring in his [study of scales](https://ianring.com/musictheory/scales/).
${makeExample(
"Mapping the note array to the E3 major scale",
diff --git a/yarn.lock b/yarn.lock
index d17fe68..e7de0e7 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3455,10 +3455,10 @@ sucrase@^3.32.0:
pirates "^4.0.1"
ts-interface-checker "^0.1.9"
-superdough@^0.9.11:
- version "0.9.11"
- resolved "https://registry.yarnpkg.com/superdough/-/superdough-0.9.11.tgz#3a3842a47d6340477f77d39077303f05e15274dd"
- integrity sha512-s0SNSg/EJHwp2sUnE2A7pTZ0G2luiSEq9NVKJvodjJw11Tn0fOp9XcnegNXINYz3U6mAsUYRoeaj4NmuTL13fA==
+superdough@^0.9.12:
+ version "0.9.12"
+ resolved "https://registry.yarnpkg.com/superdough/-/superdough-0.9.12.tgz#455f8860bc13cffbe1d8f391919e8f1dba1ff0b5"
+ integrity sha512-rsdCoYk5rLYster4tE5mSGjotf/TNP3gPpsuK4hxTZNxL92TkdEcbPFLnJfky5oMQJtpRY1XqAXUx3htLbHEZA==
dependencies:
nanostores "^0.8.1"
@@ -4028,10 +4028,10 @@ yaml@^2.1.1:
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.1.tgz#02fe0975d23cd441242aa7204e09fc28ac2ac33b"
integrity sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==
-zifferjs@^0.0.51:
- version "0.0.51"
- resolved "https://registry.yarnpkg.com/zifferjs/-/zifferjs-0.0.51.tgz#567efb39a1675fa622a1edc54d671318b58c43c7"
- integrity sha512-0uYFZNsdUL4wOv8x37HLenoEOKmcMi1hVpZIWXQwx9AsTeGvZqgVak0y02MSne5S5dMFmAO5s5ZXokc4kzbCeQ==
+zifferjs@^0.0.54:
+ version "0.0.54"
+ resolved "https://registry.yarnpkg.com/zifferjs/-/zifferjs-0.0.54.tgz#2dd4b43820f85d797c13dd35d933476ecacdb146"
+ integrity sha512-vo1I12VvW4yFdVJTVnrfOxeOpWq7tIMZ67BfXxcK0t9GveLi+3GrF1zjowq8WCDssVgw+lQHEjdGVhO5FbK3RA==
zyklus@^0.1.4:
version "0.1.4"