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"