diff --git a/package.json b/package.json index c299daf1..41225628 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "test-coverage": "vitest --coverage", "bootstrap": "lerna bootstrap", "setup": "npm i && npm run bootstrap && cd repl && npm i && cd ../tutorial && npm i", - "snapshot": "cd repl && npm run snapshot", + "snapshot": "vitest run -u --silent", "repl": "cd repl && npm run dev", "osc": "cd packages/osc && npm run server", "build": "rm -rf out && cd repl && npm run build && cd ../tutorial && npm run build", diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index c3211ee7..2cab88c3 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -1050,7 +1050,7 @@ export class Pattern { * @param {function} func function to apply * @returns Pattern * @example - * note("c3 d3 e3 g3").every(4, x=>x.rev()) + * note("c3 d3 e3 g3").each(4, x=>x.rev()) */ each(n, func) { const pat = this; diff --git a/packages/core/util.mjs b/packages/core/util.mjs index 7b8a72d1..f6b7d8b8 100644 --- a/packages/core/util.mjs +++ b/packages/core/util.mjs @@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th */ // returns true if the given string is a note -export const isNote = (name) => /^[a-gA-G][#b]*[0-9]$/.test(name); +export const isNote = (name) => /^[a-gA-G][#bs]*[0-9]$/.test(name); export const tokenizeNote = (note) => { if (typeof note !== 'string') { return []; diff --git a/packages/react/dist/index.cjs.js b/packages/react/dist/index.cjs.js index ca537205..cd672e9e 100644 --- a/packages/react/dist/index.cjs.js +++ b/packages/react/dist/index.cjs.js @@ -1 +1 @@ -"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const t=require("react"),G=require("@uiw/react-codemirror"),b=require("@codemirror/view"),R=require("@codemirror/state"),Q=require("@codemirror/lang-javascript"),o=require("@lezer/highlight"),W=require("@uiw/codemirror-themes"),X=require("react-hook-inview"),V=require("@strudel.cycles/webaudio"),Y=require("@strudel.cycles/core"),Z=require("@strudel.cycles/transpiler"),j=e=>e&&typeof e=="object"&&"default"in e?e:{default:e},n=j(t),ee=j(G),te=W.createTheme({theme:"dark",settings:{background:"#222",foreground:"#75baff",caret:"#ffcc00",selection:"rgba(128, 203, 196, 0.5)",selectionMatch:"#036dd626",lineHighlight:"#00000050",gutterBackground:"transparent",gutterForeground:"#8a919966"},styles:[{tag:o.tags.keyword,color:"#c792ea"},{tag:o.tags.operator,color:"#89ddff"},{tag:o.tags.special(o.tags.variableName),color:"#eeffff"},{tag:o.tags.typeName,color:"#c3e88d"},{tag:o.tags.atom,color:"#f78c6c"},{tag:o.tags.number,color:"#c3e88d"},{tag:o.tags.definition(o.tags.variableName),color:"#82aaff"},{tag:o.tags.string,color:"#c3e88d"},{tag:o.tags.special(o.tags.string),color:"#c3e88d"},{tag:o.tags.comment,color:"#7d8799"},{tag:o.tags.variableName,color:"#c792ea"},{tag:o.tags.tagName,color:"#c3e88d"},{tag:o.tags.bracket,color:"#525154"},{tag:o.tags.meta,color:"#ffcb6b"},{tag:o.tags.attributeName,color:"#c792ea"},{tag:o.tags.propertyName,color:"#c792ea"},{tag:o.tags.className,color:"#decb6b"},{tag:o.tags.invalid,color:"#ffffff"}]});const P=R.StateEffect.define(),re=R.StateField.define({create(){return b.Decoration.none},update(e,r){try{for(let a of r.effects)if(a.is(P))if(a.value){const s=b.Decoration.mark({attributes:{style:"background-color: #FFCA2880"}});e=b.Decoration.set([s.range(0,r.newDoc.length)])}else e=b.Decoration.set([]);return e}catch(a){return console.warn("flash error",a),e}},provide:e=>b.EditorView.decorations.from(e)}),B=e=>{e.dispatch({effects:P.of(!0)}),setTimeout(()=>{e.dispatch({effects:P.of(!1)})},200)},N=R.StateEffect.define(),oe=R.StateField.define({create(){return b.Decoration.none},update(e,r){try{for(let a of r.effects)if(a.is(N)){const s=a.value.map(c=>(c.context.locations||[]).map(({start:m,end:u})=>{const d=c.context.color||"#FFCA28";let i=r.newDoc.line(m.line).from+m.column,l=r.newDoc.line(u.line).from+u.column;const h=r.newDoc.length;return i>h||l>h?void 0:b.Decoration.mark({attributes:{style:`outline: 1.5px solid ${d};`}}).range(i,l)})).flat().filter(Boolean)||[];e=b.Decoration.set(s,!0)}return e}catch{return b.Decoration.set([])}},provide:e=>b.EditorView.decorations.from(e)}),ae=[Q.javascript(),te,oe,re];function I({value:e,onChange:r,onViewChanged:a,onSelectionChange:s,options:c,editorDidMount:m}){const u=t.useCallback(l=>{r?.(l)},[r]),d=t.useCallback(l=>{a?.(l)},[a]),i=t.useCallback(l=>{l.selectionSet&&s&&s?.(l.state.selection)},[s]);return n.default.createElement(n.default.Fragment,null,n.default.createElement(ee.default,{value:e,onChange:u,onCreateEditor:d,onUpdate:i,extensions:ae}))}function H(...e){return e.filter(Boolean).join(" ")}function K({view:e,pattern:r,active:a,getTime:s}){const c=t.useRef([]),m=t.useRef();t.useEffect(()=>{if(e)if(r&&a){let d=function(){try{const i=s(),h=[Math.max(m.current||i,i-1/10,0),i+1/60];m.current=h[1],c.current=c.current.filter(p=>p.whole.end>i);const v=r.queryArc(...h).filter(p=>p.hasOnset());c.current=c.current.concat(v),e.dispatch({effects:N.of(c.current)})}catch{e.dispatch({effects:N.of([])})}u=requestAnimationFrame(d)},u=requestAnimationFrame(d);return()=>{cancelAnimationFrame(u)}}else c.current=[],e.dispatch({effects:N.of([])})},[r,a,e])}const ne="_container_3i85k_1",se="_header_3i85k_5",ce="_buttons_3i85k_9",ie="_button_3i85k_9",le="_buttonDisabled_3i85k_17",ue="_error_3i85k_21",de="_body_3i85k_25",E={container:ne,header:se,buttons:ce,button:ie,buttonDisabled:le,error:ue,body:de};function z({type:e}){return n.default.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",className:"sc-h-5 sc-w-5",viewBox:"0 0 20 20",fill:"currentColor"},{refresh:n.default.createElement("path",{fillRule:"evenodd",d:"M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z",clipRule:"evenodd"}),play:n.default.createElement("path",{fillRule:"evenodd",d:"M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z",clipRule:"evenodd"}),pause:n.default.createElement("path",{fillRule:"evenodd",d:"M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z",clipRule:"evenodd"})}[e])}function O({defaultOutput:e,interval:r,getTime:a,evalOnMount:s=!1,initialCode:c="",autolink:m=!1,beforeEval:u,afterEval:d,onEvalError:i,onToggle:l}){const[h,v]=t.useState(),[p,D]=t.useState(),[w,k]=t.useState(c),[y,q]=t.useState(),[F,_]=t.useState(),[C,A]=t.useState(!1),M=w!==y,{scheduler:f,evaluate:x,start:U,stop:J,pause:$}=t.useMemo(()=>Y.repl({interval:r,defaultOutput:e,onSchedulerError:v,onEvalError:g=>{D(g),i?.(g)},getTime:a,transpiler:Z.transpiler,beforeEval:({code:g})=>{k(g),u?.()},afterEval:({pattern:g,code:T})=>{q(T),_(g),D(),v(),m&&(window.location.hash="#"+encodeURIComponent(btoa(T))),d?.()},onToggle:g=>{A(g),l?.(g)}}),[e,r,a]),S=t.useCallback(async(g=!0)=>x(w,g),[x,w]),L=t.useRef();return t.useEffect(()=>{!L.current&&s&&w&&(L.current=!0,S())},[S,s,w]),t.useEffect(()=>()=>{f.stop()},[f]),{code:w,setCode:k,error:h||p,schedulerError:h,scheduler:f,evalError:p,evaluate:x,activateCode:S,activeCode:y,isDirty:M,pattern:F,started:C,start:U,stop:J,pause:$,togglePlay:async()=>{C?f.pause():await S()}}}const fe=()=>V.getAudioContext().currentTime;function ge({tune:e,hideOutsideView:r=!1,init:a,enableKeyboard:s}){const{code:c,setCode:m,evaluate:u,activateCode:d,error:i,isDirty:l,activeCode:h,pattern:v,started:p,scheduler:D,togglePlay:w,stop:k}=O({initialCode:e,defaultOutput:V.webaudioOutput,getTime:fe}),[y,q]=t.useState(),[F,_]=X.useInView({threshold:.01}),C=t.useRef(),A=t.useMemo(()=>((_||!r)&&(C.current=!0),_||C.current),[_,r]);return K({view:y,pattern:v,active:p&&!h?.includes("strudel disable-highlighting"),getTime:()=>D.getPhase()}),t.useLayoutEffect(()=>{if(s){const M=async f=>{(f.ctrlKey||f.altKey)&&(f.code==="Enter"?(f.preventDefault(),B(y),await d()):f.code==="Period"&&(k(),f.preventDefault()))};return window.addEventListener("keydown",M,!0),()=>window.removeEventListener("keydown",M,!0)}},[s,v,c,u,k,y]),n.default.createElement("div",{className:E.container,ref:F},n.default.createElement("div",{className:E.header},n.default.createElement("div",{className:E.buttons},n.default.createElement("button",{className:H(E.button,p?"sc-animate-pulse":""),onClick:()=>w()},n.default.createElement(z,{type:p?"pause":"play"})),n.default.createElement("button",{className:H(l?E.button:E.buttonDisabled),onClick:()=>d()},n.default.createElement(z,{type:"refresh"}))),i&&n.default.createElement("div",{className:E.error},i.message)),n.default.createElement("div",{className:E.body},A&&n.default.createElement(I,{value:c,onChange:m,onViewChanged:q})))}function me(e){return t.useEffect(()=>(window.addEventListener("message",e),()=>window.removeEventListener("message",e)),[e]),t.useCallback(r=>window.postMessage(r,"*"),[])}const he=e=>t.useLayoutEffect(()=>(window.addEventListener("keydown",e,!0),()=>window.removeEventListener("keydown",e,!0)),[e]);exports.CodeMirror=I;exports.MiniRepl=ge;exports.cx=H;exports.flash=B;exports.useHighlighting=K;exports.useKeydown=he;exports.usePostMessage=me;exports.useStrudel=O; +"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const t=require("react"),X=require("@uiw/react-codemirror"),p=require("@codemirror/view"),A=require("@codemirror/state"),Y=require("@codemirror/lang-javascript"),o=require("@lezer/highlight"),Z=require("@uiw/codemirror-themes"),ee=require("react-hook-inview"),B=require("@strudel.cycles/webaudio"),te=require("@strudel.cycles/core"),re=require("@strudel.cycles/transpiler"),I=e=>e&&typeof e=="object"&&"default"in e?e:{default:e},n=I(t),oe=I(X),ae=Z.createTheme({theme:"dark",settings:{background:"#222",foreground:"#75baff",caret:"#ffcc00",selection:"rgba(128, 203, 196, 0.5)",selectionMatch:"#036dd626",lineHighlight:"#00000050",gutterBackground:"transparent",gutterForeground:"#8a919966"},styles:[{tag:o.tags.keyword,color:"#c792ea"},{tag:o.tags.operator,color:"#89ddff"},{tag:o.tags.special(o.tags.variableName),color:"#eeffff"},{tag:o.tags.typeName,color:"#c3e88d"},{tag:o.tags.atom,color:"#f78c6c"},{tag:o.tags.number,color:"#c3e88d"},{tag:o.tags.definition(o.tags.variableName),color:"#82aaff"},{tag:o.tags.string,color:"#c3e88d"},{tag:o.tags.special(o.tags.string),color:"#c3e88d"},{tag:o.tags.comment,color:"#7d8799"},{tag:o.tags.variableName,color:"#c792ea"},{tag:o.tags.tagName,color:"#c3e88d"},{tag:o.tags.bracket,color:"#525154"},{tag:o.tags.meta,color:"#ffcb6b"},{tag:o.tags.attributeName,color:"#c792ea"},{tag:o.tags.propertyName,color:"#c792ea"},{tag:o.tags.className,color:"#decb6b"},{tag:o.tags.invalid,color:"#ffffff"}]});const L=A.StateEffect.define(),ne=A.StateField.define({create(){return p.Decoration.none},update(e,r){try{for(let a of r.effects)if(a.is(L))if(a.value){const s=p.Decoration.mark({attributes:{style:"background-color: #FFCA2880"}});e=p.Decoration.set([s.range(0,r.newDoc.length)])}else e=p.Decoration.set([]);return e}catch(a){return console.warn("flash error",a),e}},provide:e=>p.EditorView.decorations.from(e)}),K=e=>{e.dispatch({effects:L.of(!0)}),setTimeout(()=>{e.dispatch({effects:L.of(!1)})},200)},x=A.StateEffect.define(),se=A.StateField.define({create(){return p.Decoration.none},update(e,r){try{for(let a of r.effects)if(a.is(x)){const s=a.value.map(c=>(c.context.locations||[]).map(({start:g,end:d})=>{const f=c.context.color||"#FFCA28";let i=r.newDoc.line(g.line).from+g.column,l=r.newDoc.line(d.line).from+d.column;const m=r.newDoc.length;return i>m||l>m?void 0:p.Decoration.mark({attributes:{style:`outline: 1.5px solid ${f};`}}).range(i,l)})).flat().filter(Boolean)||[];e=p.Decoration.set(s,!0)}return e}catch{return p.Decoration.set([])}},provide:e=>p.EditorView.decorations.from(e)}),ce=[Y.javascript(),ae,se,ne];function O({value:e,onChange:r,onViewChanged:a,onSelectionChange:s,options:c,editorDidMount:g}){const d=t.useCallback(l=>{r?.(l)},[r]),f=t.useCallback(l=>{a?.(l)},[a]),i=t.useCallback(l=>{l.selectionSet&&s&&s?.(l.state.selection)},[s]);return n.default.createElement(n.default.Fragment,null,n.default.createElement(oe.default,{value:e,onChange:d,onCreateEditor:f,onUpdate:i,extensions:ce}))}function T(...e){return e.filter(Boolean).join(" ")}function U({view:e,pattern:r,active:a,getTime:s}){const c=t.useRef([]),g=t.useRef();t.useEffect(()=>{if(e)if(r&&a){let f=function(){try{const i=s(),m=[Math.max(g.current||i,i-1/10,0),i+1/60];g.current=m[1],c.current=c.current.filter(h=>h.whole.end>i);const v=r.queryArc(...m).filter(h=>h.hasOnset());c.current=c.current.concat(v),e.dispatch({effects:x.of(c.current)})}catch{e.dispatch({effects:x.of([])})}d=requestAnimationFrame(f)},d=requestAnimationFrame(f);return()=>{cancelAnimationFrame(d)}}else c.current=[],e.dispatch({effects:x.of([])})},[r,a,e])}const ie="_container_3i85k_1",le="_header_3i85k_5",ue="_buttons_3i85k_9",de="_button_3i85k_9",fe="_buttonDisabled_3i85k_17",ge="_error_3i85k_21",me="_body_3i85k_25",E={container:ie,header:le,buttons:ue,button:de,buttonDisabled:fe,error:ge,body:me};function j({type:e}){return n.default.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",className:"sc-h-5 sc-w-5",viewBox:"0 0 20 20",fill:"currentColor"},{refresh:n.default.createElement("path",{fillRule:"evenodd",d:"M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z",clipRule:"evenodd"}),play:n.default.createElement("path",{fillRule:"evenodd",d:"M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z",clipRule:"evenodd"}),pause:n.default.createElement("path",{fillRule:"evenodd",d:"M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z",clipRule:"evenodd"})}[e])}function J(e){return t.useEffect(()=>(window.addEventListener("message",e),()=>window.removeEventListener("message",e)),[e]),t.useCallback(r=>window.postMessage(r,"*"),[])}function $({defaultOutput:e,interval:r,getTime:a,evalOnMount:s=!1,initialCode:c="",autolink:g=!1,beforeEval:d,afterEval:f,onEvalError:i,onToggle:l}){const m=t.useMemo(()=>he(),[]),[v,h]=t.useState(),[_,C]=t.useState(),[b,y]=t.useState(c),[M,P]=t.useState(),[k,D]=t.useState(),[S,N]=t.useState(!1),w=b!==M,{scheduler:R,evaluate:H,start:G,stop:z,pause:Q}=t.useMemo(()=>te.repl({interval:r,defaultOutput:e,onSchedulerError:h,onEvalError:u=>{C(u),i?.(u)},getTime:a,transpiler:re.transpiler,beforeEval:({code:u})=>{y(u),d?.()},afterEval:({pattern:u,code:F})=>{P(F),D(u),C(),h(),g&&(window.location.hash="#"+encodeURIComponent(btoa(F))),f?.()},onToggle:u=>{N(u),l?.(u)}}),[e,r,a]),W=J(({data:{from:u,type:F}})=>{F==="start"&&u!==m&&z()}),q=t.useCallback(async(u=!0)=>{await H(b,u),W({type:"start",from:m})},[H,b]),V=t.useRef();return t.useEffect(()=>{!V.current&&s&&b&&(V.current=!0,q())},[q,s,b]),t.useEffect(()=>()=>{R.stop()},[R]),{code:b,setCode:y,error:v||_,schedulerError:v,scheduler:R,evalError:_,evaluate:H,activateCode:q,activeCode:M,isDirty:w,pattern:k,started:S,start:G,stop:z,pause:Q,togglePlay:async()=>{S?R.pause():await q()}}}function he(){return Math.floor((1+Math.random())*65536).toString(16).substring(1)}const pe=()=>B.getAudioContext().currentTime;function be({tune:e,hideOutsideView:r=!1,init:a,enableKeyboard:s}){const{code:c,setCode:g,evaluate:d,activateCode:f,error:i,isDirty:l,activeCode:m,pattern:v,started:h,scheduler:_,togglePlay:C,stop:b}=$({initialCode:e,defaultOutput:B.webaudioOutput,getTime:pe}),[y,M]=t.useState(),[P,k]=ee.useInView({threshold:.01}),D=t.useRef(),S=t.useMemo(()=>((k||!r)&&(D.current=!0),k||D.current),[k,r]);return U({view:y,pattern:v,active:h&&!m?.includes("strudel disable-highlighting"),getTime:()=>_.getPhase()}),t.useLayoutEffect(()=>{if(s){const N=async w=>{(w.ctrlKey||w.altKey)&&(w.code==="Enter"?(w.preventDefault(),K(y),await f()):w.code==="Period"&&(b(),w.preventDefault()))};return window.addEventListener("keydown",N,!0),()=>window.removeEventListener("keydown",N,!0)}},[s,v,c,d,b,y]),n.default.createElement("div",{className:E.container,ref:P},n.default.createElement("div",{className:E.header},n.default.createElement("div",{className:E.buttons},n.default.createElement("button",{className:T(E.button,h?"sc-animate-pulse":""),onClick:()=>C()},n.default.createElement(j,{type:h?"pause":"play"})),n.default.createElement("button",{className:T(l?E.button:E.buttonDisabled),onClick:()=>f()},n.default.createElement(j,{type:"refresh"}))),i&&n.default.createElement("div",{className:E.error},i.message)),n.default.createElement("div",{className:E.body},S&&n.default.createElement(O,{value:c,onChange:g,onViewChanged:M})))}const ve=e=>t.useLayoutEffect(()=>(window.addEventListener("keydown",e,!0),()=>window.removeEventListener("keydown",e,!0)),[e]);exports.CodeMirror=O;exports.MiniRepl=be;exports.cx=T;exports.flash=K;exports.useHighlighting=U;exports.useKeydown=ve;exports.usePostMessage=J;exports.useStrudel=$; diff --git a/packages/react/dist/index.es.js b/packages/react/dist/index.es.js index bbd3b758..499895c3 100644 --- a/packages/react/dist/index.es.js +++ b/packages/react/dist/index.es.js @@ -1,15 +1,15 @@ -import n, { useCallback as N, useRef as x, useEffect as R, useState as w, useMemo as I, useLayoutEffect as K } from "react"; -import Q from "@uiw/react-codemirror"; -import { Decoration as E, EditorView as O } from "@codemirror/view"; -import { StateEffect as j, StateField as U } from "@codemirror/state"; -import { javascript as W } from "@codemirror/lang-javascript"; +import n, { useCallback as _, useRef as H, useEffect as L, useMemo as V, useState as w, useLayoutEffect as j } from "react"; +import X from "@uiw/react-codemirror"; +import { Decoration as E, EditorView as U } from "@codemirror/view"; +import { StateEffect as $, StateField as G } from "@codemirror/state"; +import { javascript as Y } from "@codemirror/lang-javascript"; import { tags as r } from "@lezer/highlight"; -import { createTheme as X } from "@uiw/codemirror-themes"; -import { useInView as Y } from "react-hook-inview"; -import { webaudioOutput as Z, getAudioContext as ee } from "@strudel.cycles/webaudio"; -import { repl as te } from "@strudel.cycles/core"; -import { transpiler as re } from "@strudel.cycles/transpiler"; -const oe = X({ +import { createTheme as Z } from "@uiw/codemirror-themes"; +import { useInView as ee } from "react-hook-inview"; +import { webaudioOutput as te, getAudioContext as re } from "@strudel.cycles/webaudio"; +import { repl as oe } from "@strudel.cycles/core"; +import { transpiler as ne } from "@strudel.cycles/transpiler"; +const ae = Z({ theme: "dark", settings: { background: "#222", @@ -42,14 +42,14 @@ const oe = X({ { tag: r.invalid, color: "#ffffff" } ] }); -const T = j.define(), ne = U.define({ +const B = $.define(), se = G.define({ create() { return E.none; }, update(e, t) { try { for (let o of t.effects) - if (o.is(T)) + if (o.is(B)) if (o.value) { const a = E.mark({ attributes: { style: "background-color: #FFCA2880" } }); e = E.set([a.range(0, t.newDoc.length)]); @@ -60,25 +60,25 @@ const T = j.define(), ne = U.define({ return console.warn("flash error", o), e; } }, - provide: (e) => O.decorations.from(e) -}), ae = (e) => { - e.dispatch({ effects: T.of(!0) }), setTimeout(() => { - e.dispatch({ effects: T.of(!1) }); + provide: (e) => U.decorations.from(e) +}), ce = (e) => { + e.dispatch({ effects: B.of(!0) }), setTimeout(() => { + e.dispatch({ effects: B.of(!1) }); }, 200); -}, A = j.define(), se = U.define({ +}, z = $.define(), ie = G.define({ create() { return E.none; }, update(e, t) { try { for (let o of t.effects) - if (o.is(A)) { + if (o.is(z)) { const a = o.value.map( - (s) => (s.context.locations || []).map(({ start: m, end: l }) => { - const d = s.context.color || "#FFCA28"; - let c = t.newDoc.line(m.line).from + m.column, i = t.newDoc.line(l.line).from + l.column; - const g = t.newDoc.length; - return c > g || i > g ? void 0 : E.mark({ attributes: { style: `outline: 1.5px solid ${d};` } }).range(c, i); + (s) => (s.context.locations || []).map(({ start: f, end: d }) => { + const u = s.context.color || "#FFCA28"; + let c = t.newDoc.line(f.line).from + f.column, i = t.newDoc.line(d.line).from + d.column; + const m = t.newDoc.length; + return c > m || i > m ? void 0 : E.mark({ attributes: { style: `outline: 1.5px solid ${u};` } }).range(c, i); }) ).flat().filter(Boolean) || []; e = E.set(a, !0); @@ -88,69 +88,69 @@ const T = j.define(), ne = U.define({ return E.set([]); } }, - provide: (e) => O.decorations.from(e) -}), ce = [W(), oe, se, ne]; -function ie({ value: e, onChange: t, onViewChanged: o, onSelectionChange: a, options: s, editorDidMount: m }) { - const l = N( + provide: (e) => U.decorations.from(e) +}), le = [Y(), ae, ie, se]; +function de({ value: e, onChange: t, onViewChanged: o, onSelectionChange: a, options: s, editorDidMount: f }) { + const d = _( (i) => { t?.(i); }, [t] - ), d = N( + ), u = _( (i) => { o?.(i); }, [o] - ), c = N( + ), c = _( (i) => { i.selectionSet && a && a?.(i.state.selection); }, [a] ); - return /* @__PURE__ */ n.createElement(n.Fragment, null, /* @__PURE__ */ n.createElement(Q, { + return /* @__PURE__ */ n.createElement(n.Fragment, null, /* @__PURE__ */ n.createElement(X, { value: e, - onChange: l, - onCreateEditor: d, + onChange: d, + onCreateEditor: u, onUpdate: c, - extensions: ce + extensions: le })); } -function B(...e) { +function K(...e) { return e.filter(Boolean).join(" "); } -function le({ view: e, pattern: t, active: o, getTime: a }) { - const s = x([]), m = x(); - R(() => { +function ue({ view: e, pattern: t, active: o, getTime: a }) { + const s = H([]), f = H(); + L(() => { if (e) if (t && o) { - let d = function() { + let u = function() { try { - const c = a(), g = [Math.max(m.current || c, c - 1 / 10, 0), c + 1 / 60]; - m.current = g[1], s.current = s.current.filter((p) => p.whole.end > c); - const h = t.queryArc(...g).filter((p) => p.hasOnset()); - s.current = s.current.concat(h), e.dispatch({ effects: A.of(s.current) }); + const c = a(), m = [Math.max(f.current || c, c - 1 / 10, 0), c + 1 / 60]; + f.current = m[1], s.current = s.current.filter((g) => g.whole.end > c); + const h = t.queryArc(...m).filter((g) => g.hasOnset()); + s.current = s.current.concat(h), e.dispatch({ effects: z.of(s.current) }); } catch { - e.dispatch({ effects: A.of([]) }); + e.dispatch({ effects: z.of([]) }); } - l = requestAnimationFrame(d); - }, l = requestAnimationFrame(d); + d = requestAnimationFrame(u); + }, d = requestAnimationFrame(u); return () => { - cancelAnimationFrame(l); + cancelAnimationFrame(d); }; } else - s.current = [], e.dispatch({ effects: A.of([]) }); + s.current = [], e.dispatch({ effects: z.of([]) }); }, [t, o, e]); } -const de = "_container_3i85k_1", ue = "_header_3i85k_5", fe = "_buttons_3i85k_9", me = "_button_3i85k_9", ge = "_buttonDisabled_3i85k_17", pe = "_error_3i85k_21", he = "_body_3i85k_25", b = { - container: de, - header: ue, - buttons: fe, - button: me, - buttonDisabled: ge, - error: pe, - body: he +const fe = "_container_3i85k_1", me = "_header_3i85k_5", ge = "_buttons_3i85k_9", pe = "_button_3i85k_9", he = "_buttonDisabled_3i85k_17", be = "_error_3i85k_21", ve = "_body_3i85k_25", v = { + container: fe, + header: me, + buttons: ge, + button: pe, + buttonDisabled: he, + error: be, + body: ve }; -function q({ type: e }) { +function O({ type: e }) { return /* @__PURE__ */ n.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", className: "sc-h-5 sc-w-5", @@ -174,137 +174,147 @@ function q({ type: e }) { }) }[e]); } -function ve({ +function Ee(e) { + return L(() => (window.addEventListener("message", e), () => window.removeEventListener("message", e)), [e]), _((t) => window.postMessage(t, "*"), []); +} +function we({ defaultOutput: e, interval: t, getTime: o, evalOnMount: a = !1, initialCode: s = "", - autolink: m = !1, - beforeEval: l, - afterEval: d, + autolink: f = !1, + beforeEval: d, + afterEval: u, onEvalError: c, onToggle: i }) { - const [g, h] = w(), [p, D] = w(), [v, k] = w(s), [y, P] = w(), [z, _] = w(), [C, H] = w(!1), F = v !== y, { scheduler: u, evaluate: L, start: $, stop: G, pause: J } = I( - () => te({ + const m = V(() => ye(), []), [h, g] = w(), [C, N] = w(), [p, y] = w(s), [M, S] = w(), [k, D] = w(), [F, x] = w(!1), b = p !== M, { scheduler: A, evaluate: T, start: J, stop: q, pause: Q } = V( + () => oe({ interval: t, defaultOutput: e, - onSchedulerError: h, - onEvalError: (f) => { - D(f), c?.(f); + onSchedulerError: g, + onEvalError: (l) => { + N(l), c?.(l); }, getTime: o, - transpiler: re, - beforeEval: ({ code: f }) => { - k(f), l?.(); + transpiler: ne, + beforeEval: ({ code: l }) => { + y(l), d?.(); }, - afterEval: ({ pattern: f, code: S }) => { - P(S), _(f), D(), h(), m && (window.location.hash = "#" + encodeURIComponent(btoa(S))), d?.(); + afterEval: ({ pattern: l, code: P }) => { + S(P), D(l), N(), g(), f && (window.location.hash = "#" + encodeURIComponent(btoa(P))), u?.(); }, - onToggle: (f) => { - H(f), i?.(f); + onToggle: (l) => { + x(l), i?.(l); } }), [e, t, o] - ), M = N(async (f = !0) => L(v, f), [L, v]), V = x(); - return R(() => { - !V.current && a && v && (V.current = !0, M()); - }, [M, a, v]), R(() => () => { - u.stop(); - }, [u]), { - code: v, - setCode: k, - error: g || p, - schedulerError: g, - scheduler: u, - evalError: p, - evaluate: L, - activateCode: M, - activeCode: y, - isDirty: F, - pattern: z, - started: C, - start: $, - stop: G, - pause: J, + ), W = Ee(({ data: { from: l, type: P } }) => { + P === "start" && l !== m && q(); + }), R = _( + async (l = !0) => { + await T(p, l), W({ type: "start", from: m }); + }, + [T, p] + ), I = H(); + return L(() => { + !I.current && a && p && (I.current = !0, R()); + }, [R, a, p]), L(() => () => { + A.stop(); + }, [A]), { + code: p, + setCode: y, + error: h || C, + schedulerError: h, + scheduler: A, + evalError: C, + evaluate: T, + activateCode: R, + activeCode: M, + isDirty: b, + pattern: k, + started: F, + start: J, + stop: q, + pause: Q, togglePlay: async () => { - C ? u.pause() : await M(); + F ? A.pause() : await R(); } }; } -const be = () => ee().currentTime; -function Pe({ tune: e, hideOutsideView: t = !1, init: o, enableKeyboard: a }) { +function ye() { + return Math.floor((1 + Math.random()) * 65536).toString(16).substring(1); +} +const ke = () => re().currentTime; +function Se({ tune: e, hideOutsideView: t = !1, init: o, enableKeyboard: a }) { const { code: s, - setCode: m, - evaluate: l, - activateCode: d, + setCode: f, + evaluate: d, + activateCode: u, error: c, isDirty: i, - activeCode: g, + activeCode: m, pattern: h, - started: p, - scheduler: D, - togglePlay: v, - stop: k - } = ve({ + started: g, + scheduler: C, + togglePlay: N, + stop: p + } = we({ initialCode: e, - defaultOutput: Z, - getTime: be - }), [y, P] = w(), [z, _] = Y({ + defaultOutput: te, + getTime: ke + }), [y, M] = w(), [S, k] = ee({ threshold: 0.01 - }), C = x(), H = I(() => ((_ || !t) && (C.current = !0), _ || C.current), [_, t]); - return le({ + }), D = H(), F = V(() => ((k || !t) && (D.current = !0), k || D.current), [k, t]); + return ue({ view: y, pattern: h, - active: p && !g?.includes("strudel disable-highlighting"), - getTime: () => D.getPhase() - }), K(() => { + active: g && !m?.includes("strudel disable-highlighting"), + getTime: () => C.getPhase() + }), j(() => { if (a) { - const F = async (u) => { - (u.ctrlKey || u.altKey) && (u.code === "Enter" ? (u.preventDefault(), ae(y), await d()) : u.code === "Period" && (k(), u.preventDefault())); + const x = async (b) => { + (b.ctrlKey || b.altKey) && (b.code === "Enter" ? (b.preventDefault(), ce(y), await u()) : b.code === "Period" && (p(), b.preventDefault())); }; - return window.addEventListener("keydown", F, !0), () => window.removeEventListener("keydown", F, !0); + return window.addEventListener("keydown", x, !0), () => window.removeEventListener("keydown", x, !0); } - }, [a, h, s, l, k, y]), /* @__PURE__ */ n.createElement("div", { - className: b.container, - ref: z + }, [a, h, s, d, p, y]), /* @__PURE__ */ n.createElement("div", { + className: v.container, + ref: S }, /* @__PURE__ */ n.createElement("div", { - className: b.header + className: v.header }, /* @__PURE__ */ n.createElement("div", { - className: b.buttons + className: v.buttons }, /* @__PURE__ */ n.createElement("button", { - className: B(b.button, p ? "sc-animate-pulse" : ""), - onClick: () => v() - }, /* @__PURE__ */ n.createElement(q, { - type: p ? "pause" : "play" + className: K(v.button, g ? "sc-animate-pulse" : ""), + onClick: () => N() + }, /* @__PURE__ */ n.createElement(O, { + type: g ? "pause" : "play" })), /* @__PURE__ */ n.createElement("button", { - className: B(i ? b.button : b.buttonDisabled), - onClick: () => d() - }, /* @__PURE__ */ n.createElement(q, { + className: K(i ? v.button : v.buttonDisabled), + onClick: () => u() + }, /* @__PURE__ */ n.createElement(O, { type: "refresh" }))), c && /* @__PURE__ */ n.createElement("div", { - className: b.error + className: v.error }, c.message)), /* @__PURE__ */ n.createElement("div", { - className: b.body - }, H && /* @__PURE__ */ n.createElement(ie, { + className: v.body + }, F && /* @__PURE__ */ n.createElement(de, { value: s, - onChange: m, - onViewChanged: P + onChange: f, + onViewChanged: M }))); } -function ze(e) { - return R(() => (window.addEventListener("message", e), () => window.removeEventListener("message", e)), [e]), N((t) => window.postMessage(t, "*"), []); -} -const He = (e) => K(() => (window.addEventListener("keydown", e, !0), () => window.removeEventListener("keydown", e, !0)), [e]); +const Te = (e) => j(() => (window.addEventListener("keydown", e, !0), () => window.removeEventListener("keydown", e, !0)), [e]); export { - ie as CodeMirror, - Pe as MiniRepl, - B as cx, - ae as flash, - le as useHighlighting, - He as useKeydown, - ze as usePostMessage, - ve as useStrudel + de as CodeMirror, + Se as MiniRepl, + K as cx, + ce as flash, + ue as useHighlighting, + Te as useKeydown, + Ee as usePostMessage, + we as useStrudel }; diff --git a/packages/react/src/hooks/useStrudel.mjs b/packages/react/src/hooks/useStrudel.mjs index 14fabfb4..82326415 100644 --- a/packages/react/src/hooks/useStrudel.mjs +++ b/packages/react/src/hooks/useStrudel.mjs @@ -1,6 +1,7 @@ import { useRef, useCallback, useEffect, useMemo, useState } from 'react'; import { repl } from '@strudel.cycles/core'; import { transpiler } from '@strudel.cycles/transpiler'; +import usePostMessage from './usePostMessage.mjs'; function useStrudel({ defaultOutput, @@ -14,6 +15,7 @@ function useStrudel({ onEvalError, onToggle, }) { + const id = useMemo(() => s4(), []); // scheduler const [schedulerError, setSchedulerError] = useState(); const [evalError, setEvalError] = useState(); @@ -57,7 +59,19 @@ function useStrudel({ }), [defaultOutput, interval, getTime], ); - const activateCode = useCallback(async (autostart = true) => evaluate(code, autostart), [evaluate, code]); + const broadcast = usePostMessage(({ data: { from, type } }) => { + if (type === 'start' && from !== id) { + // console.log('message', from, type); + stop(); + } + }); + const activateCode = useCallback( + async (autostart = true) => { + await evaluate(code, autostart); + broadcast({ type: 'start', from: id }); + }, + [evaluate, code], + ); const inited = useRef(); useEffect(() => { @@ -103,3 +117,9 @@ function useStrudel({ } export default useStrudel; + +function s4() { + return Math.floor((1 + Math.random()) * 0x10000) + .toString(16) + .substring(1); +} diff --git a/repl/package.json b/repl/package.json index 88a5b1c0..97ac8dbd 100644 --- a/repl/package.json +++ b/repl/package.json @@ -11,7 +11,6 @@ "build": "vite build", "preview": "vite preview", "test": "vitest run --reporter verbose -v --no-isolate", - "snapshot": "vitest run -u --silent", "add-license": "cat etc/agpl-header.txt ../docs/static/js/*LICENSE.txt > /tmp/strudel-license.txt && cp /tmp/strudel-license.txt ../docs/static/js/*LICENSE.txt", "predeploy": "npm run build", "deploy": "gh-pages -d ../docs", diff --git a/tutorial/MiniRepl.jsx b/tutorial/MiniRepl.jsx index ef63d563..c86a83fd 100644 --- a/tutorial/MiniRepl.jsx +++ b/tutorial/MiniRepl.jsx @@ -19,7 +19,7 @@ evalScope( import('@strudel.cycles/osc'), ); -prebake(); +// prebake(); export function MiniRepl({ tune }) { return <_MiniRepl tune={tune} hideOutsideView={true} />; diff --git a/tutorial/Tutorial.jsx b/tutorial/Tutorial.jsx index 66117b46..1ec51187 100644 --- a/tutorial/Tutorial.jsx +++ b/tutorial/Tutorial.jsx @@ -10,6 +10,9 @@ import Tutorial from './tutorial.rendered.mdx'; // import ApiDoc from './ApiDoc'; import './style.scss'; import '@strudel.cycles/react/dist/style.css'; +import { initAudioOnFirstClick } from '@strudel.cycles/webaudio'; + +initAudioOnFirstClick(); ReactDOM.render( diff --git a/tutorial/test/__snapshots__/examples.test.mjs.snap b/tutorial/test/__snapshots__/examples.test.mjs.snap index 305f7279..e97d20b7 100644 --- a/tutorial/test/__snapshots__/examples.test.mjs.snap +++ b/tutorial/test/__snapshots__/examples.test.mjs.snap @@ -767,10 +767,10 @@ exports[`runs examples > example "dry" example index 0 1`] = ` exports[`runs examples > example "each" example index 0 1`] = ` [ - "3/4 -> 1/1: {\\"note\\":\\"c3\\"}", - "1/2 -> 3/4: {\\"note\\":\\"d3\\"}", - "1/4 -> 1/2: {\\"note\\":\\"e3\\"}", - "0/1 -> 1/4: {\\"note\\":\\"g3\\"}", + "0/1 -> 1/4: {\\"note\\":\\"c3\\"}", + "1/4 -> 1/2: {\\"note\\":\\"d3\\"}", + "1/2 -> 3/4: {\\"note\\":\\"e3\\"}", + "3/4 -> 1/1: {\\"note\\":\\"g3\\"}", "1/1 -> 5/4: {\\"note\\":\\"c3\\"}", "5/4 -> 3/2: {\\"note\\":\\"d3\\"}", "3/2 -> 7/4: {\\"note\\":\\"e3\\"}", @@ -779,10 +779,10 @@ exports[`runs examples > example "each" example index 0 1`] = ` "9/4 -> 5/2: {\\"note\\":\\"d3\\"}", "5/2 -> 11/4: {\\"note\\":\\"e3\\"}", "11/4 -> 3/1: {\\"note\\":\\"g3\\"}", - "3/1 -> 13/4: {\\"note\\":\\"c3\\"}", - "13/4 -> 7/2: {\\"note\\":\\"d3\\"}", - "7/2 -> 15/4: {\\"note\\":\\"e3\\"}", - "15/4 -> 4/1: {\\"note\\":\\"g3\\"}", + "15/4 -> 4/1: {\\"note\\":\\"c3\\"}", + "7/2 -> 15/4: {\\"note\\":\\"d3\\"}", + "13/4 -> 7/2: {\\"note\\":\\"e3\\"}", + "3/1 -> 13/4: {\\"note\\":\\"g3\\"}", ] `; diff --git a/tutorial/tutorial.mdx b/tutorial/tutorial.mdx index de12742e..76bc31e1 100644 --- a/tutorial/tutorial.mdx +++ b/tutorial/tutorial.mdx @@ -253,7 +253,7 @@ Using round brackets, we can create rhythmical sub-divisions based on three para The first parameter controls how may beats will be played. The second parameter controls the total amount of segments the beats will be distributed over. The third (optional) parameter controls the starting position for distributing the beats. -One popular Euclidian rhythm (going by various names, such as "Pop Clave") is "(3,8,1)" or simply "(3,8)", +One popular Euclidian rhythm (going by various names, such as "Pop Clave") is "(3,8,0)" or simply "(3,8)", resulting in a rhythmical structure of "x ~ ~ x ~ ~ x ~" (3 beats over 8 segments, starting on position 1). @@ -358,7 +358,7 @@ For pitched sounds, you can use `note`, just like with synths: @2").s('gtr').gain(.5)`} /> @@ -368,7 +368,7 @@ If we want them to behave more like a synth, we can add `clip(1)`: @2").s('gtr').clip(1) .gain(.5)`} @@ -380,8 +380,8 @@ If we have 2 samples with different base pitches, we can make them in tune by sp @2").s("gtr,moog").clip(1) .gain(.5)`} @@ -393,7 +393,7 @@ We can also declare different samples for different regions of the keyboard: ".layer( - x => x.voicings(['d3','g4']).struct("~ x"), - x => x.rootNotes(2).tone(synth(osc('sawtooth4')).chain(out())) -).note()`} + x => x.voicings(['d3','g4']).struct("~ x").note(), + x => x.rootNotes(2).note().s('sawtooth').cutoff(800) +)`} /> @@ -786,17 +786,15 @@ Strudel also supports midi via [webmidi](https://npmjs.com/package/webmidi). ### midi(outputName?) -Make sure to have a midi device connected or to use an IAC Driver. +Either connect a midi device or use the IAC Driver (Mac) or Midi Through Port (Linux) for internal midi messages. If no outputName is given, it uses the first midi output it finds. -Midi is currently not supported by the mini repl used here, but you can [open the midi example in the repl](https://strudel.tidalcycles.org/#c3RhY2soIjxDXjcgQTcgRG03IEc3PiIubS52b2ljaW5ncygpLCAnPEMzIEEyIEQzIEcyPicubSkKICAubWlkaSgp). - -In the REPL, you will se a log of the available MIDI devices. - - +/> + +In the console, you will see a log of the available MIDI devices as soon as you run the code, e.g. `Midi connected! Using "Midi Through Port-0".` # Superdirt API