diff --git a/.gitignore b/.gitignore index 8081af77..43684564 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,5 @@ repl_old tutorial.rendered.mdx doc.json talk/public/EmuSP12 -talk/public/samples \ No newline at end of file +talk/public/samples +server/samples/old \ No newline at end of file diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 737df84f..84441a1b 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -1046,6 +1046,22 @@ export class Pattern { onTrigger(onTrigger) { return this._withHap((hap) => hap.setContext({ ...hap.context, onTrigger })); } + log(func = id) { + return this._withHap((hap) => + hap.setContext({ + ...hap.context, + onTrigger: (...args) => { + if (hap.context.onTrigger) { + hap.context.onTrigger(...args); + } + console.log(func(...args)); + }, + }), + ); + } + logValues(func = id) { + return this.log((_, hap) => func(hap.value)); + } } // TODO - adopt value.mjs fully.. diff --git a/packages/core/util.mjs b/packages/core/util.mjs index 0a4866d0..6a96be87 100644 --- a/packages/core/util.mjs +++ b/packages/core/util.mjs @@ -31,6 +31,19 @@ export const fromMidi = (n) => { return Math.pow(2, (n - 69) / 12) * 440; }; +export const getFreq = (noteOrMidi) => { + if (typeof noteOrMidi === 'number') { + return fromMidi(noteOrMidi); + } + return fromMidi(toMidi(noteOrMidi)); +}; + +export const midi2note = (n) => { + const oct = Math.floor(n / 12) - 1; + const pc = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'][n % 12]; + return pc + oct; +}; + // modulo that works with negative numbers e.g. mod(-1, 3) = 2 // const mod = (n: number, m: number): number => (n < 0 ? mod(n + m, m) : n % m); export const mod = (n, m) => ((n % m) + m) % m; diff --git a/packages/react/dist/index.cjs.js b/packages/react/dist/index.cjs.js index 55eeacd9..e73ce204 100644 --- a/packages/react/dist/index.cjs.js +++ b/packages/react/dist/index.cjs.js @@ -1,3 +1,3 @@ -"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});var a=require("react"),oe=require("react-codemirror6"),y=require("@codemirror/view"),F=require("@codemirror/state"),ae=require("@codemirror/lang-javascript"),c=require("@codemirror/highlight"),re=require("react-hook-inview"),ne=require("@strudel.cycles/eval"),se=require("@strudel.cycles/core/util.mjs"),p=require("@strudel.cycles/tone"),j=require("@strudel.cycles/core"),v=require("@strudel.cycles/midi");function ce(e){return e&&typeof e=="object"&&"default"in e?e:{default:e}}var f=ce(a);const le="#abb2bf",ie="#7d8799",ue="#ffffff",de="#21252b",W="rgba(0, 0, 0, 0.5)",fe="transparent",P="#353a42",ge="rgba(128, 203, 196, 0.5)",O="#ffcc00",me=y.EditorView.theme({"&":{color:"#ffffff",backgroundColor:fe,fontSize:"15px","z-index":11},".cm-content":{caretColor:O,lineHeight:"22px"},".cm-line":{background:"#2C323699"},"&.cm-focused .cm-cursor":{backgroundColor:O,width:"3px"},"&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection":{backgroundColor:ge},".cm-panels":{backgroundColor:de,color:"#ffffff"},".cm-panels.cm-panels-top":{borderBottom:"2px solid black"},".cm-panels.cm-panels-bottom":{borderTop:"2px solid black"},".cm-searchMatch":{backgroundColor:"#72a1ff59",outline:"1px solid #457dff"},".cm-searchMatch.cm-searchMatch-selected":{backgroundColor:"#6199ff2f"},".cm-activeLine":{backgroundColor:W},".cm-selectionMatch":{backgroundColor:"#aafe661a"},"&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket":{backgroundColor:"#bad0f847",outline:"1px solid #515a6b"},".cm-gutters":{background:"transparent",color:"#676e95",border:"none"},".cm-activeLineGutter":{backgroundColor:W},".cm-foldPlaceholder":{backgroundColor:"transparent",border:"none",color:"#ddd"},".cm-tooltip":{border:"none",backgroundColor:P},".cm-tooltip .cm-tooltip-arrow:before":{borderTopColor:"transparent",borderBottomColor:"transparent"},".cm-tooltip .cm-tooltip-arrow:after":{borderTopColor:P,borderBottomColor:P},".cm-tooltip-autocomplete":{"& > ul > li[aria-selected]":{backgroundColor:W,color:le}}},{dark:!0}),be=c.HighlightStyle.define([{tag:c.tags.keyword,color:"#c792ea"},{tag:c.tags.operator,color:"#89ddff"},{tag:c.tags.special(c.tags.variableName),color:"#eeffff"},{tag:c.tags.typeName,color:"#f07178"},{tag:c.tags.atom,color:"#f78c6c"},{tag:c.tags.number,color:"#ff5370"},{tag:c.tags.definition(c.tags.variableName),color:"#82aaff"},{tag:c.tags.string,color:"#c3e88d"},{tag:c.tags.special(c.tags.string),color:"#f07178"},{tag:c.tags.comment,color:ie},{tag:c.tags.variableName,color:"#f07178"},{tag:c.tags.tagName,color:"#ff5370"},{tag:c.tags.bracket,color:"#a2a1a4"},{tag:c.tags.meta,color:"#ffcb6b"},{tag:c.tags.attributeName,color:"#c792ea"},{tag:c.tags.propertyName,color:"#c792ea"},{tag:c.tags.className,color:"#decb6b"},{tag:c.tags.invalid,color:ue}]),pe=[me,be],V=F.StateEffect.define(),he=F.StateField.define({create(){return y.Decoration.none},update(e,o){try{for(let r of o.effects)if(r.is(V))if(r.value){const i=y.Decoration.mark({attributes:{style:"background-color: #FFCA2880"}});e=y.Decoration.set([i.range(0,o.newDoc.length)])}else e=y.Decoration.set([]);return e}catch(r){return console.warn("flash error",r),e}},provide:e=>y.EditorView.decorations.from(e)}),ve=e=>{e.dispatch({effects:V.of(!0)}),setTimeout(()=>{e.dispatch({effects:V.of(!1)})},200)},B=F.StateEffect.define(),ye=F.StateField.define({create(){return y.Decoration.none},update(e,o){try{for(let r of o.effects)r.is(B)&&(e=y.Decoration.set(r.value.flatMap(i=>(i.context.locations||[]).map(({start:g,end:l})=>{const u=i.context.color||"#FFCA28";let m=o.newDoc.line(g.line).from+g.column,s=o.newDoc.line(l.line).from+l.column;const n=o.newDoc.length;return m>n||s>n?void 0:y.Decoration.mark({attributes:{style:`outline: 1px solid ${u}`}}).range(m,s)})).filter(Boolean),!0));return e}catch{return e}},provide:e=>y.EditorView.decorations.from(e)});function Q({value:e,onChange:o,onViewChanged:r,onCursor:i,options:g,editorDidMount:l}){return f.default.createElement(f.default.Fragment,null,f.default.createElement(oe.CodeMirror,{onViewChange:r,style:{display:"flex",flexDirection:"column",flex:"1 0 auto"},value:e,onChange:o,extensions:[ae.javascript(),pe,ye,he]}))}function U(e){const{onEvent:o,onQuery:r,onSchedule:i,ready:g=!0,onDraw:l}=e,[u,m]=a.useState(!1),s=1,n=()=>Math.floor(p.Tone.getTransport().seconds/s),C=(b=n())=>{const k=new j.TimeSpan(b,b+1),D=r?.(new j.State(k))||[];i?.(D,b);const H=k.begin.valueOf();p.Tone.getTransport().cancel(H);const N=(b+1)*s-.5,R=Math.max(p.Tone.getTransport().seconds,N)+.1;p.Tone.getTransport().schedule(()=>{C(b+1)},R),D?.filter(h=>h.part.begin.equals(h.whole?.begin)).forEach(h=>{p.Tone.getTransport().schedule(M=>{o(M,h,p.Tone.getContext().currentTime),p.Tone.Draw.schedule(()=>{l?.(M,h)},M)},h.part.begin.valueOf())})};a.useEffect(()=>{g&&C()},[o,i,r,l,g]);const S=async()=>{m(!0),await p.Tone.start(),p.Tone.getTransport().start("+0.1")},w=()=>{p.Tone.getTransport().pause(),m(!1)};return{start:S,stop:w,onEvent:o,started:u,setStarted:m,toggle:()=>u?w():S(),query:C,activeCycle:n}}function G(e){return a.useEffect(()=>(window.addEventListener("message",e),()=>window.removeEventListener("message",e)),[e]),a.useCallback(o=>window.postMessage(o,"*"),[])}let Ce=()=>Math.floor((1+Math.random())*65536).toString(16).substring(1);const we=e=>encodeURIComponent(btoa(e));function J({tune:e,defaultSynth:o,autolink:r=!0,onEvent:i,onDraw:g}){const l=a.useMemo(()=>Ce(),[]),[u,m]=a.useState(e),[s,n]=a.useState(),[C,S]=a.useState(""),[w,T]=a.useState(),[b,k]=a.useState(!1),[D,H]=a.useState(""),[N,R]=a.useState(),h=a.useMemo(()=>u!==s||w,[u,s,w]),M=a.useCallback(d=>S(t=>t+`${t?` +"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});var t=require("react"),ne=require("react-codemirror6"),k=require("@codemirror/view"),W=require("@codemirror/state"),se=require("@codemirror/lang-javascript"),c=require("@codemirror/highlight"),ce=require("react-hook-inview"),le=require("@strudel.cycles/eval"),ie=require("@strudel.cycles/core/util.mjs"),y=require("@strudel.cycles/tone"),I=require("@strudel.cycles/core"),C=require("@strudel.cycles/midi");function ue(e){return e&&typeof e=="object"&&"default"in e?e:{default:e}}var g=ue(t);const de="#abb2bf",fe="#7d8799",ge="#ffffff",me="#21252b",$="rgba(0, 0, 0, 0.5)",pe="transparent",V="#353a42",be="rgba(128, 203, 196, 0.5)",K="#ffcc00",he=k.EditorView.theme({"&":{color:"#ffffff",backgroundColor:pe,fontSize:"15px","z-index":11},".cm-content":{caretColor:K,lineHeight:"22px"},".cm-line":{background:"transparent"},".cm-line > *":{background:"#00000090"},"&.cm-focused .cm-cursor":{backgroundColor:K,width:"3px"},"&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection":{backgroundColor:be},".cm-panels":{backgroundColor:me,color:"#ffffff"},".cm-panels.cm-panels-top":{borderBottom:"2px solid black"},".cm-panels.cm-panels-bottom":{borderTop:"2px solid black"},".cm-searchMatch":{backgroundColor:"#72a1ff59",outline:"1px solid #457dff"},".cm-searchMatch.cm-searchMatch-selected":{backgroundColor:"#6199ff2f"},".cm-selectionMatch":{backgroundColor:"#aafe661a"},"&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket":{backgroundColor:"#bad0f847",outline:"1px solid #515a6b"},".cm-gutters":{background:"transparent",color:"#676e95",border:"none"},".cm-activeLineGutter":{backgroundColor:$},".cm-foldPlaceholder":{backgroundColor:"transparent",border:"none",color:"#ddd"},".cm-tooltip":{border:"none",backgroundColor:V},".cm-tooltip .cm-tooltip-arrow:before":{borderTopColor:"transparent",borderBottomColor:"transparent"},".cm-tooltip .cm-tooltip-arrow:after":{borderTopColor:V,borderBottomColor:V},".cm-tooltip-autocomplete":{"& > ul > li[aria-selected]":{backgroundColor:$,color:de}}},{dark:!0}),ve=c.HighlightStyle.define([{tag:c.tags.keyword,color:"#c792ea"},{tag:c.tags.operator,color:"#89ddff"},{tag:c.tags.special(c.tags.variableName),color:"#eeffff"},{tag:c.tags.typeName,color:"#f07178"},{tag:c.tags.atom,color:"#f78c6c"},{tag:c.tags.number,color:"#ff5370"},{tag:c.tags.definition(c.tags.variableName),color:"#82aaff"},{tag:c.tags.string,color:"#c3e88d"},{tag:c.tags.special(c.tags.string),color:"#f07178"},{tag:c.tags.comment,color:fe},{tag:c.tags.variableName,color:"#f07178"},{tag:c.tags.tagName,color:"#ff5370"},{tag:c.tags.bracket,color:"#a2a1a4"},{tag:c.tags.meta,color:"#ffcb6b"},{tag:c.tags.attributeName,color:"#c792ea"},{tag:c.tags.propertyName,color:"#c792ea"},{tag:c.tags.className,color:"#decb6b"},{tag:c.tags.invalid,color:ge}]),ye=[he,ve],A=W.StateEffect.define(),we=W.StateField.define({create(){return k.Decoration.none},update(e,a){try{for(let n of a.effects)if(n.is(A))if(n.value){const u=k.Decoration.mark({attributes:{style:"background-color: #FFCA2880"}});e=k.Decoration.set([u.range(0,a.newDoc.length)])}else e=k.Decoration.set([]);return e}catch(n){return console.warn("flash error",n),e}},provide:e=>k.EditorView.decorations.from(e)}),G=e=>{e.dispatch({effects:A.of(!0)}),setTimeout(()=>{e.dispatch({effects:A.of(!1)})},200)},P=W.StateEffect.define(),Ce=W.StateField.define({create(){return k.Decoration.none},update(e,a){try{for(let n of a.effects)n.is(P)&&(e=k.Decoration.set(n.value.flatMap(u=>(u.context.locations||[]).map(({start:d,end:l})=>{const i=u.context.color||"#FFCA28";let m=a.newDoc.line(d.line).from+d.column,r=a.newDoc.line(l.line).from+l.column;const s=a.newDoc.length;return m>s||r>s?void 0:k.Decoration.mark({attributes:{style:`outline: 1.5px solid ${i};`}}).range(m,r)})).filter(Boolean),!0));return e}catch{return e}},provide:e=>k.EditorView.decorations.from(e)});function J({value:e,onChange:a,onViewChanged:n,onCursor:u,options:d,editorDidMount:l}){return g.default.createElement(g.default.Fragment,null,g.default.createElement(ne.CodeMirror,{onViewChange:n,style:{display:"flex",flexDirection:"column",flex:"1 0 auto"},value:e,onChange:a,extensions:[se.javascript(),ye,Ce,we]}))}function X(e){const{onEvent:a,onQuery:n,onSchedule:u,ready:d=!0,onDraw:l}=e,[i,m]=t.useState(!1),r=1,s=()=>Math.floor(y.Tone.getTransport().seconds/r),M=(b=s())=>{const S=new I.TimeSpan(b,b+1),N=n?.(new I.State(S))||[];u?.(N,b);const B=S.begin.valueOf();y.Tone.getTransport().cancel(B);const w=(b+1)*r-.5,q=Math.max(y.Tone.getTransport().seconds,w)+.1;y.Tone.getTransport().schedule(()=>{M(b+1)},q),N?.filter(h=>h.part.begin.equals(h.whole?.begin)).forEach(h=>{y.Tone.getTransport().schedule(v=>{a(v,h,y.Tone.getContext().currentTime),y.Tone.Draw.schedule(()=>{l?.(v,h)},v)},h.part.begin.valueOf())})};t.useEffect(()=>{d&&M()},[a,u,n,l,d]);const E=async()=>{m(!0),await y.Tone.start(),y.Tone.getTransport().start("+0.1")},x=()=>{y.Tone.getTransport().pause(),m(!1)};return{start:E,stop:x,onEvent:a,started:i,setStarted:m,toggle:()=>i?x():E(),query:M,activeCycle:s}}function Y(e){return t.useEffect(()=>(window.addEventListener("message",e),()=>window.removeEventListener("message",e)),[e]),t.useCallback(a=>window.postMessage(a,"*"),[])}let ke=()=>Math.floor((1+Math.random())*65536).toString(16).substring(1);const Me=e=>encodeURIComponent(btoa(e));function Z({tune:e,defaultSynth:a,autolink:n=!0,onEvent:u,onDraw:d}){const l=t.useMemo(()=>ke(),[]),[i,m]=t.useState(e),[r,s]=t.useState(),[M,E]=t.useState(""),[x,T]=t.useState(),[b,S]=t.useState(!1),[N,B]=t.useState(""),[w,q]=t.useState(),h=t.useMemo(()=>i!==r||x,[i,r,x]),v=t.useCallback(f=>E(o=>o+`${o?` -`:""}${d}`),[]),X=a.useMemo(()=>{if(s&&!s.includes("strudel disable-highlighting"))return(d,t)=>g?.(d,t,s)},[s,g]),x=U({onDraw:X,onEvent:a.useCallback((d,t,Z)=>{try{i?.(t),t.context.logs?.length&&t.context.logs.forEach(M);const{onTrigger:_,velocity:ee}=t.context;if(_)_(d,t,Z,1);else if(o){const te=se.getPlayableNoteValue(t);o.triggerAttackRelease(te,t.duration.valueOf(),d,ee)}else throw new Error("no defaultSynth passed to useRepl.")}catch(_){console.warn(_),_.message="unplayable event: "+_?.message,M(_.message)}},[i,M,o]),onQuery:a.useCallback(d=>{try{return N?.query(d)||[]}catch(t){return console.warn(t),t.message="query error: "+t.message,T(t),[]}},[N]),onSchedule:a.useCallback((d,t)=>Y(d,t),[]),ready:!!N&&!!s}),L=G(({data:{from:d,type:t}})=>{t==="start"&&d!==l&&(x.setStarted(!1),n(void 0))}),z=a.useCallback(async(d=u)=>{if(s&&!h){T(void 0),x.start();return}try{k(!0);const t=await ne.evaluate(d);x.start(),L({type:"start",from:l}),R(()=>t.pattern),r&&(window.location.hash="#"+encodeURIComponent(btoa(u))),H(we(u)),T(void 0),n(d),k(!1)}catch(t){t.message="evaluation error: "+t.message,console.warn(t),T(t)}},[s,h,u,x,r,l,L]),Y=(d,t)=>{d.length};return{pending:b,code:u,setCode:m,pattern:N,error:w,cycle:x,setPattern:R,dirty:h,log:C,togglePlay:()=>{x.started?x.stop():z()},setActiveCode:n,activateCode:z,activeCode:s,pushLog:M,hash:D}}function A(...e){return e.filter(Boolean).join(" ")}let q=[],I;function K({view:e,pattern:o,active:r}){a.useEffect(()=>{if(e)if(o&&r){let g=function(){try{const l=p.Tone.getTransport().seconds,m=[Math.max(I||l,l-1/10),l+1/60];I=l+1/60,q=q.filter(n=>n.whole.end>l);const s=o.queryArc(...m).filter(n=>n.hasOnset());q=q.concat(s),e.dispatch({effects:B.of(q)})}catch{e.dispatch({effects:B.of([])})}i=requestAnimationFrame(g)},i=requestAnimationFrame(g);return()=>{cancelAnimationFrame(i)}}else q=[],e.dispatch({effects:B.of([])})},[o,r,e])}const ke="_container_10e1g_1",Me="_header_10e1g_5",Ee="_buttons_10e1g_9",Te="_button_10e1g_9",xe="_buttonDisabled_10e1g_17",_e="_error_10e1g_21",Se="_body_10e1g_25";var E={container:ke,header:Me,buttons:Ee,button:Te,buttonDisabled:xe,error:_e,body:Se};function $({type:e}){return f.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:f.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:f.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:f.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 De({tune:e,defaultSynth:o,hideOutsideView:r=!1}){const{code:i,setCode:g,pattern:l,activateCode:u,error:m,cycle:s,dirty:n,togglePlay:C}=J({tune:e,defaultSynth:o,autolink:!1}),[S,w]=a.useState(),[T,b]=re.useInView({threshold:.01}),k=a.useRef(),D=a.useMemo(()=>((b||!r)&&(k.current=!0),b||k.current),[b,r]);return K({view:S,pattern:l,active:s.started}),f.default.createElement("div",{className:E.container,ref:T},f.default.createElement("div",{className:E.header},f.default.createElement("div",{className:E.buttons},f.default.createElement("button",{className:A(E.button,s.started?"sc-animate-pulse":""),onClick:()=>C()},f.default.createElement($,{type:s.started?"pause":"play"})),f.default.createElement("button",{className:A(n?E.button:E.buttonDisabled),onClick:()=>u()},f.default.createElement($,{type:"refresh"}))),m&&f.default.createElement("div",{className:E.error},m.message)),f.default.createElement("div",{className:E.body},D&&f.default.createElement(Q,{value:i,onChange:g,onViewChanged:w})))}function Ne(e){const{ready:o,connected:r,disconnected:i}=e,[g,l]=a.useState(!0),[u,m]=a.useState(v.WebMidi?.outputs||[]);return a.useEffect(()=>{v.enableWebMidi().then(()=>{v.WebMidi.addListener("connected",n=>{m([...v.WebMidi.outputs]),r?.(v.WebMidi,n)}),v.WebMidi.addListener("disconnected",n=>{m([...v.WebMidi.outputs]),i?.(v.WebMidi,n)}),o?.(v.WebMidi),l(!1)}).catch(n=>{if(n){console.error(n),console.warn("Web Midi could not be enabled..");return}})},[o,r,i,u]),{loading:g,outputs:u,outputByName:n=>v.WebMidi.getOutputByName(n)}}exports.CodeMirror=Q;exports.MiniRepl=De;exports.cx=A;exports.flash=ve;exports.useCycle=U;exports.useHighlighting=K;exports.usePostMessage=G;exports.useRepl=J;exports.useWebMidi=Ne; +`:""}${f}`),[]),F=t.useMemo(()=>{if(r&&!r.includes("strudel disable-highlighting"))return(f,o)=>d?.(f,o,r)},[r,d]),L=t.useMemo(()=>r&&r.includes("strudel hide-header"),[r]),H=t.useMemo(()=>r&&r.includes("strudel hide-console"),[r]),p=X({onDraw:F,onEvent:t.useCallback((f,o,oe)=>{try{u?.(o),o.context.logs?.length&&o.context.logs.forEach(v);const{onTrigger:D,velocity:ae}=o.context;if(D)D(f,o,oe,1);else if(a){const re=ie.getPlayableNoteValue(o);a.triggerAttackRelease(re,o.duration.valueOf(),f,ae)}else throw new Error("no defaultSynth passed to useRepl.")}catch(D){console.warn(D),D.message="unplayable event: "+D?.message,v(D.message)}},[u,v,a]),onQuery:t.useCallback(f=>{try{return w?.query(f)||[]}catch(o){return console.warn(o),o.message="query error: "+o.message,T(o),[]}},[w]),onSchedule:t.useCallback((f,o)=>te(f,o),[]),ready:!!w&&!!r}),j=Y(({data:{from:f,type:o}})=>{o==="start"&&f!==l&&(p.setStarted(!1),s(void 0))}),O=t.useCallback(async(f=i)=>{if(r&&!h){T(void 0),p.start();return}try{S(!0);const o=await le.evaluate(f);p.start(),j({type:"start",from:l}),q(()=>o.pattern),n&&(window.location.hash="#"+encodeURIComponent(btoa(i))),B(Me(i)),T(void 0),s(f),S(!1)}catch(o){o.message="evaluation error: "+o.message,console.warn(o),T(o)}},[r,h,i,p,n,l,j]),te=(f,o)=>{f.length};return{hideHeader:L,hideConsole:H,pending:b,code:i,setCode:m,pattern:w,error:x,cycle:p,setPattern:q,dirty:h,log:M,togglePlay:()=>{p.started?p.stop():O()},setActiveCode:s,activateCode:O,activeCode:r,pushLog:v,hash:N}}function z(...e){return e.filter(Boolean).join(" ")}let R=[],Q;function ee({view:e,pattern:a,active:n}){t.useEffect(()=>{if(e)if(a&&n){let d=function(){try{const l=y.Tone.getTransport().seconds,m=[Math.max(Q||l,l-1/10),l+1/60];Q=l+1/60,R=R.filter(s=>s.whole.end>l);const r=a.queryArc(...m).filter(s=>s.hasOnset());R=R.concat(r),e.dispatch({effects:P.of(R)})}catch{e.dispatch({effects:P.of([])})}u=requestAnimationFrame(d)},u=requestAnimationFrame(d);return()=>{cancelAnimationFrame(u)}}else R=[],e.dispatch({effects:P.of([])})},[a,n,e])}const Ee="_container_xpa19_1",xe="_header_xpa19_5",Te="_buttons_xpa19_9",_e="_button_xpa19_9",Se="_buttonDisabled_xpa19_17",De="_error_xpa19_21",Ne="_body_xpa19_25";var _={container:Ee,header:xe,buttons:Te,button:_e,buttonDisabled:Se,error:De,body:Ne};function U({type:e}){return g.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:g.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:g.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:g.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 qe({tune:e,defaultSynth:a,hideOutsideView:n=!1,theme:u,init:d,onEvent:l,enableKeyboard:i}){const{code:m,setCode:r,pattern:s,activeCode:M,activateCode:E,evaluateOnly:x,error:T,cycle:b,dirty:S,togglePlay:N,stop:B}=Z({tune:e,defaultSynth:a,autolink:!1,onEvent:l});t.useEffect(()=>{d&&x()},[e,d]);const[w,q]=t.useState(),[h,v]=ce.useInView({threshold:.01}),F=t.useRef(),L=t.useMemo(()=>((v||!n)&&(F.current=!0),v||F.current),[v,n]);return ee({view:w,pattern:s,active:b.started&&!M?.includes("strudel disable-highlighting")}),t.useLayoutEffect(()=>{if(i){const H=async p=>{(p.ctrlKey||p.altKey)&&(p.code==="Enter"?(p.preventDefault(),G(w),await E()):p.code==="Period"&&(b.stop(),p.preventDefault()))};return window.addEventListener("keydown",H,!0),()=>window.removeEventListener("keydown",H,!0)}},[i,s,m,E,b,w]),g.default.createElement("div",{className:_.container,ref:h},g.default.createElement("div",{className:_.header},g.default.createElement("div",{className:_.buttons},g.default.createElement("button",{className:z(_.button,b.started?"sc-animate-pulse":""),onClick:()=>N()},g.default.createElement(U,{type:b.started?"pause":"play"})),g.default.createElement("button",{className:z(S?_.button:_.buttonDisabled),onClick:()=>E()},g.default.createElement(U,{type:"refresh"}))),T&&g.default.createElement("div",{className:_.error},T.message)),g.default.createElement("div",{className:_.body},L&&g.default.createElement(J,{value:m,onChange:r,onViewChanged:q})))}function Re(e){const{ready:a,connected:n,disconnected:u}=e,[d,l]=t.useState(!0),[i,m]=t.useState(C.WebMidi?.outputs||[]);return t.useEffect(()=>{C.enableWebMidi().then(()=>{C.WebMidi.addListener("connected",s=>{m([...C.WebMidi.outputs]),n?.(C.WebMidi,s)}),C.WebMidi.addListener("disconnected",s=>{m([...C.WebMidi.outputs]),u?.(C.WebMidi,s)}),a?.(C.WebMidi),l(!1)}).catch(s=>{if(s){console.error(s),console.warn("Web Midi could not be enabled..");return}})},[a,n,u,i]),{loading:d,outputs:i,outputByName:s=>C.WebMidi.getOutputByName(s)}}exports.CodeMirror=J;exports.MiniRepl=qe;exports.cx=z;exports.flash=G;exports.useCycle=X;exports.useHighlighting=ee;exports.usePostMessage=Y;exports.useRepl=Z;exports.useWebMidi=Re; diff --git a/packages/react/dist/index.es.js b/packages/react/dist/index.es.js index a0269039..739f406f 100644 --- a/packages/react/dist/index.es.js +++ b/packages/react/dist/index.es.js @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; +import React, { useState, useEffect, useCallback, useMemo, useRef, useLayoutEffect } from 'react'; import { CodeMirror as CodeMirror$1 } from 'react-codemirror6'; import { EditorView, Decoration } from '@codemirror/view'; import { StateEffect, StateField } from '@codemirror/state'; @@ -46,7 +46,13 @@ const materialPalenightTheme = EditorView.theme( lineHeight: '22px', }, '.cm-line': { - background: '#2C323699', + // background: '#2C323699', + background: 'transparent', + }, + '.cm-line > *': { + // background: '#2C323699', + background: '#00000090', + // background: 'transparent', }, // done '&.cm-focused .cm-cursor': { @@ -71,7 +77,8 @@ const materialPalenightTheme = EditorView.theme( backgroundColor: '#6199ff2f', }, - '.cm-activeLine': { backgroundColor: highlightBackground }, + // commented out because it looks bad in mini repl one liners + //'.cm-activeLine': { backgroundColor: cursor + '50' }, '.cm-selectionMatch': { backgroundColor: '#aafe661a' }, '&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': { @@ -193,7 +200,7 @@ const highlightField = StateField.define({ if (from > l || to > l) { return; } - const mark = Decoration.mark({ attributes: { style: `outline: 1px solid ${color}` } }); + const mark = Decoration.mark({ attributes: { style: `outline: 1.5px solid ${color};` } }); return mark.range(from, to); })).filter(Boolean), true); } @@ -351,6 +358,8 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP } }, [activeCode, onDrawProp]); + const hideHeader = useMemo(() => activeCode && activeCode.includes('strudel hide-header'), [activeCode]); + const hideConsole = useMemo(() => activeCode && activeCode.includes('strudel hide-console'), [activeCode]); // cycle hook to control scheduling const cycle = useCycle({ onDraw, @@ -449,6 +458,8 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP }; return { + hideHeader, + hideConsole, pending, code, setCode, @@ -521,13 +532,13 @@ var tailwind = ''; var style = ''; -const container = "_container_10e1g_1"; -const header = "_header_10e1g_5"; -const buttons = "_buttons_10e1g_9"; -const button = "_button_10e1g_9"; -const buttonDisabled = "_buttonDisabled_10e1g_17"; -const error = "_error_10e1g_21"; -const body = "_body_10e1g_25"; +const container = "_container_xpa19_1"; +const header = "_header_xpa19_5"; +const buttons = "_buttons_xpa19_9"; +const button = "_button_xpa19_9"; +const buttonDisabled = "_buttonDisabled_xpa19_17"; +const error = "_error_xpa19_21"; +const body = "_body_xpa19_25"; var styles = { container: container, header: header, @@ -563,12 +574,16 @@ function Icon({ type }) { }[type]); } -function MiniRepl({ tune, defaultSynth, hideOutsideView = false }) { - const { code, setCode, pattern, activateCode, error, cycle, dirty, togglePlay } = useRepl({ +function MiniRepl({ tune, defaultSynth, hideOutsideView = false, theme, init, onEvent, enableKeyboard }) { + const { code, setCode, pattern, activeCode, activateCode, evaluateOnly, error, cycle, dirty, togglePlay, stop } = useRepl({ tune, defaultSynth, - autolink: false + autolink: false, + onEvent }); + useEffect(() => { + init && evaluateOnly(); + }, [tune, init]); const [view, setView] = useState(); const [ref, isVisible] = useInView({ threshold: 0.01 @@ -580,7 +595,25 @@ function MiniRepl({ tune, defaultSynth, hideOutsideView = false }) { } return isVisible || wasVisible.current; }, [isVisible, hideOutsideView]); - useHighlighting({ view, pattern, active: cycle.started }); + useHighlighting({ view, pattern, active: cycle.started && !activeCode?.includes("strudel disable-highlighting") }); + useLayoutEffect(() => { + if (enableKeyboard) { + const handleKeyPress = async (e) => { + if (e.ctrlKey || e.altKey) { + if (e.code === "Enter") { + e.preventDefault(); + flash(view); + await activateCode(); + } else if (e.code === "Period") { + cycle.stop(); + e.preventDefault(); + } + } + }; + window.addEventListener("keydown", handleKeyPress, true); + return () => window.removeEventListener("keydown", handleKeyPress, true); + } + }, [enableKeyboard, pattern, code, activateCode, cycle, view]); return /* @__PURE__ */ React.createElement("div", { className: styles.container, ref diff --git a/packages/react/dist/style.css b/packages/react/dist/style.css index 1ca85d38..1b9d85d7 100644 --- a/packages/react/dist/style.css +++ b/packages/react/dist/style.css @@ -1 +1 @@ -*,:before,:after{--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.sc-h-5{height:1.25rem}.sc-w-5{width:1.25rem}@keyframes sc-pulse{50%{opacity:.5}}.sc-animate-pulse{animation:sc-pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cm-editor{background-color:transparent!important}._container_10e1g_1{overflow:hidden;border-radius:.375rem;--tw-bg-opacity: 1;background-color:rgb(68 76 87 / var(--tw-bg-opacity))}._header_10e1g_5{display:flex;justify-content:space-between;border-top-width:1px;--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity))}._buttons_10e1g_9{display:flex}._button_10e1g_9{display:flex;width:4rem;cursor:pointer;align-items:center;justify-content:center;border-right-width:1px;--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity));padding:.25rem;--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}._button_10e1g_9:hover{--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity))}._buttonDisabled_10e1g_17{display:flex;width:4rem;cursor:pointer;cursor:not-allowed;align-items:center;justify-content:center;--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity));padding:.25rem;--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}._error_10e1g_21{padding:.25rem;text-align:right;font-size:.875rem;line-height:1.25rem;--tw-text-opacity: 1;color:rgb(254 202 202 / var(--tw-text-opacity))}._body_10e1g_25{position:relative;overflow:auto} +*,:before,:after{--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.sc-h-5{height:1.25rem}.sc-w-5{width:1.25rem}@keyframes sc-pulse{50%{opacity:.5}}.sc-animate-pulse{animation:sc-pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cm-editor{background-color:transparent!important}._container_xpa19_1{overflow:hidden;border-radius:.375rem;--tw-bg-opacity: 1;background-color:rgb(17 17 17 / var(--tw-bg-opacity))}._header_xpa19_5{display:flex;justify-content:space-between;border-top-width:1px;--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity))}._buttons_xpa19_9{display:flex}._button_xpa19_9{display:flex;width:4rem;cursor:pointer;align-items:center;justify-content:center;border-right-width:1px;--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity));padding:.25rem;--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}._button_xpa19_9:hover{--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity))}._buttonDisabled_xpa19_17{display:flex;width:4rem;cursor:pointer;cursor:not-allowed;align-items:center;justify-content:center;--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity));padding:.25rem;--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}._error_xpa19_21{padding:.25rem;text-align:right;font-size:.875rem;line-height:1.25rem;--tw-text-opacity: 1;color:rgb(254 202 202 / var(--tw-text-opacity))}._body_xpa19_25{position:relative;overflow:auto} diff --git a/packages/react/src/components/CodeMirror6.jsx b/packages/react/src/components/CodeMirror6.jsx index 74051a97..c8c7f1c2 100644 --- a/packages/react/src/components/CodeMirror6.jsx +++ b/packages/react/src/components/CodeMirror6.jsx @@ -60,7 +60,8 @@ const highlightField = StateField.define({ if (from > l || to > l) { return; // dont mark outside of range, as it will throw an error } - const mark = Decoration.mark({ attributes: { style: `outline: 1px solid ${color}` } }); + // const mark = Decoration.mark({ attributes: { style: `outline: 1px solid ${color}` } }); + const mark = Decoration.mark({ attributes: { style: `outline: 1.5px solid ${color};` } }); return mark.range(from, to); }), ) diff --git a/packages/react/src/components/MiniRepl.jsx b/packages/react/src/components/MiniRepl.jsx index 244911ae..d4807138 100644 --- a/packages/react/src/components/MiniRepl.jsx +++ b/packages/react/src/components/MiniRepl.jsx @@ -1,20 +1,25 @@ -import React, { useState, useMemo, useRef } from 'react'; +import React, { useState, useMemo, useRef, useEffect, useLayoutEffect } from 'react'; import { useInView } from 'react-hook-inview'; import useRepl from '../hooks/useRepl.mjs'; import cx from '../cx'; import useHighlighting from '../hooks/useHighlighting.mjs'; -import CodeMirror6 from './CodeMirror6'; +import CodeMirror6, { flash } from './CodeMirror6'; import 'tailwindcss/tailwind.css'; import './style.css'; import styles from './MiniRepl.module.css'; import { Icon } from './Icon'; -export function MiniRepl({ tune, defaultSynth, hideOutsideView = false }) { - const { code, setCode, pattern, activateCode, error, cycle, dirty, togglePlay } = useRepl({ - tune, - defaultSynth, - autolink: false, - }); +export function MiniRepl({ tune, defaultSynth, hideOutsideView = false, theme, init, onEvent, enableKeyboard }) { + const { code, setCode, pattern, activeCode, activateCode, evaluateOnly, error, cycle, dirty, togglePlay, stop } = + useRepl({ + tune, + defaultSynth, + autolink: false, + onEvent, + }); + useEffect(() => { + init && evaluateOnly(); + }, [tune, init]); const [view, setView] = useState(); const [ref, isVisible] = useInView({ threshold: 0.01, @@ -26,7 +31,28 @@ export function MiniRepl({ tune, defaultSynth, hideOutsideView = false }) { } return isVisible || wasVisible.current; }, [isVisible, hideOutsideView]); - useHighlighting({ view, pattern, active: cycle.started }); + useHighlighting({ view, pattern, active: cycle.started && !activeCode?.includes('strudel disable-highlighting') }); + + // set active pattern on ctrl+enter + useLayoutEffect(() => { + if (enableKeyboard) { + const handleKeyPress = async (e) => { + if (e.ctrlKey || e.altKey) { + if (e.code === 'Enter') { + e.preventDefault(); + flash(view); + await activateCode(); + } else if (e.code === 'Period') { + cycle.stop(); + e.preventDefault(); + } + } + }; + window.addEventListener('keydown', handleKeyPress, true); + return () => window.removeEventListener('keydown', handleKeyPress, true); + } + }, [enableKeyboard, pattern, code, activateCode, cycle, view]); + return (
@@ -40,7 +66,7 @@ export function MiniRepl({ tune, defaultSynth, hideOutsideView = false }) {
{error &&
{error.message}
}
-
+
{show && }
diff --git a/packages/react/src/components/MiniRepl.module.css b/packages/react/src/components/MiniRepl.module.css index 20c023df..7826f9fb 100644 --- a/packages/react/src/components/MiniRepl.module.css +++ b/packages/react/src/components/MiniRepl.module.css @@ -1,5 +1,5 @@ .container { - @apply sc-rounded-md sc-overflow-hidden sc-bg-[#444C57]; + @apply sc-rounded-md sc-overflow-hidden sc-bg-[#111111]; } .header { diff --git a/packages/react/src/hooks/useRepl.mjs b/packages/react/src/hooks/useRepl.mjs index 99e697ee..d7f3041f 100644 --- a/packages/react/src/hooks/useRepl.mjs +++ b/packages/react/src/hooks/useRepl.mjs @@ -36,6 +36,8 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP } }, [activeCode, onDrawProp]); + const hideHeader = useMemo(() => activeCode && activeCode.includes('strudel hide-header'), [activeCode]); + const hideConsole = useMemo(() => activeCode && activeCode.includes('strudel hide-console'), [activeCode]); // cycle hook to control scheduling const cycle = useCycle({ onDraw, @@ -136,6 +138,8 @@ function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawP }; return { + hideHeader, + hideConsole, pending, code, setCode, diff --git a/packages/react/src/themes/material-palenight.js b/packages/react/src/themes/material-palenight.js index 59117ec5..64bbd461 100644 --- a/packages/react/src/themes/material-palenight.js +++ b/packages/react/src/themes/material-palenight.js @@ -36,7 +36,13 @@ export const materialPalenightTheme = EditorView.theme( lineHeight: '22px', }, '.cm-line': { - background: '#2C323699', + // background: '#2C323699', + background: 'transparent', + }, + '.cm-line > *': { + // background: '#2C323699', + background: '#00000090', + // background: 'transparent', }, // done '&.cm-focused .cm-cursor': { @@ -61,7 +67,8 @@ export const materialPalenightTheme = EditorView.theme( backgroundColor: '#6199ff2f', }, - '.cm-activeLine': { backgroundColor: highlightBackground }, + // commented out because it looks bad in mini repl one liners + //'.cm-activeLine': { backgroundColor: cursor + '50' }, '.cm-selectionMatch': { backgroundColor: '#aafe661a' }, '&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': { diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 8a13e30c..aa6262ed 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -126,146 +126,150 @@ const splitSN = (s, n) => { Pattern.prototype.out = function () { return this.onTrigger(async (t, hap, ct) => { - const ac = getAudioContext(); - // calculate correct time (tone.js workaround) - t = ac.currentTime + t - ct; - // destructure value - let { - freq, - s, - sf, - clip = 0, // if 1, samples will be cut off when the hap ends - n = 0, - note, - gain = 1, - cutoff, - resonance = 1, - hcutoff, - hresonance = 1, - bandf, - bandq = 1, - pan, - attack = 0.001, - decay = 0.05, - sustain = 0.5, - release = 0.001, - speed = 1, // sample playback speed - begin = 0, - end = 1, - } = hap.value; - const { velocity = 1 } = hap.context; - gain *= velocity; // legacy fix for velocity - // the chain will hold all audio nodes that connect to each other - const chain = []; - if (typeof s === 'string') { - [s, n] = splitSN(s, n); - } - if (typeof note === 'string') { - [note, n] = splitSN(note, n); - } - if (!s || ['sine', 'square', 'triangle', 'sawtooth'].includes(s)) { - // with synths, n and note are the same thing - n = note || n; - if (typeof n === 'string') { - n = toMidi(n); // e.g. c3 => 48 + try { + const ac = getAudioContext(); + // calculate correct time (tone.js workaround) + t = ac.currentTime + t - ct; + // destructure value + let { + freq, + s, + sf, + clip = 0, // if 1, samples will be cut off when the hap ends + n = 0, + note, + gain = 1, + cutoff, + resonance = 1, + hcutoff, + hresonance = 1, + bandf, + bandq = 1, + pan, + attack = 0.001, + decay = 0.05, + sustain = 0.5, + release = 0.001, + speed = 1, // sample playback speed + begin = 0, + end = 1, + } = hap.value; + const { velocity = 1 } = hap.context; + gain *= velocity; // legacy fix for velocity + // the chain will hold all audio nodes that connect to each other + const chain = []; + if (typeof s === 'string') { + [s, n] = splitSN(s, n); } - // get frequency - if (!freq && typeof n === 'number') { - freq = fromMidi(n); // + 48); + if (typeof note === 'string') { + [note, n] = splitSN(note, n); } - // make oscillator - const o = getOscillator({ t, s, freq, duration: hap.duration, release }); - chain.push(o); - // level down oscillators as they are really loud compared to samples i've tested - const g = ac.createGain(); - g.gain.value = 0.3; - chain.push(g); - // TODO: make adsr work with samples without pops - // envelope - const adsr = getADSR(attack, decay, sustain, release, 1, t, t + hap.duration); - chain.push(adsr); - } else { - // load sample - if (speed === 0) { - // no playback - return; - } - if (!s) { - console.warn('no sample specified'); - return; - } - const soundfont = getSoundfontKey(s); - let bufferSource; - - try { - if (soundfont) { - // is soundfont - bufferSource = await globalThis.getFontBufferSource(soundfont, note || n, ac); - } else { - // is sample from loaded samples(..) - bufferSource = await getSampleBufferSource(s, n, note); + if (!s || ['sine', 'square', 'triangle', 'sawtooth'].includes(s)) { + // with synths, n and note are the same thing + n = note || n; + if (typeof n === 'string') { + n = toMidi(n); // e.g. c3 => 48 } - } catch (err) { - console.warn(err); - return; - } - // asny stuff above took too long? - if (ac.currentTime > t) { - console.warn('sample still loading:', s, n); - return; - } - if (!bufferSource) { - console.warn('no buffer source'); - return; - } - bufferSource.playbackRate.value = Math.abs(speed) * bufferSource.playbackRate.value; - // TODO: nudge, unit, cut, loop - let duration = soundfont || clip ? hap.duration : bufferSource.buffer.duration; - // let duration = bufferSource.buffer.duration; - const offset = begin * duration; - duration = ((end - begin) * duration) / Math.abs(speed); - if (soundfont || clip) { - bufferSource.start(t, offset); // duration does not work here for some reason + // get frequency + if (!freq && typeof n === 'number') { + freq = fromMidi(n); // + 48); + } + // make oscillator + const o = getOscillator({ t, s, freq, duration: hap.duration, release }); + chain.push(o); + // level down oscillators as they are really loud compared to samples i've tested + const g = ac.createGain(); + g.gain.value = 0.3; + chain.push(g); + // TODO: make adsr work with samples without pops + // envelope + const adsr = getADSR(attack, decay, sustain, release, 1, t, t + hap.duration); + chain.push(adsr); } else { - bufferSource.start(t, offset, duration); - } - chain.push(bufferSource); - if (soundfont || clip) { - const env = ac.createGain(); - const releaseLength = 0.1; - env.gain.value = 0.6; - env.gain.setValueAtTime(env.gain.value, t + duration); - env.gain.linearRampToValueAtTime(0, t + duration + releaseLength); - // env.gain.linearRampToValueAtTime(0, t + duration + releaseLength); - chain.push(env); - bufferSource.stop(t + duration + releaseLength); - } else { - bufferSource.stop(t + duration); - } - } - // master out - const master = ac.createGain(); - master.gain.value = gain; - chain.push(master); + // load sample + if (speed === 0) { + // no playback + return; + } + if (!s) { + console.warn('no sample specified'); + return; + } + const soundfont = getSoundfontKey(s); + let bufferSource; - // filters - cutoff !== undefined && chain.push(getFilter('lowpass', cutoff, resonance)); - hcutoff !== undefined && chain.push(getFilter('highpass', hcutoff, hresonance)); - bandf !== undefined && chain.push(getFilter('bandpass', bandf, bandq)); - // TODO vowel - // TODO delay / delaytime / delayfeedback - // panning - if (pan !== undefined) { - const panner = ac.createStereoPanner(); - panner.pan.value = 2 * pan - 1; - chain.push(panner); - } - // master out - /* const master = ac.createGain(); + try { + if (soundfont) { + // is soundfont + bufferSource = await globalThis.getFontBufferSource(soundfont, note || n, ac); + } else { + // is sample from loaded samples(..) + bufferSource = await getSampleBufferSource(s, n, note); + } + } catch (err) { + console.warn(err); + return; + } + // asny stuff above took too long? + if (ac.currentTime > t) { + console.warn('sample still loading:', s, n); + return; + } + if (!bufferSource) { + console.warn('no buffer source'); + return; + } + bufferSource.playbackRate.value = Math.abs(speed) * bufferSource.playbackRate.value; + // TODO: nudge, unit, cut, loop + let duration = soundfont || clip ? hap.duration : bufferSource.buffer.duration; + // let duration = bufferSource.buffer.duration; + const offset = begin * duration; + duration = ((end - begin) * duration) / Math.abs(speed); + if (soundfont || clip) { + bufferSource.start(t, offset); // duration does not work here for some reason + } else { + bufferSource.start(t, offset, duration); + } + chain.push(bufferSource); + if (soundfont || clip) { + const env = ac.createGain(); + const releaseLength = 0.1; + env.gain.value = 0.6; + env.gain.setValueAtTime(env.gain.value, t + duration); + env.gain.linearRampToValueAtTime(0, t + duration + releaseLength); + // env.gain.linearRampToValueAtTime(0, t + duration + releaseLength); + chain.push(env); + bufferSource.stop(t + duration + releaseLength); + } else { + bufferSource.stop(t + duration); + } + } + // master out + const master = ac.createGain(); + master.gain.value = gain; + chain.push(master); + + // filters + cutoff !== undefined && chain.push(getFilter('lowpass', cutoff, resonance)); + hcutoff !== undefined && chain.push(getFilter('highpass', hcutoff, hresonance)); + bandf !== undefined && chain.push(getFilter('bandpass', bandf, bandq)); + // TODO vowel + // TODO delay / delaytime / delayfeedback + // panning + if (pan !== undefined) { + const panner = ac.createStereoPanner(); + panner.pan.value = 2 * pan - 1; + chain.push(panner); + } + // master out + /* const master = ac.createGain(); master.gain.value = 0.8 * gain; chain.push(master); */ - chain.push(ac.destination); - // connect chain elements together - chain.slice(1).reduce((last, current) => last.connect(current), chain[0]); + chain.push(ac.destination); + // connect chain elements together + chain.slice(1).reduce((last, current) => last.connect(current), chain[0]); + } catch (e) { + console.warn('.out error:', e); + } }); }; diff --git a/repl/.gitignore b/repl/.gitignore index 7e93270e..b9ef2472 100644 --- a/repl/.gitignore +++ b/repl/.gitignore @@ -23,4 +23,5 @@ dist-ssr *.sln *.sw? -oldtunes.mjs \ No newline at end of file +oldtunes.mjs +public/samples/EMU World/ \ No newline at end of file diff --git a/repl/src/App.css b/repl/src/App.css index 9c8edd83..f0bc85c7 100644 --- a/repl/src/App.css +++ b/repl/src/App.css @@ -3,7 +3,7 @@ @tailwind utilities; body { - background-color: #2a3236; + background-color: #111; } .react-codemirror2, @@ -15,12 +15,12 @@ body { } .CodeMirror-line > span { - background-color: #2a323699; + background-color: #11111190; } .darken::before { content: ' '; - position: absolute; + position: fixed; top: 0; left: 0; width: 100vw; diff --git a/repl/src/App.jsx b/repl/src/App.jsx index 27c7310a..ce9be045 100644 --- a/repl/src/App.jsx +++ b/repl/src/App.jsx @@ -108,6 +108,8 @@ function App() { pattern, pushLog, pending, + hideHeader, + hideConsole, } = useRepl({ tune: '// LOADING...', defaultSynth, @@ -167,142 +169,149 @@ function App() { return (
- + )}
-
+
{/* onCursor={markParens} */} - + {!cycle.started ? `press ctrl+enter to play\n` : dirty ? `ctrl+enter to update\n` : 'no changes\n'} {error && ( -
+
{error?.message || 'unknown error'}
)}
- {!isEmbedded && ( + {!isEmbedded && !hideConsole && (